| Token | Lifetime | Storage | Purpose |
|---|---|---|---|
| Access Token | 15 minutes | JavaScript memory (Authorization header) | API request authentication |
| Refresh Token | 30 days | HttpOnly cookie (SameSite=Lax, Secure) | Silent access token renewal |
Token rotation: Every refresh request invalidates the old refresh token and issues a new one. If a stolen refresh token is reused after the legitimate user has rotated, the stolen token is already revoked.
Logout: Revokes current refresh token. logout-all revokes every refresh token for the user.
Google ID tokens are verified server-side using google-auth-library. On first sign-in, a new User record is created. Subsequent sign-ins match by email.
- 6-digit codes, bcrypt-hashed before storage
- 10-minute expiry (configurable via
OTP_EXPIRY_SECONDS) - Max 5 verification attempts per OTP
- Generic error messages (don't reveal whether email exists)
- Rate limited: 3 sends per 5 minutes, 5 verifies per 5 minutes
Middleware: csrf.ts
Validates Origin or Referer header on all state-mutating requests (POST, PUT, PATCH, DELETE). Allowed origins are derived from FRONTEND_URL env var.
- Requests with no
OriginAND noRefererare allowed through (server-to-server / non-browser clients) - Only browser requests (which include Origin headers) are checked
- GET, HEAD, OPTIONS are exempt
In-memory store via express-rate-limit. Per-IP tracking.
| Endpoint | Limit | Window | Response |
|---|---|---|---|
POST /auth/send-magic-link |
3 requests | 5 minutes | 429 + Retry-After |
POST /auth/verify-email-otp |
5 requests | 5 minutes | 429 + Retry-After |
POST /recordings/chunk |
30 requests | 1 minute | 429 + Retry-After |
| All other endpoints | 100 requests | 1 minute | 429 + Retry-After |
Note: In-memory store resets on server restart. For multi-instance deployments, swap to Redis-backed store.
Next.js (next.config.ts) sets these headers on all frontend responses:
| Header | Value |
|---|---|
Strict-Transport-Security |
max-age=63072000; includeSubDomains; preload |
X-Frame-Options |
DENY |
X-Content-Type-Options |
nosniff |
X-DNS-Prefetch-Control |
on |
Referrer-Policy |
strict-origin-when-cross-origin |
Permissions-Policy |
microphone=(self), camera=(), geolocation=(), payment=() |
Content-Security-Policy |
default-src 'self'; connect-src 'self' <API_URL> wss://livekit...; ... |
Express uses helmet middleware for API-side security headers.
- Upload SAS tokens: Write-only permission (
w), valid for 30 minutes, scoped to specific blob path - Download SAS tokens: Read-only permission (
r), valid for 60 minutes, generated on-demand by admin - Permission type is validated as a string enum (not arbitrary values)
voice-recordings/
sessions/{sessionId}/{recordingId}/
chunk-{index}.wav # Individual 10s chunks
final.wav # Merged 24-bit WAV
Zod schemas on every route that accepts input:
| Route | Validated Fields |
|---|---|
| Auth: send-magic-link | email (valid email format) |
| Auth: verify-otp | email, otp (6 chars) |
| Auth: google | idToken (non-empty string) |
| Recordings: chunk | sessionId (UUID), recordingId (1-200 chars), chunkIndex (0-10000) |
| Recordings: finalize | sessionId (UUID), recordingId (1-200 chars), totalChunks (optional) |
| Profile: update | name (2-100 chars), preferredLanguage (max 10 chars) |
| Profile: payment | accountNumber (9-18 digits), ifscCode (regex), panNumber (regex) |
| Admin: pagination | page (min 1), limit (1-100), search (max 100 chars) |
| Admin: reject | reason (1-500 chars) |
| Admin: bulk approve | ids (1-100 UUIDs) |
| Consent | consents array (1-10 items, enum type) |
Binary format validation on upload:
- WAV: RIFF header at offset 0 + WAVE header at offset 8
- WebM: EBML header (0x1A 0x45 0xDF 0xA3) at offset 0
- OGG: "OggS" magic bytes at offset 0
- Minimum chunk size: 1KB (rejects empty/corrupt files)
- Maximum chunk size: 60MB (multer limit)
The Winston logger automatically redacts fields matching these patterns from all log output:
otp, password, token, accessToken, refreshToken, secret, accountNumber, ifscCode, upiId
All logged as [REDACTED].
API responses mask sensitive payment fields:
- Account number:
****1234(last 4 digits only) - PAN number:
******1234(last 4 only)
Raw values are never sent to the client.
Every request gets a unique X-Request-ID (UUID). This ID is:
- Set on the response header for client-side tracing
- Automatically injected into every log entry via AsyncLocalStorage
- Included in error responses for support debugging
- Sessions use UUIDs as IDs (not sequential integers)
- Invite codes are 8-character alphanumeric (nanoid)
- Users can only access sessions they are host or guest of
- Only the host can start/end recording
- Session state transitions are validated (prevents skip-ahead attacks)