@savannahanddelta/sentinel
v0.1.1
Published
Sentinel presence SDK — privacy-first, provider-agnostic face presence for the web
Maintainers
Readme
@savannahanddelta/sentinel
Privacy-first presence guard. Your camera never leaves your device.
Sentinel watches whether the right person is in front of the screen. When they leave, it blurs or locks your content. When they return, it unblurs. All face processing runs locally in WASM — no video, no images, no biometric data is ever transmitted.
Install
npm install @savannahanddelta/sentinelYou also need the MediaPipe provider:
npm install @savannahanddelta/sentinel-provider-mediapipeQuick start
import { Sentinel } from '@savannahanddelta/sentinel'
import { MediaPipeProvider } from '@savannahanddelta/sentinel-provider-mediapipe'
const sentinel = new Sentinel({
provider: new MediaPipeProvider(),
guards: [
{
selector: '#document-viewer', // CSS selector of the element to protect
action: 'blur', // 'blur' | 'lock' | 'frost'
threshold_ms: 3000, // how long absent before triggering
recovery: 'auto', // 'auto' | 'face' | 'pin'
}
]
})
sentinel.on('absent', ({ duration_ms }) => {
console.log(`User has been away for ${duration_ms}ms`)
})
sentinel.on('present', () => {
console.log('User returned')
})
sentinel.on('spoof', () => {
console.log('Static image detected — locking')
})
await sentinel.start()That's it. Sentinel will:
- Ask for camera permission on
start() - Detect presence continuously at 2–8 fps (adaptive)
- Blur
#document-viewerafter 3 seconds of absence - Unblur automatically when the user returns
- Fire
spoofif a printed photo or static image is detected
Guard actions
| Action | Behaviour | Best for |
|--------|-----------|----------|
| blur | CSS backdrop blur over the element | Stepped away briefly |
| frost | Heavy blur + dark tint | Sensitive content |
| lock | Solid black overlay, manual recovery | High-security documents |
Recovery modes
| Recovery | Behaviour |
|----------|-----------|
| auto | Uncovers as soon as face returns |
| face | Requires face re-verification to unlock |
| pin | Shows PIN prompt (your UI handles unlock via sentinel.unlock(selector)) |
Multiple guards
Protect different parts of the page differently:
const sentinel = new Sentinel({
provider: new MediaPipeProvider(),
guards: [
{
selector: '#boardpack-viewer',
action: 'blur',
threshold_ms: 3000,
recovery: 'auto',
},
{
selector: '#vote-panel',
action: 'lock',
threshold_ms: 0, // instant — cannot vote while away
recovery: 'face',
},
{
selector: '#resolution-text',
action: 'frost',
threshold_ms: 5000,
recovery: 'auto',
}
]
})
await sentinel.start()The sidebar, navbar, and any unguarded elements remain fully interactive.
Heartbeat logging
Send presence events to your server for audit trails and compliance:
const sentinel = new Sentinel({
provider: new MediaPipeProvider(),
serverUrl: 'https://your-app.com/api/sentinel/heartbeat',
guards: [{ selector: '#doc', action: 'blur', threshold_ms: 3000, recovery: 'auto' }]
})
await sentinel.start()Each heartbeat is a small JSON object — no biometric data:
{
"session_id": "uuid",
"ts": 1711180800000,
"present": true,
"liveness_score": 0.94,
"identity_score": 0.0,
"absence_ms": 0,
"provider": "[email protected]"
}Swapping the provider
Sentinel's detection model is pluggable. If a better model ships, swap it without changing anything else:
import { Sentinel } from '@savannahanddelta/sentinel'
import { ONNXProvider } from '@savannahanddelta/sentinel-provider-onnx'
const sentinel = new Sentinel({
provider: new ONNXProvider({ modelUrl: '/models/my-face-model.onnx' }),
guards: [{ selector: '#doc', action: 'blur', threshold_ms: 3000, recovery: 'auto' }]
})
await sentinel.start()Or implement your own:
import type { IPresenceProvider, PresenceFrame } from '@savannahanddelta/sentinel'
class MyProvider implements IPresenceProvider {
readonly name = 'my-provider'
readonly version = '1.0.0'
readonly capabilities = { landmarks: false, gaze: false, head_pose: false, eye_openness: false }
async load() { /* load your model */ }
async detect(frame: VideoFrame): Promise<PresenceFrame> {
return { faceDetected: true, confidence: 0.95, processingTimeMs: 12 }
}
isLoaded() { return true }
unload() {}
}
const sentinel = new Sentinel({ provider: new MyProvider(), guards: [...] })React example
import { useEffect, useRef } from 'react'
import { Sentinel } from '@savannahanddelta/sentinel'
import { MediaPipeProvider } from '@savannahanddelta/sentinel-provider-mediapipe'
export function ProtectedDocument({ children }: { children: React.ReactNode }) {
const sentinelRef = useRef<Sentinel | null>(null)
useEffect(() => {
const sentinel = new Sentinel({
provider: new MediaPipeProvider(),
guards: [{
selector: '#protected-content',
action: 'blur',
threshold_ms: 5000,
recovery: 'auto',
}]
})
sentinel.start().catch(console.error)
sentinelRef.current = sentinel
return () => sentinelRef.current?.stop()
}, [])
return (
<div id="protected-content">
{children}
</div>
)
}Privacy
- All face detection runs in the browser via WASM
- No video frames, images, or facial landmarks are transmitted
- The only data that leaves the device is the heartbeat signal:
{ present, liveness_score, absence_ms }— three numbers - Camera permission is requested explicitly and can be revoked at any time via
sentinel.stop() - No biometric data is stored unless you explicitly enable identity mode
Browser support
| Browser | Support | |---------|---------| | Chrome / Edge 88+ | Full | | Firefox 90+ | Full | | Safari 15.4+ | Full | | Mobile Chrome | Full | | Mobile Safari | Full |
Requires getUserMedia (HTTPS or localhost).
Packages
| Package | Description |
|---------|-------------|
| @savannahanddelta/sentinel | Core SDK — install this |
| @savannahanddelta/sentinel-provider-mediapipe | MediaPipe face detection (default) |
| @savannahanddelta/sentinel-provider-onnx | ONNX runtime — bring your own model |
| @savannahanddelta/sentinel-types | TypeScript types (auto-installed) |
License
Business Source License 1.1 — free for non-commercial use. Commercial use requires a licence. See LICENSE.
Built by Savanna Technologies · Kisumu, Kenya
