@muralidhar55/exam-proctor-sdk
v1.1.4
Published
A Node.js SDK for integrating exam proctoring features(Registration + verification + offline proctoring) into your applications.
Maintainers
Readme
@muralidhar55/exam-proctor-sdk
Lightweight Express middleware for in-page exam proctoring — no iframes, no sidecars, no separate processes.
Overview
@muralidhar55/exam-proctor-sdk mounts into your existing Express app and adds a complete
browser-side proctoring flow:
- face registration
- identity verification
- in-exam proctoring
- violation collection
- same-origin SDK routes
It does not require iframes, a sidecar process, or a separate Python service.
Features
- Express middleware integration
- Fast AI: Dual-layer caching (Service Worker + IndexedDB) for 0MB model downloads after first load.
- Low Config Support: Motion-detection based AI throttling for old/weak CPUs.
- Browser-based face registration and verification
- In-page proctoring runtime
- Same-origin routes for register, verify, worker, models, and violations
- Database-agnostic persistence
- Default model CDN with optional self-hosting
- Graceful fallback when no database is configured
Install
npm install @muralidhar55/exam-proctor-sdkQuick Start (30 Seconds)
1. Setup Environment
Add these to your .env file:
PORT=3000
PROCTOR_SESSION_JWT_SECRET=your_super_secret_key
# The internal API endpoint where violations will be persisted
PROCTOR_DATABASE_URL=http://localhost:3000/proctor/api/violations2. Implementation
const express = require('express');
const examProctor = require('@muralidhar55/exam-proctor-sdk');
require('dotenv').config();
const app = express();
app.use(express.json());
// Mount the SDK
app.use(examProctor.middleware({
databaseUrl: process.env.PROCTOR_DATABASE_URL,
sessionJwtSecret: process.env.PROCTOR_SESSION_JWT_SECRET,
// High-performance models pinned to v1.1.3
modelsBaseUrl: 'https://cdn.jsdelivr.net/gh/muralidhar55/violation-detect@models-cdn/v1.1.3',
allowedOrigins: ['http://localhost:3000'],
}));
app.listen(3000, () => {
console.log('Proctoring enabled at http://localhost:3000/proctor/register');
});What Gets Added To Your App
After app.use(examProctor.middleware(config)), these routes are available:
/proctor/register Student face enrollment page
/proctor/verify Student identity validation (Face Match)
/proctor/api/violations GET, POST (Persistence layer)
/proctor/api/session-token GET (Auth context)
/proctor/proctor-bundle.js Main monitoring runtime script
/proctor/register-bundle.js Registration logic
/proctor/auth-bundle.js Authorization logic
/proctor/proctoringWorker.js Background AI threadHow to use in your Frontend
Once you have mounted the middleware, you can trigger proctoring in your HTML pages.
1. Include the config and bundle
In your server-side template (EJS, Handlebars, etc.):
<head>
<!-- Injects required metadata and SharedArrayBuffer settings -->
${examProctor.configScriptTag(config)}
</head>
<body>
<!-- Include the proctoring runtime -->
<script src="/proctor/proctor-bundle.js"></script>
<script>
async function startExam() {
const sessionId = "student_sid_123";
// Starts camera and real-time AI monitoring
await window.ExamProctor.start(sessionId, "Student Full Name");
console.log("AI Monitoring is active!");
}
</script>
</body>2. Standard Student Flow (Recommended)
We recommend redirecting students through these steps to ensure identity:
- Register: Redirect new students to
/proctor/register. - Verify: On exam day, redirect to
/proctor/verify?redirect=/exam-page. - Proctor: On the
/exam-page, callwindow.ExamProctor.start().
Quick Start
1. Mount the package in Express
const express = require('express');
const examProctor = require('@muralidhar55/exam-proctor-sdk');
const app = express();
app.use(express.json());
app.use(examProctor.middleware({
databaseUrl: 'http://localhost:3000/api/proctor/violations',
sessionJwtSecret: process.env.PROCTOR_SESSION_JWT_SECRET,
modelsBaseUrl: 'https://cdn.jsdelivr.net/gh/muralidhar55/violation-detect@models-cdn/v1.1.3',
allowedOrigins: ['http://localhost:3000'],
}));2. Add your backend violations endpoint
databaseUrl should point to your backend API route, not a raw database
connection string.
app.post('/api/proctor/violations', async (req, res) => {
const violations = Array.isArray(req.body?.violations)
? req.body.violations
: Array.isArray(req.body)
? req.body
: [];
// Save to PostgreSQL / MySQL / MongoDB / Supabase / Prisma / etc.
res.status(201).json({ saved: violations.length });
});
app.get('/api/proctor/violations', async (req, res) => {
const sessionId = String(req.query.sessionId || '').trim();
// Read violations for this session from your database
res.json([]);
});3. Inject the runtime config and bundle on your exam page
app.get('/exam', (req, res) => {
const proctorConfig = {
databaseUrl: 'http://localhost:3000/api/proctor/violations',
sessionJwtSecret: process.env.PROCTOR_SESSION_JWT_SECRET,
modelsBaseUrl: 'https://cdn.jsdelivr.net/gh/Murali55-09/violation-detect@models-cdn/',
allowedOrigins: ['http://localhost:3000'],
};
res.send(`
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
${examProctor.configScriptTag(proctorConfig)}
</head>
<body>
<h1>Exam</h1>
<button id="start-exam">Start Proctoring</button>
<script src="/proctor/proctor-bundle.js"></script>
<script>
document.getElementById('start-exam').addEventListener('click', async () => {
const sessionId = 'session_' + crypto.randomUUID();
await window.ExamProctor.start(sessionId, 'Student Name');
});
</script>
</body>
</html>
`);
});HTML Integration
Minimal HTML example:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<h1>Exam Page</h1>
<button id="start">Start Proctoring</button>
<script src="/proctor/proctor-bundle.js"></script>
<script>
document.getElementById('start').addEventListener('click', async () => {
const sessionId = 'session_' + crypto.randomUUID();
await window.ExamProctor.start(sessionId, 'Student Name');
});
</script>
</body>
</html>Recommended Exam Flow
/ -> /proctor/register -> /proctor/verify -> /exam -> /resultHelpers:
app.get('/', (_req, res) => {
res.redirect(examProctor.registerUrl());
});
app.get('/verify-start', (req, res) => {
res.redirect(examProctor.verifyUrl(String(req.query.sessionId || '')));
});Database Configuration
This package is database-agnostic.
You can use:
- PostgreSQL
- MySQL
- MongoDB
- Supabase
- Prisma
- any other backend storage layer
Option A: databaseUrl
Use databaseUrl when you already have a backend route for persistence.
app.use(examProctor.middleware({
databaseUrl: 'https://your-app.com/api/proctor/violations',
}));Expected POST request body:
{
"sessionId": "session-123",
"violations": [
{
"id": "v1",
"sessionId": "session-123",
"type": "tab_switch",
"timestamp": 1710000000000,
"severity": "high",
"metadata": {
"reason": "visibility_hidden"
},
"studentName": "Student Name"
}
],
"source": "exam-proctor-sdk"
}Recommended POST response:
{
"saved": 1
}Expected GET request:
GET /api/proctor/violations?sessionId=session-123Recommended GET response:
[
{
"id": "v1",
"sessionId": "session-123",
"type": "tab_switch",
"timestamp": 1710000000000,
"severity": "high",
"metadata": {
"reason": "visibility_hidden"
},
"studentName": "Student Name"
}
]Option B: Persistence callbacks
Use callbacks when you want direct control inside Express:
app.use(examProctor.middleware({
saveViolations: async (violations, ctx) => {
return violations.length;
},
listViolations: async (sessionId, ctx) => {
return [];
},
}));Available callbacks:
saveViolations(violations, ctx)saveViolation(violation, ctx)listViolations(sessionId, ctx)issueSessionToken(sessionId, ctx)
Important Security Note
Do not expose raw database credentials in browser config.
Bad:
DATABASE_URL=postgresql://user:pass@host:5432/dbinside client-side config.
Good:
- keep DB credentials only on your backend
- expose your own backend route
- pass that route to
databaseUrl
Environment Variables
Example backend environment:
PORT=3000
DATABASE_URL=postgresql://user:pass@host:5432/dbname
PROCTOR_SESSION_JWT_SECRET=change-meUsage:
DATABASE_URLused by your backend code to connect to SQL or NoSQL storagePROCTOR_SESSION_JWT_SECRETused by the SDK when signing generic session JWTs
If No Database Is Configured
The package still works.
That means:
- models still load
- registration and verification still work
- proctoring runtime still runs
- the SDK logs a warning
- violations stay in memory for the current process only
This is useful for demos and local testing.
Model Loading
The package fetches model assets at runtime.
Default model base:
https://cdn.jsdelivr.net/gh/Murali55-09/violation-detect@models-cdn/Runtime assets include:
MobileFaceNet.onnxyolov8n_tunned.onnxface_mesh.jsort-wasm-simd-threaded.wasm
If you want to self-host models:
app.use(examProctor.middleware({
modelsBaseUrl: 'https://your-cdn.example.com/models',
}));If you do not set modelsBaseUrl, the default CDN is used.
Frontend API
After loading /proctor/proctor-bundle.js, the browser gets:
await window.ExamProctor.start(sessionId, studentName);
await window.ExamProctor.stop();
window.ExamProctor.getMode();Modes:
fullbasictab-onlystopped
Recommended Headers
Serve pages with:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Resource-Policy: cross-originPostgreSQL Example
const express = require('express');
const { Pool } = require('pg');
const examProctor = require('exam-proctor-sdk');
const app = express();
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
app.use(express.json());
app.post('/api/proctor/violations', async (req, res) => {
const violations = Array.isArray(req.body?.violations) ? req.body.violations : [];
for (const item of violations) {
await pool.query(
`insert into violations (id, session_id, type, timestamp, severity, metadata, student_name)
values ($1, $2, $3, to_timestamp($4 / 1000.0), $5, $6::jsonb, $7)
on conflict (id) do update set
session_id = excluded.session_id,
type = excluded.type,
timestamp = excluded.timestamp,
severity = excluded.severity,
metadata = excluded.metadata,
student_name = excluded.student_name`,
[
item.id,
item.sessionId,
item.type,
item.timestamp,
item.severity,
JSON.stringify(item.metadata || {}),
item.studentName || '',
]
);
}
res.status(201).json({ saved: violations.length });
});
app.get('/api/proctor/violations', async (req, res) => {
const sessionId = String(req.query.sessionId || '').trim();
const result = await pool.query(
'select * from violations where session_id = $1 order by timestamp asc',
[sessionId]
);
res.json(result.rows);
});
app.use(examProctor.middleware({
databaseUrl: 'http://localhost:3000/api/proctor/violations',
sessionJwtSecret: process.env.PROCTOR_SESSION_JWT_SECRET,
}));Configuration Reference
| Key | Type | Description |
|---|---|---|
| mountPath | string | Base route for SDK endpoints. Default: /proctor |
| modelsBaseUrl | string | Base URL for runtime model assets |
| modelsPath | string | Alternative model path under the mounted route |
| databaseUrl | string | Your backend API endpoint for violation writes and reads |
| saveViolations | function | Batch persistence callback |
| saveViolation | function | Single-row persistence callback |
| listViolations | function | Read violations for a session |
| sessionJwtSecret | string | Secret used for generic session JWT signing |
| issueSessionToken | function | Custom token issuer callback |
| sessionTokenUrl | string | Override the default session token route |
| violationsApiUrl | string | Override the default violations API route |
| verifySuccessUrl | string | Redirect target after verification |
| workerPath | string | Override worker route |
| allowedOrigins | string[] \| string | Allowed origins for SDK CORS |
Exports
| Export | Description |
|---|---|
| middleware(config) | Mounts SDK routes into your Express app |
| configScriptTag(config) | Returns an inline config <script> tag |
| registerUrl() | Returns the registration route |
| verifyUrl(sessionId) | Returns the verification route for a session |
| DEFAULT_CONFIG | Default SDK config values |
Troubleshooting
Models do not load
Check:
modelsBaseUrl- network access to model files
- browser console for blocked assets
Proctoring runs but violations are not saved
Check:
databaseUrlpoints to your backend route- your backend accepts JSON
- your backend returns
{ "saved": number } - your database is configured on the backend
I only installed the npm package and nothing happens
Installing the package alone is not enough.
You still must:
- mount the middleware
- inject the SDK script on your exam page
- configure persistence if you want stored violations
License
MIT
