openvibers/openvibe.inventory AGENTS.md

AGENTS.md

main 11.9 KB · 162 lines · markdown

1# AGENTS.md — OpenVibe.Inventory
2
3## Project Overview
4
5OpenVibe.Inventory is the network-wide inventory for one OpenVibe account (ADR-054): the item kinds a surface can
6render, the definitions an issuer publishes, the instances a person owns, the equipped set, the ledger, the community
7Workshop and the reviewed cosmetic store. One inventory for every OpenVibe site, the way Steam's works across games.
8
9- **Site:** `openvibe.my/inventory` (base-path aware; API host `inventory.openvibe.network`) · **Port:** 5030 ·
10 **Service id:** `inventory` · **Env prefix:** `INVENTORY`
11- **Runtime:** Node 22, Express 4, PostgreSQL 18 through `openvibe-sdk/db` (PGlite in development/tests), Valkey 9
12 for shared limits.
13- **Read first:** [README.md](README.md), [docs/store.md](docs/store.md), [STATUS.json](STATUS.json), ADR-054 in
14 OpenVibe.Contracts.
15
16**Network context:** subjects (`usr_…`), usernames, tokens, JWKS and roles are OpenVibe.Network. Image bytes are
17OpenVibe.Media (a Workshop image is uploaded to Inventory's Media tenant and referenced by `med_` id). Events are
18published through the SDK outbox to OpenVibe.Events; account export/deletion arrives there at `/internal/events`.
19Money is OpenVibe.Billing: the store snapshots a reviewed price, Billing authorizes the spend, and Inventory matches
20Billing's payment before granting. Inventory never moves money itself.
21
22## Commands
23
24```bash
25npm ci # install (contracts/sdk/shared pinned by tag)
26npm run dev # http://localhost:5030 (base path from PUBLIC_URL/BASE_URL)
27npm start # node server/index.js
28npm test # every test/*.test.js on temp PGlite, mocked Network/Media/Billing
29npm run test:pg # the same through PostgreSQL + PgBouncer
30npm test -- holdings # only test files whose name contains the word
31npm test -- --strict # a skipped test fails the run
32node --check <file.js> # syntax check (no linter configured)
33```
34
35`test/run.js` uses `openvibe-shared/test-runner`; tests use fixtures and never reach the internet. No build step:
36pages are server-rendered and assets live under [public/](public/).
37
38## Architecture at a Glance
39
40- **Entry:** [server/index.js](server/index.js) — listener, telemetry retention, the outbox relay and the two account
41 subscriptions; graceful stop in `createLifecycle()`. `INVENTORY_DRILL` keeps boot-time work off.
42- **App factory:** [server/app.js](server/app.js) — CSP, SSO, `/auth`, `/internal/events`, the `/shared` + static
43 mounts, `/api/v1`, discovery/agent twins, pages, 404/error handler.
44- **Config and URLs:** [server/config.js](server/config.js) (`load(env)` is pure), [server/urls.js](server/urls.js)
45 (the one base-path translation for links, redirects, metadata and discovery — API paths keep their shape).
46- **Model/store:** [server/inventory/store.js](server/inventory/store.js) (kinds, definitions, instances, grants,
47 equipped, ledger, subject merges), [server/inventory/holdings.js](server/inventory/holdings.js) (stacked public/own
48 holdings and cursors), [server/inventory/commerce.js](server/inventory/commerce.js) (store, review, orders,
49 Billing), [server/inventory/seed.js](server/inventory/seed.js), [server/inventory/account-data.js](server/inventory/account-data.js).
50 Seed data: [server/data/kinds.json](server/data/kinds.json), [server/data/live-definitions.json](server/data/live-definitions.json),
51 [server/data/live-grantors.json](server/data/live-grantors.json).
52- **Workshop:** [server/workshop/pages.js](server/workshop/pages.js), [server/workshop/people.js](server/workshop/people.js),
53 [server/workshop/media.js](server/workshop/media.js), [server/workshop/image.js](server/workshop/image.js); upload JS
54 [public/js/workshop-upload.js](public/js/workshop-upload.js).
55- **HTTP:** [server/http/api.js](server/http/api.js) (`/api/v1`), [server/http/pages.js](server/http/pages.js),
56 [server/http/discovery.js](server/http/discovery.js), [server/http/principal.js](server/http/principal.js)
57 (person vs service, `X-OV-Subject`, same-origin cookie writes, capabilities),
58 [server/http/wallet-cors.js](server/http/wallet-cors.js), [server/http/router.js](server/http/router.js) (async router).
59- **Auth:** [server/auth/sso.js](server/auth/sso.js), [server/auth/keys.js](server/auth/keys.js),
60 [server/auth/scoped-session.js](server/auth/scoped-session.js).
61- **Frontend:** server-rendered through [server/render/layout.js](server/render/layout.js),
62 [server/render/html.js](server/render/html.js), [server/render/markdown.js](server/render/markdown.js) and
63 `openvibe-shared/shell|frame|showcase`; assets [public/css/app.css](public/css/app.css),
64 [public/holdings.js](public/holdings.js). Every page works without JavaScript.
65- **Database:** [server/db.js](server/db.js) and [migrations/](migrations/): `inventory_kinds`,
66 `inventory_definitions`, `inventory_definition_aliases`, `inventory_instances`, `inventory_grants`,
67 `inventory_equipped`, `inventory_ledger`, `inventory_store_listings`, `inventory_store_purchases`,
68 `inventory_subject_merges`, `event_outbox`, `account_data_events`, `telemetry_samples`.
69- **Deploy:** [deploy/systemd/openvibe-inventory.service](deploy/systemd/openvibe-inventory.service)
70 (`/etc/openvibe/inventory.env`, `/opt/inventory.openvibe.network`) and
71 [deploy/nginx/inventory.openvibe.network.conf](deploy/nginx/inventory.openvibe.network.conf). `sudo ovhost deploy
72 inventory`; database `ov_inventory`; rollback restores the previous sha when `/api/ready` fails.
73
74## Ownership and Boundaries
75
76Inventory is the authority for kinds, definitions, instances, grants, the equipped set and the ledger, plus its
77Workshop definitions, store listings/orders and the receipts of account-export/deletion events. It is **not** the
78authority for accounts (Network), file bytes (Media), event delivery (Events), money (Billing), or the host/database/
79Valkey (Host). Live's old `user_cosmetics`/`user_equipped` are Live's until
80[scripts/import-live.js](scripts/import-live.js) has converted them; since ADR-054 Live reads and writes only through
81this API. Account deletion removes a person's items and equipped set but keeps ledger rows without the subject, so
82supply counts stay true.
83
84## Auth and Capabilities
85
86- Capabilities (Contracts): `inventory.item.read`, `inventory.item.list`, `inventory.equip.manage`,
87 `inventory.item.grant`, `inventory.item.consume`, `inventory.definition.manage`, `inventory.definition.review`,
88 and `inventory.store.list` (pins Contracts until released).
89- A person acts for themself with their Network token or this site's session; a cookie write must be same-origin. A
90 service acting for a person sends `X-OV-Subject: usr_…` and holds the route capability. Public reads need no token;
91 a presented token must still hold `inventory.item.read`.
92- Public reads (`/kinds`, `/definitions`, `/people/:subject/*`, `/equipped`) answer `Access-Control-Allow-Origin: *`
93 with no credentials so any site can render what people wear; every other route stays same-site. Do not add a
94 credentialed wildcard.
95- Outbound: `events.event.publish` + `events.subscription.manage`, `identity.subject.resolve`, and Media
96 `media.object.upload` on namespace `inventory`; store purchases use Billing with the person's own bearer.
97- Tokens are audience `openvibe.inventory`. Refusals are RFC 9457 problem+json with stable `inventory.*` codes.
98
99## API
100
101`/api/v1`: `GET /ping`, `GET /kinds[/:id]`, `GET /definitions[/:id]`, `GET /people/:subject/items|equipped|holdings|holdings/count`,
102`GET /equipped?subjects=a,b,…`, `GET /me/items|holdings|holdings/count`, `PUT /me/equipped`, `POST /grants`,
103`POST /instances/:id/consume|revoke`, `POST /definitions`, `PATCH /definitions/:id`,
104`GET /store`, `GET /store/purchases/:id`, `POST /store/:id/buy|list|review`.
105
106Holdings: 60 stacks per page (max 200), filters `issuer|site`, `kind`, `rarity`, `equipped`, `tradable`, `text`; sort
107`newest|name|rarity|count`; pass `next_cursor` back with the same filters. The cursor pins the newest instance, so
108later grants never move a page. `tradable=true` is empty: there is no trade/transfer operation. Events:
109`inventory.item.granted|consumed|revoked|equipped|unequipped`, `inventory.definition.published`.
110
111## Conventions
112
113- CommonJS, 4-space indent, single quotes, semicolons; no linter/formatter. Route handlers via
114 [server/http/router.js](server/http/router.js); never leak a raw row or a secret.
115- Every browser link goes through [server/urls.js](server/urls.js) so the `openvibe.my/inventory` base path survives
116 redirects, discovery and the Markdown twins. API paths never take the prefix.
117- New schema = a new `migrations/NNNN_name.sql`; never edit a migration that has run. Keep `inventory_` prefixes.
118- CSP: add origins to `helmet` in [server/app.js](server/app.js). Media images come from `openvibe.media`; Billing and
119 the wallet's connect-src are already named. Never widen `frameAncestors`/`objectSrc`.
120- Phone first: pages must fit 390 px without horizontal scroll; `/me` works with plain forms and no JS.
121- Empty states use the shared kit, never bespoke markup; "Calm reduces, only Off removes" applies to any hide
122 preference (never delete).
123- No Font Awesome; use `openvibe-shared ov-icons` (`KIND_ICON` in [server/http/pages.js](server/http/pages.js)).
124- Nothing here sells, buys, trades or converts an item without a reviewed store listing and Billing.
125
126## Key Pitfalls
127
128- **Base path.** The site can be mounted under `/inventory`; a hard-coded `href="/items"` breaks production. Use
129 `urls.path()` and `urls.html()`.
130- **Store review is mandatory.** A price edit re-enters review; no price may be invented. An order reserves one
131 serial under the ordinary grant lock; an uncertain response is recovered by retrying the same item/currency, and
132 pending reservations do not expire by themselves.
133- **Grants are idempotent per key**; a repeat must not issue a second instance. A capped item answers 409
134 `inventory.supply_exhausted`.
135- **Public CORS is deliberately credential-free.** `/me` and all writes must stay same-site; wallet-cors is only for
136 the named first-party origins in [server/http/wallet-cors.js](server/http/wallet-cors.js).
137- **Workshop images** are checked by their own bytes (64–512 px PNG/WebP, ≤200 KB), stored in Media's tenant, and
138 never written to the database. On deletion a creator's badges pass to the kind issuer, retired.
139- **Drill mode** (`INVENTORY_DRILL`) keeps the outbox relay, subscriptions and timers off; add new background work
140 there.
141- **Import Live's cosmetics** only with the dry run, `--apply`, then `--verify`; never print Live's URL or secrets.
142- **`event_outbox`** needs its rejected partial index ([migrations/0007_outbox_rejected_index.sql](migrations/0007_outbox_rejected_index.sql)).
143
144## Testing
145
146`npm test` runs each file in its own process on a temp PGlite database with mocked Network/Media/Billing:
147[test/inventory-api.test.js](test/inventory-api.test.js), [test/holdings.test.js](test/holdings.test.js),
148[test/store.test.js](test/store.test.js), [test/workshop.test.js](test/workshop.test.js),
149[test/workshop-media-client.test.js](test/workshop-media-client.test.js), [test/public-cors.test.js](test/public-cors.test.js),
150[test/base-path.test.js](test/base-path.test.js), [test/import-live.test.js](test/import-live.test.js),
151account-data, search-index, SSO, limits, secret-leak/redirect, layout and perf budgets. Run the narrowest file first,
152then the suite; `npm run test:pg` for PostgreSQL. Always `node --check` changed files.
153
154## Documentation
155
156- [README.md](README.md) — purpose, ownership, capabilities, model, API, Workshop, pages, Live migration,
157 configuration, acceptance, deploy, security notes.
158- [docs/store.md](docs/store.md) — the universal store, review, orders and Billing matching.
159- [STATUS.json](STATUS.json) — stage, deployed flag, features and not-yet list.
160- [.env.example](.env.example) — variable names only; never commit values.
161- [CHANGELOG.md](CHANGELOG.md), [SECURITY.md](SECURITY.md).
162