openvibers/openvibe.vip Commits

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

OP OpenVibers committed 1 days ago · df55a0df43 · 2 files, +105 −0

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

It covers: VIP as plans/perks/policy over Billing's entitlement truth; the nine authority and four
operational tables (including the immutable plan versions); the products that consume it and the
consumer client; the `ov_vip` migrations and trigger-enforced immutability; the capability/ownership
model and fail-closed entitlement projection; deploy; tests; conventions; and the never-store-
membership, never-price-plan, immutability, ordered-Billing-events and account-deletion 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..0ec4802
---- /dev/null
++++ b/AGENTS.md
@@@ -0,0 +1,104 @@
++# AGENTS.md — OpenVibe.VIP
++
++## Project overview
++
++OpenVibe.VIP (`https://openvibe.vip`) is membership on the OpenVibe network: creator and network plans
++with versioned terms, perks, gated-resource policy, members-only posts, and a product-wide answer to
++"is this person a member, and may they see this?". VIP owns plans and perks; **Billing owns
++subscriptions, prices and the entitlement truth** (ADR-012).
++
++- **Entry:** `server/index.js`; app `server/app.js`; port `4620`, service id `vip`.
++- **API:** `server/api/v1.js`, auth `server/api/auth.js`, limits `server/api/actor-limits.js`.
++- **Domain:** `server/domain/` — `plans.js` (versions), `perks.js`, `policies.js` (gated resources),
++ `memberships.js`, `entitlements.js` (projection), `periods.js`, `checkout.js`, `posts.js`,
++ `prices.js`, `creators.js`, `cards.js`, `markdown.js`, `account-data.js`, `index.js`.
++- **Pages and embeds:** `server/web/` (`pages.js`, `routes.js`, `layout.js`, `session.js`,
++ `embeds.js`, `forms.js`, `short.js`); the public card/badge/embed at `/embed`; `public/js/vip.js`
++ and `post-upload.js`; own CSS in `public/css`.
++- **Consumer client:** `client/vip-client.js` — the package products use to check membership and gate
++ a resource, with the product cache and its convergence bound (README "Consumer seam").
++- **Docs:** `docs/cutover-0002-renewal-periods.md` (renewal periods/grace), `SECURITY.md`.
++
++## Owns, does not own
++
++VIP is the only writer of the nine authority tables: `vip_creators`, `vip_plans`,
++`vip_plan_versions` (immutable; triggers refuse UPDATE/DELETE except the publish stamp), `vip_perks`,
++`vip_plan_perks` (immutable per version), `vip_product_bindings`, `vip_member_preferences`,
++`vip_gated_resource_rules` and `vip_migration_maps`. Operational tables: `vip_memberships` (which plan
++version a membership was bought under — never whether it is active), `vip_entitlement_projection` (a
++cache with `valid_until`), `vip_checkouts`, `vip_membership_periods` (Billing's charge records),
++`event_outbox` and `idempotency_receipts`.
++
++Does not own: subscription contracts, prices, charges, renewals, cancellations, refunds, chargebacks or
++canonical entitlements (Billing — VIP never prices anything and never moves money; plan terms carry no
++price); identities (Network); products' own resources (only an EntityRef); badge rendering (each
++product).
++
++## Who calls it, what it calls
++
++People use the pages/API with a Network user JWT and act on their own plans, perks, rules, members,
++memberships and checks; staff with `staff.site.configure` manage network plans. Services use
++client-credentials tokens for audience `openvibe.vip` with one `vip.*` capability per route. Products
++consume VIP through `client/vip-client.js` / SDK `openvibe-sdk/vip` and the product cache: Chat (member
++badge), Community (members-only spaces/threads), Blog (members-only posts), Wiki (VIP spaces/pages);
++Live is not wired. VIP calls Billing (`billing.intent.create`, `billing.subscription.manage`,
++`billing.entitlement.check`), Network (JWKS, OAuth), Events (Billing deliveries in, VIP outbox out) and
++Media (a members-only post's image, `media.object.upload`, `vip` tenant; without it the post has none).
++
++## Database and migrations
++
++PostgreSQL `ov_vip` through `openvibe-sdk/db` (PgBouncer for the app, direct owner role for
++migrations); Valkey optionally holds per-actor limit counters. Migrations `0001..0007` include plan
++tiers, account data, telemetry, member posts and the outbox rejection index; applied at boot, never
++edited after they run, only additive so a rollback works. The immutability of plan versions and
++plan-perks is enforced by database triggers, not only by code.
++
++## Entitlements and auth
++
++Every API route checks one capability (service) or ownership (person); the API never reads cookies.
++Billing events arrive HMAC-SHA256-signed over the raw body (`VIP_EVENTS_SECRET`, rotation by list),
++`source` must be `billing`, the inbox is exactly-once and the ordering guard rejects a stale period.
++**Fail closed everywhere a membership is decided:** "unknown" is never "yes"; the projection has a
++`valid_until` and a convergence bound, and an expired/unsynced row is not a membership.
++
++## Deploy
++
++`sudo ovhost deploy vip` (git-checkout: fast-forward `/opt/openvibe.vip`, install on a lockfile
++change, restart, wait for `/api/ready`; rollback `sudo ovhost rollback vip --to <sha>`). Unit
++`openvibe-vip.service` on `127.0.0.1:4620`, env `/etc/openvibe/vip.env`, database `ov_vip`, nginx
++`deploy/nginx/openvibe.vip.conf` (live since 2026-10-17). The release migrates at boot.
++
++## Commands and tests
++
++```bash
++npm ci
++npm run dev # http://127.0.0.1:4620
++npm test # every test/*.test.js, in-process Network/Billing stubs
++npm test -- plans import # only matching files
++npm run test:pg # the same on PostgreSQL
++npm run subscribe # create the Events subscriptions once
++```
++
++`test/run.js` uses `openvibe-shared/test-runner` (60 s, serial); temp databases and stubs, never the
++network. Tests cover immutable plan versions, perks/bindings, gated policy incl. sensitive resources,
++the Billing projection and convergence, renewal periods/grace/refunds, checkout/cancel, member posts,
++the consumer client/cache, the embed/card, account export/deletion (a paid period stays), and the
++pages.
++
++## Conventions and pitfalls
++
++- CommonJS, 4-space indent, single quotes, semicolons; RFC 9457 problem+json; escaped HTML and a safe
++ Markdown renderer (a post source can never carry markup).
++- **Never store "is a member".** `vip_memberships` says which version was bought; access is answered
++ by the entitlement check/projection, and a projection row is a cache with a validated `valid_until`.
++- **Never put a price in plan terms.** The terms say Billing sets and charges it; `vip_prices` is a
++ display of Billing's numbers, not an authority.
++- **Immutability is real:** do not add an UPDATE path to `vip_plan_versions`/`vip_plan_perks`; publish
++ stamps once, then a new version is a new row.
++- **A gated resource rule is policy, not content:** add a `{service, type, id}` EntityRef rule and a
++ product binding; the product still renders its own resource.
++- **Billing is exactly-once and ordered:** a redelivery, a stale `billing.entitlement.changed` or a
++ reversed transaction must converge, not double-count a period or grant access after a refund.
++- **Deleting a member does not delete the books:** memberships/preferences/projection/checkouts go,
++ paid periods stay; a deleted creator is suspended without their name but plan versions members joined
++ under stay whole.
ddiff --git a/CLAUDE.md b/CLAUDE.md
nnew file mode 100644
iindex 0000000..43c994c
---- /dev/null
++++ b/CLAUDE.md
@@@ -0,0 +1 @@