A production-grade, privacy-first browser extension for verifying technical-interview integrity through behavioral, environmental, and system-level signals.
- Layer 1: Behavioral — Focus changes, paste events, typing cadence, mouse presence
- Layer 2: Environmental — Cell phone detection, multi-person detection (camera-based object detection, no facial analysis)
- Layer 3: System — Optional process blacklist monitoring (opt-in native companion app)
- Risk Engine — Correlated scoring with GREEN/YELLOW/RED classification (RED requires ≥2 independent layer signals)
- Recruiter Dashboard — React-based timeline view of flagged events and risk assessments
- ✅ No gaze tracking, facial emotion analysis, or general behavior judgment
- ✅ No code content capture; metadata only (timestamps, lengths, event types)
- ✅ All ML inference client-side; no raw media leaves the browser
- ✅ Camera snapshots sent only when detection threshold is breached
- ✅ No automated session termination; flags are for human review only
- ✅ Minimal permissions; each justified in code comments
interview-extension/
├── extension/ # Layer 1 & 2, manifest.json, content-script, service-worker
├── backend/ # Ingestion API, Risk Engine, Supabase integration
├── dashboard/ # React recruiter UI
├── native-host/ # Layer 3 (optional): process monitoring
├── docker-compose.yml # Local Docker deployment entrypoint
├── k8s/ # Kubernetes manifests
└── shared/ # TypeScript data contracts
- Node.js 18+
- Supabase project with a service role key
- Docker Desktop (optional, for local container runs)
- Chrome 120+ (for MV3)
cd extension
# Compile TypeScript
npm install -g typescript
tsc content-script.ts --outDir .
tsc background.ts --outDir .
# Load unpacked extension in Chrome:
# 1. chrome://extensions/
# 2. Enable "Developer mode" (top right)
# 3. "Load unpacked"
# 4. Select ./extension folderKey Files:
manifest.json— MV3 permissions (activeTab, camera optional)content-script.ts— Layer 1 event capturebackground.ts— Batching + API callslayer2.html— Camera + TensorFlow.js for Layer 2popup.html— Extension popup UIonboarding.html— First-run consent flowsettings.html— User-configurable settings
cd backend
# Install dependencies
npm install
# Configure backend/.env with your Supabase values
# The service will create its required tables automatically on startup
# Start server (development)
npm run dev
# Server listens on http://localhost:3000
# POST /api/ingest — accept event batches from extension
# GET /api/sessions — list sessions
# GET /api/session/:id — get session details + eventsService Setup:
- Put your Supabase values in backend/.env
- The backend uses
SUPABASE_DIRECT_CONNECTION_STRINGfor the database connection - The startup flow creates the required tables automatically, so there is no manual DB setup step
Database Schema:
Session— interview metadata + aggregated risk stateRiskEvent— individual events (Layer 1, 2, or 3)SessionRiskState— cached risk classification for fast queries
Risk Engine Logic:
- Scores events individually per layer
- Correlates events across layers within a 30s window
- RED = ≥2 independent layer signals correlated (constraint C6)
- YELLOW = single minor signal
- GREEN = no material anomalies
cd dashboard
# Install & start dev server
npm install
npm run dev
# Runs on http://localhost:5173
# Displays: session list, timeline, risk levels, contributing events
# Build for production
npm run build
# Output: dist/Pages:
- Home: Session list with filter (RED/YELLOW/GREEN)
- Detail: Full timeline with event metadata and snapshots
cd native-host
# Build
npm install
npm run build
# Register with Chrome (platform-specific):
# === Windows ===
# 1. Create manifest.json in:
# C:\Users\<USER>\AppData\Local\Google\Chrome\NativeMessagingHosts\
# 2. Manifest contents:
#{
# "name": "com.interviewintegrity.layer3",
# "description": "Layer 3 process monitoring",
# "path": "C:\\path\\to\\native-host\\dist\\index.js",
# "type": "stdio",
# "allowed_origins": [
# "chrome-extension://YOUR_EXTENSION_ID/"
# ]
#}
# === macOS/Linux ===
# 1. Create manifest in ~/.config/google-chrome/NativeMessagingHosts/
# 2. Same format, adjust path to your system
# === Test ===
node dist/index.js
# Send JSON messages via stdin; receive responses via stdoutProcess Blacklist:
- Screen-share tools (Zoom, Skype, TeamViewer, etc.)
- Remote desktop (RDP, TeamViewer, VNC, Chrome Remote Desktop)
- AI assistants (Copilot, Codeium, TabNine)
- Suspicious debuggers
Privacy Note: Reports only matched category tags (e.g., ["screen-share-tool-detected"]), never a full process list (constraint C8).
{
layer1Enabled: true,
typingBurstThreshold: 150, // chars/sec
pasteLengthThreshold: 500, // bytes
layer2Enabled: false, // requires camera permission
confidenceThreshold: 0.7, // 0–1, for COCO-SSD
sampleInterval: 2.5, // seconds between camera frames
layer3Enabled: false, // requires native host
apiUrl: "http://localhost:3000/api",
batchInterval: 4, // seconds
correlationWindow: 30, // seconds (C6)
}Create backend/.env with:
SUPABASE_URL=https://<project-ref>.supabase.co
SUPABASE_SERVICE_ROLE_KEY=<service-role-key>
SUPABASE_DIRECT_CONNECTION_STRING=postgresql://postgres:<password>@db.<project-ref>.supabase.co:5432/postgres
PORT=3000
NODE_ENV=developmentThe backend reads these values automatically at startup.
Default weights (configurable in Risk Engine):
- Layer 1 Focus Change: 0.1 (common, low weight)
- Layer 1 Paste: 0.15 (size-dependent)
- Layer 1 Typing Burst: 0.05 (not suspicious alone)
- Layer 2 Phone: 0.5 (strong signal, needs corroboration)
- Layer 2 Multi-Person: 0.4 (strong signal)
- Layer 3 Process Match: 0.6 (very strong, especially with Layer 2)
Correlation (C6): Two events from different layers within 30s → escalate to RED.
docker compose up --buildThis starts the backend service on port 3000 using the environment variables from your shell or Docker environment.
kubectl apply -f k8s/Make sure the interview-secrets secret exists with the same Supabase environment variables.
###Render
Render can host the backend as a web service.
- Create a new Web Service in Render and point it to this repository.
- Set the build command to:
npm --prefix backend install && npm --prefix backend run build - Set the start command to:
npm --prefix backend start
- Add these environment variables in Render:
SUPABASE_URL=https://<project-ref>.supabase.co SUPABASE_SERVICE_ROLE_KEY=<service-role-key> SUPABASE_DIRECT_CONNECTION_STRING=postgresql://postgres:<password>@db.<project-ref>.supabase.co:5432/postgres PORT=10000 NODE_ENV=production
- Render will expose the service on its generated URL.
Update the extension and dashboard API URLs to the Render-hosted backend URL when you deploy.
// content-script.ts tests:
// T2: Single focus_change → YELLOW
// T3: Organic typing burst (plausible human cadence) → no flag
// T4: paste event → logged with length, never content// layer2.html + COCO-SSD tests:
// T5: Cell phone in frame (confidence > 0.7) → phone_detected + snapshot
// T6: Normal scene (one person, no phone) → no flag// risk-engine.ts tests:
// T1: No events → GREEN
// T2: Single signal → YELLOW (not RED, per C6)
// T7: Phone detected (Layer 2) + blacklist match (Layer 3) within 30s → RED
// T8: Phone alone (no Layer 3 signal) → not RED (pending)- Candidate consent/disclosure copy reviewed
- Data retention and deletion policy finalized
- Regional law review (BIPA, GDPR, state wiretap laws)
- Privacy policy published and linked in onboarding
- All Layer 1 permissions justified in manifest comments
- Layer 2 camera access only via dedicated extension page, not injected script
- Layer 3 clearly optional; separate install with explicit consent
- No test/debug code in production build
- Supabase backup / retention policy in place
- API authentication (JWT or OAuth) added for recruiter dashboard
- Rate limiting and DDoS protection enabled
- Logs redacted of sensitive data
- CORS configured to allow dashboard origin only
- No candidate data displayed in session list (only recruiter-facing)
- Event timestamps in recruiter's timezone
- Snapshots blurred or redacted before recruiter view (optional privacy layer)
- Check
chrome://extensions/→ Extension ID and version - Verify interview page matches
manifest.jsonmatchespatterns - Open DevTools (F12) on interview page → Console for
[Layer 1]logs - Check background service worker logs:
chrome://extensions→ Inspect background page
- Verify
background.tsis sending to correct API URL - Check
http://localhost:3000/healthfor API health - Ensure your Supabase project is reachable and your service role key is configured
- Check backend logs for ingestion errors
- Review event weights in
risk-engine.tsfor your platform - Adjust
CORRELATION_WINDOW_MSif events are too far apart - Increase
YELLOW_THRESHOLDif you want fewer YELLOW flags - Verify
contributingEventsarray in/api/session/:idto see which events triggered the score
- Check
layer2.htmlin extension → Browser console for[Layer 2]logs - Verify camera permission granted:
chrome://settings/content/camera - Ensure TensorFlow.js and COCO-SSD load from CDN (check network tab)
- Try in incognito mode to rule out conflicting extensions
- Verify manifest registered correctly:
- Windows:
regedit→HKEY_CURRENT_USER\Software\Google\Chrome\NativeMessagingHosts - macOS:
ls ~/.config/google-chrome/NativeMessagingHosts/
- Windows:
- Test native host standalone:
node dist/index.js→ send JSON to stdin - Check extension error logs for native messaging failures
- Rebuild and re-register if you moved the native host binary
- Type safety across extension, backend, and dashboard
- Shared
types.tsensures data contract consistency - Better tooling for Chrome API types (DefinitelyTyped)
- Managed Postgres under the hood
- Type-safe SDK access
- Easy schema evolution and deployment
- Real-time updates via polling (could add WebSocket later)
- Component reusability (card, timeline, badge)
- Large ecosystem for charts/visualization if needed
- Browser extensions cannot enumerate OS-level processes
- Node.js chosen for stack consistency (could switch to Rust for smaller binary)
- Native messaging keeps Layer 3 optional (no dependency in extension code)
- C1 — No code content: only metadata (paste length, not pasted text)
- C2 — No gaze/facial analysis: only phone and person count detection
- C3 — One snapshot per flag: no video clips
- C4 — No auto-termination: RED flags are for human review
- C5 — Minimal permissions: only activeTab + camera (optional)
- C6 — Multi-layer correlation: RED requires ≥2 independent signals within 30s
- C7 — Layer 3 opt-in: separate install, disclosed to candidate
- C8 — No process dump: only category tags
- C9 — Client-side inference: no raw media to backend
Proprietary. Built with privacy and transparency as core values.
Questions or issues? Review the spec in interview-integrity-extension-prompt-optimized.md or contact the development team.