openvibers/openvibe.locker AGENTS.md

AGENTS.md

main 10.4 KB · 135 lines · markdown

1# AGENTS.md — OpenVibe.Locker
2
3## Project Overview
4
5OpenVibe.Locker is the front door for private files, passwords and expiring Sends. It stores **ciphertext and opaque
6access metadata only**: the server never seals, opens, indexes, previews, transcodes or runs AI on content. Names,
7MIME types and bytes are inside the envelope, sealed and opened in the browser by the device UI and
8`openvibe-sdk/locker`; the browser worker writes to OPFS in 1 MiB chunks.
9
10- **Domain:** `openvibe.locker` · **Port:** 5145 (drill 15145) · **Service id:** `locker` · **Env prefix:** `LOCKER`/`OV_`
11- **Runtime:** Node 22, Express 4, PostgreSQL 18 through `openvibe-sdk/db` (PGlite in development/tests), Playwright
12 for browser tests.
13- **Read first:** [README.md](README.md), [docs/BLIND-FILL.md](docs/BLIND-FILL.md),
14 [deploy/LAUNCH.md](deploy/LAUNCH.md), [SECURITY.md](SECURITY.md), [public/openapi.json](public/openapi.json).
15
16**Network context:** identity, sign-in, staff roles, account export/deletion and the Companion token validation are
17OpenVibe.Network (`locker` OAuth client; the Companion token needs `locker.login.manage`; `/internal/identity/resolve`
18with `trust=1` decides standing). Ciphertext lives in a private OpenVibe.Media tenant (`locked.ovlk`,
19`application/octet-stream`); Locker queues durable erasure there. Events are OpenVibe.Events. Share UI and the SDK
20envelope are OpenVibe.Shared/SDK, not reimplemented here.
21
22## Commands
23
24```bash
25npm ci # install (contracts/sdk/shared pinned by tag)
26npm start # http://localhost:5145, PGlite under data/pglite
27npm test # node --test --test-concurrency=1 test/*.test.js (real WebCrypto, local adapters)
28npm run check # syntax check (scripts/check.js)
29npm run test:browser # starts a local instance, real browser worker, 390/1280 screenshots in artifacts/
30node --test test/companion.test.js # one file (companion isolation and ciphertext-only checks)
31```
32
33There is **no `test:pg` script**: the suite uses isolated PGlite databases. Browser checks use Playwright's Chromium
34or local Chrome. There is no build step; the page JS in [public/js/](public/js/) (`app.js`, `passwords.js`,
35`payload.js`, `seal-worker.js`) is served as-is.
36
37## Architecture at a Glance
38
39- **Entry:** [server/index.js](server/index.js) — creates the app, starts the due jobs
40 ([server/jobs.js](server/jobs.js)) and the two ADR-033 subscriptions, listens, and on SIGTERM stops jobs/subscriptions
41 and closes the database. Production refuses to start without the service credentials and event secret.
42- **App factory:** [server/app.js](server/app.js) — helmet/CSP (no-referrer, worker-src self), `/internal/events`,
43 `/api/v1/companion`, `/auth`, optional auth, the same-origin + `X-Requested-With` CSRF gate on writes, `/api/v1`,
44 `/shared` + static, pages, error handler. `createApp({ db, network, media, sso, now })` is injectable for tests.
45- **Config/db:** [server/config.js](server/config.js) (pure load), [server/db.js](server/db.js) — migrations
46 [migrations/](migrations/) at boot; no table is created at runtime.
47- **Core:** [server/service.js](server/service.js) (accounts, quota, items, viewing rules, rates, removal),
48 [server/envelope.js](server/envelope.js) (shape, prefix, exact length), [server/media.js](server/media.js) (private
49 Media objects and multipart), [server/network.js](server/network.js) (identity/standing),
50 [server/account-data.js](server/account-data.js), [server/sharing.js](server/sharing.js) (the Share sheet),
51 [server/uploads.js](server/uploads.js) (resumable sessions), [server/companion.js](server/companion.js).
52- **Pages:** [server/pages.js](server/pages.js) — `/`, `/drive`, `/passwords`, `/files/:id`, `/sends`,
53 `/s/:id#k=…`, `/recovery`, `/staff`, `/docs`; [public/llms.txt](public/llms.txt),
54 [public/llms-full.txt](public/llms-full.txt), [public/robots.txt](public/robots.txt),
55 [public/.well-known/agent.json](public/.well-known/agent.json).
56- **Database:** [migrations/](migrations/): `locker_accounts`, `locker_items`, `locker_uploads`,
57 `locker_upload_parts`, `locker_upload_results`, `locker_reports`, `locker_moderation`, `locker_rates`, `locker_gc`.
58- **Deploy:** [deploy/systemd/openvibe-locker.service](deploy/systemd/openvibe-locker.service) (PORT 5145,
59 `/etc/openvibe/locker.env`, `/opt/openvibe.locker`) and [deploy/nginx/openvibe.locker.conf](deploy/nginx/openvibe.locker.conf)
60 (33 MiB parts, 91 MiB legacy single envelopes, 64 KiB for everything else, buffering off, long read timeouts, no
61 query strings/cookies/bodies in logs). Install through `ov-vhost-install`; database `ov_locker`; Media multipart
62 must allow at least 2304 MiB of padded ciphertext.
63
64## Ownership and Boundaries
65
66Locker owns its accounts/quota, items and access rules, upload sessions/results, reports, moderation and rate rows, and
67the access/moderation history it retains as security evidence. It does **not** own identity, sign-in, Wrapped keys
68(Network), the ciphertext bytes (Media), event delivery (Events), or the envelope/crypto format (`openvibe-sdk/locker`;
69payload framing in [public/js/payload.js](public/js/payload.js) is an application payload inside that envelope). It
70never sees a filename, MIME type or plaintext byte. A folder is organisational: sharing a folder does not grant its
71children.
72
73## Access, Crypto and Moderation
74
75- **Payload:** 4-byte big-endian metadata length (≤65536) + UTF-8 JSON `{ version:1, name, type, size, folder? }` +
76 exactly `size` content bytes. `prefix`/`pack`/`unpack` are in [public/js/payload.js](public/js/payload.js); the worker
77 feeds chunks to `locker.sealStream` and verifies before commit.
78- **Access:** a Send is a link lock (`/s/:id#k=…`, the key never leaves the fragment; 1 hour–30 days and an optional
79 attempt limit); a drive item is a people lock with the owner's slot and the current sharing list. Each content
80 request consumes one claim, including disconnected downloads; GET metadata and HEAD do not.
81- **Sharing:** seal a new envelope for the owner and all remaining recipients, complete a resumable `reseal` (or an
82 inline `POST /files/:id/reseals` for ≤90 MiB), then `PUT /files/:id/share` with `{ members, envelope_ref }` (and the
83 Share GET's ETag in `If-Match` where possible). The Media reference, quota and ACL commit together; an unconsumed
84 receipt expires after an hour and old objects are queued for erasure. A departing member's old key cannot open the
85 new envelope (they may still keep a prior copy).
86- **Limits:** `LOCKER_QUOTA_BYTES` 5 GiB, `LOCKER_MAX_FILE_BYTES` 2 GiB plaintext (browser-enforced; the server can
87 only enforce the padded size), 60 uploads/hour, 20 Sends/hour, 32 MiB parts, `LOCKER_MIN_ACCOUNT_DAYS=7`; sign-in,
88 age and Network ban/deletion, local restrictions and optional `LOCKER_ALLOWLIST_ONLY` all apply.
89- **Companion:** only a Bearer token Network validates live per request; `GET /keys`, `GET /logins`,
90 `GET /logins/:id/content`, `POST /logins` (≤2 MiB, kind fixed `login`). The scoped token cannot reach the general
91 file API, key updates or blind fill. Any change to this surface must be reviewed as security-sensitive.
92- **Reports** carry a reason code only (`abuse`, `illegal`, `malware`, `other`) — never a key or plaintext. Staff
93 (Network `global_mod`+) disable items and restrict uploads; every change is recorded locally.
94- **Account export/deletion** (ADR-033) provides an opaque manifest with separately downloadable envelopes while the
95 account exists; deletion denies access immediately, queues ciphertext erasure, cancels deliveries and removes
96 memberships. Access/moderation history and the deletion marker remain as evidence, with no plaintext or keys.
97- Errors use stable `locker.*` codes and never echo an envelope's contents. Hostile input stays text.
98
99## Key Pitfalls
100
101- **The server must never decrypt, sniff, index or log content.** Do not add an MIME check by content, a preview, an
102 AI call, a search index or a filename column; "unknown plaintext metadata is rejected" is enforced on the envelope
103 header, not by opening it.
104- **Exact length:** the server checks the envelope shape and the exact expected ciphertext length while counting actual
105 received bytes; a mismatch is a 400 `locker.length`. The 64 KiB header bound is part of the protocol.
106- **CSRF:** writes require the same origin **and** `X-Requested-With`; a bearer-only or cross-site form post must not
107 pass. Do not relax it for the "convenience" of a client.
108- **Sends:** never rewrite the fragment into a query or a server log; `#k=` stays client-side. HEAD/GET metadata must
109 not consume a download claim; content requests must.
110- **Resumable uploads:** parts are immutable and retryable, numbered from zero, each exactly 32 MiB except the last;
111 completion is idempotent for 24 h and rechecks standing/revision/recipients. A lost completion response is retried
112 by querying, not by re-uploading blindly.
113- **Expiry/erasure is due-based and bounded:** access ends at `due_at` even during a Media outage; cleanup retries
114 failures. Never restore or reuse a deleted envelope.
115- **CSP/`Referrer-Policy: no-referrer`** is security-relevant: no third-party script, no referrer carrying a fragment,
116 no `frameAncestors` widening. The only named external origins are Network, Media, Chat, Billing, Events, Cloudflare
117 insights and HIBP (for the first five SHA-1 hex chars, after asking).
118- **No fake backend or sample content** in production; development uses PGlite and real WebCrypto.
119- **Drill:** port 15145, its own writable directory and drill database — never point a drill at the live database.
120
121## Testing
122
123`npm test` runs `node --test` over [test/](test/): locker access/quota/leniency, resumable uploads, passwords/login
124envelopes, sharing, Media adapter, companion isolation/ciphertext-only, public pages and real browser checks.
125`npm run test:browser` starts a local instance and exercises the real worker, saving 390/1280 screenshots in both themes
126to `artifacts/`. `npm run check` checks syntax. There is no PostgreSQL suite; state that plainly rather than inventing
127one.
128
129## Documentation
130
131- [README.md](README.md) — run/verify, pages, private payload, storage/access, companion, versions.
132- [deploy/LAUNCH.md](deploy/LAUNCH.md) — the operator handoff: PRs, database, OAuth client, env, drill, units, limits.
133- [docs/BLIND-FILL.md](docs/BLIND-FILL.md) — the Run/Companion/Run blind-fill flow and its security requirements.
134- [public/openapi.json](public/openapi.json), [public/llms.txt](public/llms.txt), [SECURITY.md](SECURITY.md).
135