openvibers/openvibe.zone Commits

AGENTS.md: the context an agent starts from (#218)

OP OpenVibers committed 1 days ago · 9df0d99ce5 · 2 files, +107 −0

Adds `AGENTS.md` (112 lines) as the context an agent starts from, plus `CLAUDE.md` (`@AGENTS.md`).

It covers: Zone as the metadata/control plane for project storage zones while Media owns objects,
keys, quota and deletion; the `ov_zone` migrations and telemetry rule; the app-vs-member capability
matrix and Media/Network/Events grants; the loopback signed events and no-secret-at-rest rules; deploy;
tests; and the metadata-only, never-reused-name, show-secret-once, idempotency and
no-local-counting pitfalls.

Checked: every cited path exists on main; no code, dependency or version changed.

Changes

ddiff --git a/AGENTS.md b/AGENTS.md
nnew file mode 100644
iindex 0000000..bfa0849
---- /dev/null
++++ b/AGENTS.md
@@@ -0,0 +1,106 @@
++# AGENTS.md — OpenVibe.Zone
++
++## Project overview
++
++OpenVibe.Zone (`https://openvibe.zone`) provides private S3-compatible storage **zones** for OpenVibe
++projects: one named object zone per project environment (sandbox or production), reached by any S3 tool
++at `https://s3.openvibe.media` (path style; `https://s3.openvibe.network` is the same surface behind
++Cloudflare). The design is ADR-031. Zone owns the zone record and control API; **Media owns everything
++inside a zone**.
++
++- **Entry:** `server/index.js`; app `server/app.js`; port `5050`, service id `zone`.
++- **API:** `server/http/api.js` (`/api/v1`), `events.js` (`/internal/events`, v2-signed,
++ loopback-only), `pages.js` (console + docs), `discovery.js`, `principal.js`, `caller-limits.js`.
++- **Domain:** `server/zone/` — `zones.js` (records, names, revisions, provisioning, deletion, the job),
++ `usage.js` (Media's hourly rollups, kept and summed), `clients.js` (Media provision/report/keys and
++ Network roles/projects), `media-limits.js`.
++- **Pages:** `server/render/` (`layout.js`, `html.js`, `markdown.js`, `short.js`); CSS in `public/css`.
++ Console changes are same-origin forms; no SPA.
++- **Secrets:** an access key's secret is shown once and never stored here (Media keeps it encrypted).
++
++## Owns, does not own
++
++Owns the zone record: its name (never reused), environment, state, description, per-zone quota; the
++control API and console; its metered usage as Media reports it (`zone_usage`); the resource index/control
++views. Tables in `ov_zone` (migrations below).
++
++Does not own: **Media owns the objects, bytes, replicas, S3/native data APIs, access keys, quota
++enforcement, usage counting and the one deletion path.** Zone never lists keys, never holds bytes and
++never counts. Identity/projects are Network's; the price comparison is Host's.
++
++## Capabilities and API
++
++Base `https://openvibe.zone/api/v1` (and `https://api.openvibe.zone/api/v1`). A caller is either a
++developer app of the project with the route's capability for audience `openvibe.zone` (a sandbox app
++reaches only the sandbox), or a project member (any role reads; admins/owners change).
++
++| Route | Capability |
++|---|---|
++| `POST /projects/:project_id/zones` (Idempotency-Key) | `zone.zone.create` |
++| `GET /projects/:project_id/zones` | `zone.zone.list` |
++| `GET /zones/:zone_id` | `zone.zone.read` |
++| `PATCH /zones/:zone_id` | `zone.zone.manage` |
++| `DELETE /zones/:zone_id?recursive=true` | `zone.zone.delete` |
++| `GET /zones/:zone_id/usage` | `zone.usage.read` |
++| `GET /resources`, `/resources/:ovrn` | `zone.resource.read` (first-party) |
++| `POST /resources/control` | `zone.resource.control` (first-party) |
++
++Grants Zone holds: `media.zone.provision` and `media.access_key.manage` (audience `openvibe.media`),
++`network.project.member.read`, and `events.subscription.manage` (the `media.usage.recorded`
++subscription). SDK `openvibe-sdk/zone` is the client surface.
++
++## Database and migrations
++
++PostgreSQL `ov_zone` through `openvibe-sdk/db` (PgBouncer for the app, owner role for migrations):
++`0001` initial, `0002` usage, `0003` telemetry, `0004` account data, `0005`–`0007` sharing/defaults.
++Applied at boot; never edit a migration that has run. Telemetry is aggregated by route template,
++method and status class (never raw URL), with 14-day retention; `/shared`, static assets, health,
++readiness and `/metrics` are not counted.
++
++## Auth and security
++
++Network RS256 JWTs (JWKS) for apps/members; `/internal/events` answers only on loopback (nginx 404s
++it), requires a valid v2 signature (`ZONE_EVENTS_SECRET`) and refuses a request through a proxy. Every
++console change is a same-origin form; only a project's admins and owners change zones and keys. Media's
++S3 answers are `private, no-store` with CDN no-store, so no edge keeps them. Never log or store an
++access-key secret; no shared loopback key exists (tested by grep and at runtime). Account export/
++deletion covers every table with a person column or exempts it with a reason.
++
++## Deploy
++
++`sudo ovhost deploy zone`: unit `openvibe-zone.service` on `127.0.0.1:5050`, database `ov_zone`, env
++`/etc/openvibe/zone.env` (with `EVENTS_URL` and `ZONE_EVENTS_SECRET` for the usage subscription), nginx
++`deploy/nginx/openvibe.zone.conf` on openvibe.zone's own certificate; migrations run at boot. Rollback
++with `sudo ovhost rollback zone --to <sha>`.
++
++## Commands and tests
++
++```bash
++npm ci
++npm run dev # http://127.0.0.1:5050
++npm test # every test/*.test.js, temp PGlite + mock Network
++npm test -- login # only matching files
++npm run test:pg # the same on PostgreSQL
++```
++
++`test/run.js` uses `openvibe-shared/test-runner`; temp PGlite and an in-process Network mock, never the
++network. Coverage includes zone names/revisions/provisioning/deletion, usage rollups, app vs member
++authorization, the S3 surface/limits, nginx auth limits, the no-internal-key and secrets checks,
++session/redirect security, account data and the pages.
++
++## Conventions and pitfalls
++
++- CommonJS, 4-space indent, single quotes, semicolons; problem+json errors; escaped HTML.
++- **Zone is metadata only.** Do not add an object list, byte store, replica, key listing or a second
++ delete path — that is Media's, and a duplicate would drift. Operations call Media with Zone's own
++ grants.
++- **A zone name is never reused**, even after deletion; a rename does not change the S3 bucket identity.
++- **The secret is shown once.** A key response may carry it that one time; never persist, log or
++ re-render it. Media holds the encrypted key.
++- **Idempotency:** create carries `Idempotency-Key`; a retry must return the same zone, not a second one.
++- **Sandbox apps reach only the sandbox**, and only their project; a member check is Network's answer,
++ never a client claim.
++- **The usage is Media's:** keep the `media.usage.recorded` rollups and sum them; never estimate or
++ count a byte locally.
++- **No SQL migration may block a rollback**, and one with a person column must be covered by account
++ data or exempted in `server/account-data.js` with a reason.
ddiff --git a/CLAUDE.md b/CLAUDE.md
nnew file mode 100644
iindex 0000000..43c994c
---- /dev/null
++++ b/CLAUDE.md
@@@ -0,0 +1 @@