openvibers/openvibe.inventory Commits

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

OP OpenVibers committed 1 days ago · c232be8a8d · 2 files, +162 −0

Adds AGENTS.md (ownership, commands, architecture, boundaries, pitfalls, tests) and CLAUDE.md.

Changes

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