openvibers/openvibe.inventory AGENTS.md
AGENTS.md
| 1 | # AGENTS.md — OpenVibe.Inventory |
| 2 | |
| 3 | ## Project Overview |
| 4 | |
| 5 | OpenVibe.Inventory is the network-wide inventory for one OpenVibe account (ADR-054): the item kinds a surface can |
| 6 | render, the definitions an issuer publishes, the instances a person owns, the equipped set, the ledger, the community |
| 7 | Workshop 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 |
| 17 | OpenVibe.Media (a Workshop image is uploaded to Inventory's Media tenant and referenced by `med_` id). Events are |
| 18 | published through the SDK outbox to OpenVibe.Events; account export/deletion arrives there at `/internal/events`. |
| 19 | Money is OpenVibe.Billing: the store snapshots a reviewed price, Billing authorizes the spend, and Inventory matches |
| 20 | Billing's payment before granting. Inventory never moves money itself. |
| 21 | |
| 22 | ## Commands |
| 23 | |
| 24 | ```bash |
| 25 | npm ci # install (contracts/sdk/shared pinned by tag) |
| 26 | npm run dev # http://localhost:5030 (base path from PUBLIC_URL/BASE_URL) |
| 27 | npm start # node server/index.js |
| 28 | npm test # every test/*.test.js on temp PGlite, mocked Network/Media/Billing |
| 29 | npm run test:pg # the same through PostgreSQL + PgBouncer |
| 30 | npm test -- holdings # only test files whose name contains the word |
| 31 | npm test -- --strict # a skipped test fails the run |
| 32 | node --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: |
| 36 | pages 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 | |
| 76 | Inventory is the authority for kinds, definitions, instances, grants, the equipped set and the ledger, plus its |
| 77 | Workshop definitions, store listings/orders and the receipts of account-export/deletion events. It is **not** the |
| 78 | authority for accounts (Network), file bytes (Media), event delivery (Events), money (Billing), or the host/database/ |
| 79 | Valkey (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 |
| 81 | this API. Account deletion removes a person's items and equipped set but keeps ledger rows without the subject, so |
| 82 | supply 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 | |
| 106 | Holdings: 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 |
| 108 | later 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), |
| 151 | account-data, search-index, SSO, limits, secret-leak/redirect, layout and perf budgets. Run the narrowest file first, |
| 152 | then 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 |