Worker entry
The worker (workers/app.ts) routes requests in priority order:
/api/*— API routes (auth, passkeys, user trails, audit, sessions, health)/session/*— Durable Object session management and WebSocket upgrades- Everything else — React Router SSR handler
CORS is handled with a blanket Access-Control-Allow-Origin: * on all API responses.
Authentication API (src/auth.ts)
All auth endpoints return JSON with Cache-Control: no-store.
POST /api/auth/register — Create a new account.
- Body:
{ email, name, password, termsAccepted } - Validates password >= 6 chars, terms accepted, email uniqueness
- Hashes password with PBKDF2 (100k iterations, SHA-256, random 16-byte salt)
- Generates 6-digit email verification code (15 min expiry)
- Sends verification email via Resend API (or logs to console if no API key)
- Returns:
{ ok, userId, requiresVerification }
POST /api/auth/verify — Verify email with 6-digit code.
- Body:
{ email, code } - On success: marks email verified, creates 30-day session token
- Returns:
{ ok, token, user }
POST /api/auth/resend-code — Resend verification code.
- Body:
{ email } - Generates new code with fresh 15-min expiry
POST /api/auth/login — Password login.
- Body:
{ email, password } - Verifies PBKDF2 hash, checks email_verified flag
- Creates 30-day session token
- Returns:
{ ok, token, user }or{ error, requiresVerification }if unverified
POST /api/auth/logout — Invalidate session.
- Reads token from
Authorization: Bearerheader - Deletes session row from D1
GET /api/auth/me — Get current user.
- Reads token from
Authorizationheader orblincr_tokencookie - Returns:
{ user: { id, email, name } }or 401
Passkey (WebAuthn) API (src/passkey.ts)
Implements FIDO2/WebAuthn passkeys using @simplewebauthn/server v11. The relying party ID is derived dynamically from the request hostname (localhost for dev, blincr.com for production).
Challenges are stored in the webauthn_challenges D1 table with a 5-minute TTL and consumed on verification (single-use).
POST /api/auth/passkey/register-options — Generate registration options. Requires auth.
- Returns WebAuthn
PublicKeyCredentialCreationOptionsJSONand achallengeId - Excludes already-registered credentials for the user
- Uses
attestationType: "none",residentKey: "preferred",userVerification: "preferred"
POST /api/auth/passkey/register-verify — Verify registration response. Requires auth.
- Body:
{ challengeId, response (RegistrationResponseJSON), deviceName? } - Verifies the attestation, stores credential public key (base64url-encoded) and counter in
user_credentials - Returns:
{ ok, credentialId }
POST /api/auth/passkey/login-options — Generate authentication options. No auth required.
- Returns
PublicKeyCredentialRequestOptionsJSONwithchallengeId - Empty
allowCredentials(discoverable credential / resident key flow)
POST /api/auth/passkey/login-verify — Verify authentication response. No auth required.
- Body:
{ challengeId, response (AuthenticationResponseJSON) } - Looks up credential by
response.id, verifies signature against stored public key - Updates credential counter to prevent replay attacks
- Creates a 30-day session token
- Returns:
{ ok, token, user }
GET /api/auth/passkey/list — List user's registered passkeys. Requires auth.
- Returns:
{ credentials: [{ id, device_name, created_at }] }
DELETE /api/auth/passkey/:id — Remove a passkey. Requires auth.
- Deletes by credential row ID, scoped to the authenticated user
User trails API (src/auth.ts)
All user trail endpoints require authentication (token from header or cookie).
GET /api/user/trails?folder=/ — List user's cloud trails (up to 100, ordered by updated_at DESC).
POST /api/user/trails — Save a trail to the cloud.
- Body:
{ trailId, title, metadata, folderPath? }
PUT /api/user/trails/:id — Update trail metadata (title, folderPath, isPublic).
DELETE /api/user/trails/:id — Delete a cloud trail (removes both D1 record and R2 object).
POST /api/user/trails/:id/share — Generate a share token and public URL.
Public trails API
GET /api/trails — List all R2 trail snapshots (session-based, not user-scoped).
GET /api/trails/:id — Fetch a trail JSON snapshot from R2.
PATCH /api/trails/:id — Add a note/label to a specific event in a trail.
- Body:
{ eventIndex, note }
DELETE /api/trails/:id — Delete a trail snapshot from R2.
Audit API
GET /api/audit?offset=0&limit=50 — Paginated audit log entries (max 100 per page).
GET /api/audit/sessions — Aggregated session list (started_at, last_seen, event_count).
DELETE /api/audit/:id — Delete a single audit log entry.
DELETE /api/audit/session/:sessionId — Delete all audit entries for a session.
Session API
GET /api/sessions — List recent sessions with last activity and event counts.
POST /session/ — Create a new session (allocates a Durable Object).
GET /session/:id/websocket — WebSocket upgrade to the SessionCoordinator DO.
GET /session/:id/state — Get current session state from the DO.
SessionCoordinator Durable Object
Cloudflare Durable Object (src/session-coordinator.ts) managing real-time session state.
State: session ID, owner, participant map, trail array, current URL, creation timestamp.
WebSocket messages (server -> client):
sync— Full trail + participants + current URL (sent on connect)event— Single browse event broadcastparticipant_joined— New participant infoparticipant_left— Participant ID that disconnected
WebSocket messages (client -> server):
- Browse event JSON — Appended to trail, broadcast to all other connections (includes overlay events for comments)
{ type: "ping" }— Returns pong with session info{ type: "save" }— Saves trail snapshot to R2
Extension internal messages
These are chrome.runtime.sendMessage calls within the extension, not HTTP endpoints:
GET_OVERLAYS — Content script asks the background for materialized overlay state for a specific URL. Background scans the in-memory trail for all overlay events matching the URL, applies create/update/delete in order, and returns the resulting array of live OverlayInstance objects. Used on page load and SPA navigation to restore persistent comments.
ACTIVATE_COMMENT_MODE — Popup sends this to the content script on the active tab to trigger comment creation (centered in viewport). Equivalent to the user pressing Alt+Click.
Audit logging: Every event is also inserted into the audit_log D1 table.