openvibers/openvibe.zone AGENTS.md
AGENTS.md
| 1 | # AGENTS.md — OpenVibe.Zone |
| 2 | |
| 3 | ## Project overview |
| 4 | |
| 5 | OpenVibe.Zone (`https://openvibe.zone`) provides private S3-compatible storage **zones** for OpenVibe |
| 6 | projects: one named object zone per project environment (sandbox or production), reached by any S3 tool |
| 7 | at `https://s3.openvibe.media` (path style; `https://s3.openvibe.network` is the same surface behind |
| 8 | Cloudflare). The design is ADR-031. Zone owns the zone record and control API; **Media owns everything |
| 9 | inside a zone**. |
| 10 | |
| 11 | - **Entry:** `server/index.js`; app `server/app.js`; port `5050`, service id `zone`. |
| 12 | - **API:** `server/http/api.js` (`/api/v1`), `events.js` (`/internal/events`, v2-signed, |
| 13 | loopback-only), `pages.js` (console + docs), `discovery.js`, `principal.js`, `caller-limits.js`. |
| 14 | - **Domain:** `server/zone/` — `zones.js` (records, names, revisions, provisioning, deletion, the job), |
| 15 | `usage.js` (Media's hourly rollups, kept and summed), `clients.js` (Media provision/report/keys and |
| 16 | Network roles/projects), `media-limits.js`. |
| 17 | - **Pages:** `server/render/` (`layout.js`, `html.js`, `markdown.js`, `short.js`); CSS in `public/css`. |
| 18 | Console changes are same-origin forms; no SPA. |
| 19 | - **Secrets:** an access key's secret is shown once and never stored here (Media keeps it encrypted). |
| 20 | |
| 21 | ## Owns, does not own |
| 22 | |
| 23 | Owns the zone record: its name (never reused), environment, state, description, per-zone quota; the |
| 24 | control API and console; its metered usage as Media reports it (`zone_usage`); the resource index/control |
| 25 | views. Tables in `ov_zone` (migrations below). |
| 26 | |
| 27 | Does not own: **Media owns the objects, bytes, replicas, S3/native data APIs, access keys, quota |
| 28 | enforcement, usage counting and the one deletion path.** Zone never lists keys, never holds bytes and |
| 29 | never counts. Identity/projects are Network's; the price comparison is Host's. |
| 30 | |
| 31 | ## Capabilities and API |
| 32 | |
| 33 | Base `https://openvibe.zone/api/v1` (and `https://api.openvibe.zone/api/v1`). A caller is either a |
| 34 | developer app of the project with the route's capability for audience `openvibe.zone` (a sandbox app |
| 35 | reaches only the sandbox), or a project member (any role reads; admins/owners change). |
| 36 | |
| 37 | | Route | Capability | |
| 38 | |---|---| |
| 39 | | `POST /projects/:project_id/zones` (Idempotency-Key) | `zone.zone.create` | |
| 40 | | `GET /projects/:project_id/zones` | `zone.zone.list` | |
| 41 | | `GET /zones/:zone_id` | `zone.zone.read` | |
| 42 | | `PATCH /zones/:zone_id` | `zone.zone.manage` | |
| 43 | | `DELETE /zones/:zone_id?recursive=true` | `zone.zone.delete` | |
| 44 | | `GET /zones/:zone_id/usage` | `zone.usage.read` | |
| 45 | | `GET /resources`, `/resources/:ovrn` | `zone.resource.read` (first-party) | |
| 46 | | `POST /resources/control` | `zone.resource.control` (first-party) | |
| 47 | |
| 48 | Grants Zone holds: `media.zone.provision` and `media.access_key.manage` (audience `openvibe.media`), |
| 49 | `network.project.member.read`, and `events.subscription.manage` (the `media.usage.recorded` |
| 50 | subscription). SDK `openvibe-sdk/zone` is the client surface. |
| 51 | |
| 52 | ## Database and migrations |
| 53 | |
| 54 | PostgreSQL `ov_zone` through `openvibe-sdk/db` (PgBouncer for the app, owner role for migrations): |
| 55 | `0001` initial, `0002` usage, `0003` telemetry, `0004` account data, `0005`–`0007` sharing/defaults. |
| 56 | Applied at boot; never edit a migration that has run. Telemetry is aggregated by route template, |
| 57 | method and status class (never raw URL), with 14-day retention; `/shared`, static assets, health, |
| 58 | readiness and `/metrics` are not counted. |
| 59 | |
| 60 | ## Auth and security |
| 61 | |
| 62 | Network RS256 JWTs (JWKS) for apps/members; `/internal/events` answers only on loopback (nginx 404s |
| 63 | it), requires a valid v2 signature (`ZONE_EVENTS_SECRET`) and refuses a request through a proxy. Every |
| 64 | console change is a same-origin form; only a project's admins and owners change zones and keys. Media's |
| 65 | S3 answers are `private, no-store` with CDN no-store, so no edge keeps them. Never log or store an |
| 66 | access-key secret; no shared loopback key exists (tested by grep and at runtime). Account export/ |
| 67 | deletion covers every table with a person column or exempts it with a reason. |
| 68 | |
| 69 | ## Deploy |
| 70 | |
| 71 | `sudo ovhost deploy zone`: unit `openvibe-zone.service` on `127.0.0.1:5050`, database `ov_zone`, env |
| 72 | `/etc/openvibe/zone.env` (with `EVENTS_URL` and `ZONE_EVENTS_SECRET` for the usage subscription), nginx |
| 73 | `deploy/nginx/openvibe.zone.conf` on openvibe.zone's own certificate; migrations run at boot. Rollback |
| 74 | with `sudo ovhost rollback zone --to <sha>`. |
| 75 | |
| 76 | ## Commands and tests |
| 77 | |
| 78 | ```bash |
| 79 | npm ci |
| 80 | npm run dev # http://127.0.0.1:5050 |
| 81 | npm test # every test/*.test.js, temp PGlite + mock Network |
| 82 | npm test -- login # only matching files |
| 83 | npm run test:pg # the same on PostgreSQL |
| 84 | ``` |
| 85 | |
| 86 | `test/run.js` uses `openvibe-shared/test-runner`; temp PGlite and an in-process Network mock, never the |
| 87 | network. Coverage includes zone names/revisions/provisioning/deletion, usage rollups, app vs member |
| 88 | authorization, the S3 surface/limits, nginx auth limits, the no-internal-key and secrets checks, |
| 89 | session/redirect security, account data and the pages. |
| 90 | |
| 91 | ## Conventions and pitfalls |
| 92 | |
| 93 | - CommonJS, 4-space indent, single quotes, semicolons; problem+json errors; escaped HTML. |
| 94 | - **Zone is metadata only.** Do not add an object list, byte store, replica, key listing or a second |
| 95 | delete path — that is Media's, and a duplicate would drift. Operations call Media with Zone's own |
| 96 | grants. |
| 97 | - **A zone name is never reused**, even after deletion; a rename does not change the S3 bucket identity. |
| 98 | - **The secret is shown once.** A key response may carry it that one time; never persist, log or |
| 99 | re-render it. Media holds the encrypted key. |
| 100 | - **Idempotency:** create carries `Idempotency-Key`; a retry must return the same zone, not a second one. |
| 101 | - **Sandbox apps reach only the sandbox**, and only their project; a member check is Network's answer, |
| 102 | never a client claim. |
| 103 | - **The usage is Media's:** keep the `media.usage.recorded` rollups and sum them; never estimate or |
| 104 | count a byte locally. |
| 105 | - **No SQL migration may block a rollback**, and one with a person column must be covered by account |
| 106 | data or exempted in `server/account-data.js` with a reason. |
| 107 |