Repository Public
1# Security and privacy23## Authentication and sessions45- **Passkeys only** (WebAuthn / FIDO2, `@simplewebauthn/server`): discoverable credentials, user verification6 required, relying party = host of `APP_URL`. Touch ID / Face ID / security keys. Biometric data never leaves the7 device; the server stores a public key and a signature counter.8- **Enrolment** happens only (a) with a one-time token created from the node's shell (first setup: valid 7 days,9 written to a 600 file; later: `tsx scripts/setup-token.ts`, 30 minutes) or (b) from an authenticated session.10 The token travels in the URL fragment, which browsers do not send to servers.11- **Recovery**: eight one-use codes shown once at first enrolment (stored as SHA-256); or12 `tsx scripts/setup-token.ts --recovery` on the node, which replaces every passkey and revokes every session.13- **Sessions**: 32-byte random token in an `HttpOnly; Secure; SameSite=Lax` cookie, stored hashed, 30 days,14 listed and revocable in Settings → Security.15- **CSRF**: every state-changing admin call must (1) come from our origin (`Origin`, `Sec-Fetch-Site`) and16 (2) carry the session's CSRF token in `X-CSRF-Token`. Public POSTs also require same-origin.17- **Enumeration**: login uses discoverable credentials (no account list in the challenge); every failure returns18 the same `login_failed`.1920## Authorisation2122- One guard per API surface, executed before the handler is resolved (`makeRouter`). Every admin query is scoped23 by `user_id`; identifiers are UUIDs validated before use.24- Visitors act only through their token: the API has no public endpoint that takes a request or booking id25 (no IDOR surface). Tokens are 256-bit, stored hashed, expire 30 days after the appointment, can be revoked.26- The public API exposes: profile, active types (without `location_detail`), free slots. Never the agenda,27 other visitors, reasons, notes.2829## Input, abuse3031- Zod validation on every body and query; parameterised SQL only.32- Public form: honeypot field, signed form token (minimum fill time in production, 3 h maximum age), consent33 required, 8 submissions / hour and 30 / day per IP, at most 3 open requests per e-mail address, optional34 allow / block lists of e-mail domains.35- Rate limits on slots, management links, WebAuthn and recovery codes (5 / 15 min).36- ICS subscriptions: https only, public addresses only (SSRF guard), 8 MB cap, 25 s timeout.37- Avatar upload: PNG / JPEG / WebP by magic bytes, 1 MB, served with `nosniff`.3839## Headers4041CSP (`default-src 'self'`, no third-party origin, `frame-ancestors 'none'`, `form-action 'self'`,42`object-src 'none'`), `X-Frame-Options: DENY`, `X-Content-Type-Options`, `Referrer-Policy`,43`Permissions-Policy`, HSTS. Residual risk: `script-src` allows `'unsafe-inline'` (decision D-13); the application44never renders user-supplied HTML (React escapes everything, e-mail templates escape explicitly).4546## Secrets4748Only in `.env` on the node (mode 600) and in the `mld` manifest on the gateway; never in Git49(`pnpm secrets:scan`), never sent to the browser. `APP_SECRET` signs form tokens, derives visitor tokens and the50AES-256-GCM key of external calendar addresses.5152## Personal information (Québec, Law 25) — assessment5354| Topic | Practice |55|---|---|56| Minimisation | Name, e-mail, optional organization, reason, answers. IP kept on the request for abuse handling. |57| Consent | Mandatory checkbox, timestamp stored (`consent_at`), policy at `/privacy`. |58| Confidentiality | Reason and answers: admin area only. Not in URLs, logs, e-mails, ICS, audit metadata. |59| Retention | `settings.retention_days` (default 730): requests, bookings, orphan contacts, audit and notification logs are deleted by the worker. Backups: 30 days. |60| Access / portability | Admin → Contacts → export JSON. |61| Erasure | Admin → Contacts → delete (contact, requests, answers, links, notifications, past bookings); refused while a confirmed appointment is upcoming. |62| Responsible person | The owner; a public contact address appears on `/privacy` when "Reply-to" is set. |63| Processors | The e-mail provider (address + message content). Database and backups stay on the owner's machines. |64| Not done | No formal privacy impact assessment, no incident register: to be completed by the owner if the service is used beyond personal scheduling. |6566## Logging6768Application logs carry error names and messages, never request bodies. The audit trail records who did what to69which entity, with metadata limited to identifiers and counts.7071## Backups7273`scripts/backup.sh` daily: `pg_dump` → AES-256 (openssl, PBKDF2, passphrase in a 600 file outside the repo) →74verified by decrypt + `pg_restore --list`; 30 days kept. Restore procedure and its test: OPERATIONS.md.7576## Known limits7778- One owner account (the schema carries `user_id` everywhere; multi-user is not exposed).79- No CAPTCHA: honeypot + rate limits + approval by default were judged proportionate.80- Passkey flows are covered end-to-end with Chromium's virtual authenticator; Touch ID / Face ID on real devices81 must be checked by the owner on their own hardware.82