openvibers/openvibe.deals Commits

Pin openvibe-contracts v0.97.0 and drop the proposed-capability shim (#9)

OP OpenVibers committed 5 Oct 2026 · 65f1a8d9ba · 8 files, +77 −40

**Files changed**

- `server/auth/capabilities.js`: dropped `PROPOSED`, the local `capabilities.grants` fallback and the `PROPOSED` export; `checkCapability` now delegates straight to `capabilities.check` (all twelve `deals.*` ids are defined by the installed release), and the docstring now says so.
- `test/capabilities.test.js` (new): pins Reviews-440a865 behaviour adapted to Deals — every guarded id is defined by openvibe-contracts and the `deals` manifest (owner `deals`), and `checkCapability` agrees with `capabilities.check` for exact grant, `deals.*` prefix grant, sibling/no grant (`capability.denied`) and an undefined id (`capability.unknown`, proving delegation).
- `test/contracts.test.js`: imported the deleted `PROPOSED` — now derives the id set from `CAPABILITIES` (local `CAPS`) and refreshed the stale "next release" docstring; assertions unchanged and still pass against the released schemas.
- `STATUS.json`: `contracts` and `packages` `v0.79.0` → `v0.97.0`; also corrected the stale `openvibe-sdk v0.25.1` → `v0.26.0` on the same list and bumped `updated` to 2026-10-04.
- `README.md`: contracts pin `v0.79.0` → `v0.97.0` (status line, packages line) and the three stale `v0.23.0` registration mentions (status, launch item 5, launch item 8) → `v0.97.0`; rewrote the proposals sentence to name the pinned release and the contracts grant rule; corrected the stale `openvibe-publishing v1.2.0` → `v1.3.0` and `openvibe-sdk v0.25.1` → `v0.26.0` on the packages line; added an acceptance-table row for `test/capabilities.test.js`.
- `CHANGELOG.md`: Unreleased entry for the v0.79.0 → v0.97.0 move, the shim removal and the new test.

`package.json` / `package-lock.json` were left exactly as the harness set them (already v0.97.0) — not touched.

**Tests run**
- `ov test test/capabilities.test.js test/contracts.test.js test/lockfile.test.js` → 3 passed in 1 s
- `ov test test/api.test.js` (exercises the service-token capability path end-to-end) → 1 passed in 4 s
- `node node_modules/openvibe-contracts/bin/check-service.js --service deals --src server` → ok: contracts 0.97.0, 33 files, 12 capabilities enforced, 0 contract ids validated

**Not done / notes**
- Did not run `ov test --all`: the session rules forbid whole-suite runs on this shared machine (CI runs it after push). Targeted files cover the change.
- Used v0.97.0 everywhere, not the brief's v0.96.0, per the override.
- Kept `docs/capabilities-proposal/` and `docs/service-manifest-proposal.json`: they are the source proposals (Reviews kept the same), and `test/contracts.test.js` still validates them against the released schemas.
- `node_modules` appears as an untracked symlink; that is the harness's dependency link, not mine to commit.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex f2ea02c..4b33bff 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -2,4 +2,5 @@
## Unreleased
++- openvibe-contracts moves from v0.79.0 to v0.97.0 (pin, lockfile and `node_modules`); nothing in the range breaks Deals, and the contracts' own service check is green. All twelve `deals.*` ids are now defined by the release, so `server/auth/capabilities.js` drops its local fallback for proposed ids and sends every check through the library's grant rule; `test/capabilities.test.js` pins that every guarded id is defined and that exact, prefix, denied and unknown ids answer as `capabilities.check()` does. README and STATUS.json name v0.97.0.
- Every page is rendered through `openvibe-publishing/layout` (v1.2.0, on `openvibe-shared/shell` v2.6.0): the head, the Frame, the noscript navigation, the footer and its init come from the shared document; robots and the canonical still come from the indexability gate's decision. Styling is unchanged.
ddiff --git a/README.md b/README.md
iindex 114f88d..aa18a31 100644
---- a/README.md
++++ b/README.md
@@@ -5,7 +5,7 @@
**Status:** alpha (roadmap Wave 18, Deals). The service runs and its tests pass. It is **deployed
internally, not launched**: it runs on the production host on 127.0.0.1:4840 only (release `e72aeec`,
`/api/ready` 200) with an empty database (0 offers), and `openvibe.deals` still shows its placeholder
--from OpenVibe.Sites. Its capabilities and service manifest are registered in openvibe-contracts v0.23.0.
++from OpenVibe.Sites. Its capabilities and service manifest are registered in openvibe-contracts v0.97.0.
**Domain:** `openvibe.deals` · **Port:** 4840 · **Service id:** `deals`
**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §12.9; roadmap §15.13, §29, §32.
**License:** AGPL-3.0 (same as every OpenVibe service).
@@@ -91,9 +91,10 @@ the person in `X-OV-Subject`; browsers or apps with a Network user JWT are judge
| `GET /watches` · `POST /watches` · `DELETE /watches/:id` | `deals.watch.read` · `deals.watch.create` · `deals.watch.delete` |
The eight charter capabilities are all there; `deals.offer.merge`, `deals.offer.moderate`,
--`deals.watch.read` and `deals.flag.create` are additions (released in openvibe-contracts v0.23.0
--from the proposals in [docs/capabilities-proposal/](docs/capabilities-proposal/) and
--[docs/service-manifest-proposal.json](docs/service-manifest-proposal.json)).
++`deals.watch.read` and `deals.flag.create` are additions (released in openvibe-contracts, pinned at
++v0.97.0; the source proposals stay in [docs/capabilities-proposal/](docs/capabilities-proposal/) and
++[docs/service-manifest-proposal.json](docs/service-manifest-proposal.json)); `server/auth/capabilities.js`
++decides them with the contracts grant rule.
### Events (SDK outbox, written in the same transaction as the change)
@@@ -252,11 +253,11 @@ render time, so the cache stays short); signed-in views, forms, API responses, e
- **PostgreSQL 18 and Valkey 9** (OpenVibe.Host `roles/data/`, ADR-035): every read and write is async through
`openvibe-sdk/db`; Valkey holds the per-actor limit counters (optional: without `VALKEY_URL` they count per process).
--- **Packages** (pinned release tarballs): `openvibe-publishing` v1.2.0 (seo gate, JSON-LD, feeds,
++- **Packages** (pinned release tarballs): `openvibe-publishing` v1.3.0 (seo gate, JSON-LD, feeds,
sitemaps, index-hooks, discussion, ssr, and the shared ingest/publication chassis — the Sources
client, change cursor and pull loop, signed event consumer, common normalisers, and the publication
-- glue Deals' indexing uses), `openvibe-contracts` v0.79.0, `openvibe-shared` v2.6.0
-- (Frame, app icon, footer, legal, release, metrics, ready), `openvibe-sdk` v0.25.1 (sso, outbox,
++ glue Deals' indexing uses), `openvibe-contracts` v0.97.0, `openvibe-shared` v2.6.0
++ (Frame, app icon, footer, legal, release, metrics, ready), `openvibe-sdk` v0.26.0 (sso, outbox,
inbox, webhook signatures v2, service tokens, per-actor limits).
- **OpenVibe.Network:** SSO (OAuth client `deals`, registered in production), JWKS,
`identity.subject.resolve`.
@@@ -309,6 +310,7 @@ full list with audiences is under [Grants the Network must hold](#grants-the-net
| Useful without JavaScript: no seed deals, forms with tokens, votes, Community comments (referenced), product/store/search pages, expiry (submitter and stated), moderation (410, feeds, sitemaps, Search, thread hidden), feeds, robots, llms, sitemaps, operations endpoints | `test/pages.test.js` |
| API: one capability per route, `X-OV-Subject`, problem+json with request ids, AI text held for a person's review, votes, product resolve, merge grants, every event and Search document valid against openvibe-contracts | `test/api.test.js` |
| The contract proposals are valid against the released schemas and match the code | `test/contracts.test.js` |
++| Service-token capabilities are the contracts library's decision, not a local one: every guarded id is defined by the release and the deals manifest, and exact, prefix, denied and unknown ids answer as `capabilities.check` does | `test/capabilities.test.js` |
## Launch rule
@@@ -320,7 +322,7 @@ exists. Status against each point:
2. **Canonical identity and auth:** done; the `deals` OAuth client, service principal and grants exist in production.
3. **SSR public routes useful without JS:** done.
4. **Persistence and end-to-end workflows:** done and deployed on the host (loopback only, empty database); imports need a registered, enabled deals source in Sources, and none is enabled.
--5. **Capability and event registration against OpenVibe.Contracts:** done (openvibe-contracts v0.23.0).
++5. **Capability and event registration against OpenVibe.Contracts:** done (openvibe-contracts v0.97.0).
6. **Migration and seed strategy, threat review, sitemap/robots/feed behaviour:** done. There is nothing to migrate and no seed (below); the threat review is below.
7. **Acceptance tests:** done.
@@@ -400,7 +402,7 @@ First install (done once; kept for a rebuild):
5. **systemd:** install `deploy/systemd/openvibe-deals.service` (port 4840).
6. **nginx:** install `deploy/nginx/openvibe.deals.conf`. `/metrics` and `/internal/` are never proxied.
7. **Events subscription:** `node scripts/subscribe.js` (optional — the importer polls anyway).
--8. **Contracts:** done: the capabilities and manifest are released in openvibe-contracts v0.23.0,
++8. **Contracts:** done: the capabilities and manifest are released in openvibe-contracts v0.97.0,
and CI's contracts check runs against them.
9. **Launch:** in the same release, remove `openvibe.deals` from OpenVibe.Sites and flip the Network
hub entry (see the launch rule above).
ddiff --git a/STATUS.json b/STATUS.json
iindex 759587c..b5b5f15 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -7,12 +7,12 @@
"runtime": "node 22, express 4, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9",
"port": 4840,
"domain": "openvibe.deals",
-- "contracts": "openvibe-contracts v0.79.0",
++ "contracts": "openvibe-contracts v0.97.0",
"packages": [
"openvibe-publishing v1.3.0",
-- "openvibe-contracts v0.79.0",
++ "openvibe-contracts v0.97.0",
"openvibe-shared v2.6.0",
-- "openvibe-sdk v0.25.1"
++ "openvibe-sdk v0.26.0"
],
"capabilities": [
"deals.offer.submit",
@@@ -36,7 +36,7 @@
"deployed": true,
"deployment": "Deployed with `sudo ovhost deploy deals`: unit openvibe-deals, 127.0.0.1:4840 loopback only; Network OAuth client deals and its grants exist; on 2026-09-23 /api/ready answered 200 and the production database held 0 offers. Deployed internally, not launched: openvibe.deals still serves the OpenVibe.Sites placeholder until a person verifies a deals source's terms and enables it in Sources",
"placeholderInSites": true,
-- "updated": "2026-09-29",
++ "updated": "2026-10-04",
"features": [
"offers with timestamped price observations shown \"as of\" and stale labels",
"unknown prices stay unknown in pages, JSON, JSON-LD, feeds and Search",
ddiff --git a/package-lock.json b/package-lock.json
iindex bd9a3ba..a2db3fc 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -15,7 +15,7 @@
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.79.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.97.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.3.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
@@@ -873,9 +873,9 @@
}
},
"node_modules/openvibe-contracts": {
-- "version": "0.79.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.79.0",
-- "integrity": "sha512-FY5GLbiOq0kDw5lUPPzX6h3ATbzvsr/mX5DUCF4GRfn6vGMP0p89rZZDhgtVDaS+yCBCYMZ8zFaZxuHEkEVwVg==",
++ "version": "0.97.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.97.0",
++ "integrity": "sha512-HPGBJYx6Dh2kaTg5B41WGhpw38zUdoU1VnWtkcsFkaU4E521uQOJxf6sIaLdHMsahqov7b9k2HKEFCnD9065MQ==",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex 2f64dcc..1c7f709 100644
---- a/package.json
++++ b/package.json
@@@ -26,7 +26,7 @@
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.79.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.97.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.3.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
ddiff --git a/server/auth/capabilities.js b/server/auth/capabilities.js
iindex f7b6ff3..61675e5 100644
---- a/server/auth/capabilities.js
++++ b/server/auth/capabilities.js
@@@ -1,14 +1,10 @@
'use strict';
/**
-- * Capability checks for service tokens (audience openvibe.deals), including the ids Deals
-- * introduces before the contracts library knows them.
-- *
-- * openvibe-contracts' capabilities.check() answers capability.unknown for an id that is not in its
-- * manifests yet. Deals' ids are proposed in docs/capabilities-proposal/ for the next contracts
-- * release; until then a grant is decided locally with the library's own matching rule (the exact
-- * id, or a `prefix.*` grant covering it). An id the library does know always goes through the
-- * library, so the day the release lands nothing changes here.
++ * Capability checks for service tokens (audience openvibe.deals). The deals.* ids this service
++ * introduces are defined by the installed openvibe-contracts, so a grant is decided by the library's
++ * own matching rule (the exact id, or a `prefix.*` grant covering it). CAPABILITIES keeps the ids in
++ * one place for the guards, the proposal documents and the tests.
*
* Browsers (Network user JWTs) are never judged by capabilities: they are judged by the domain
* (signed in, submitter, moderator). A service token is judged by its capability AND, for actions a
@@@ -30,16 +26,10 @@ const CAPABILITIES = Object.freeze({
PRODUCT_RESOLVE: 'deals.product.resolve',
FLAG_CREATE: 'deals.flag.create',
});
--const PROPOSED = new Set(Object.values(CAPABILITIES));
/** → { allowed, code, reason } like capabilities.check(). */
function checkCapability(claims, capabilityId) {
-- if (!capabilities.get(capabilityId) && PROPOSED.has(capabilityId)) {
-- return capabilities.grants(claims && claims.cap, capabilityId)
-- ? { allowed: true, code: null, reason: null }
-- : { allowed: false, code: 'capability.denied', reason: `${capabilityId} not granted` };
-- }
return capabilities.check(claims, capabilityId);
}
--module.exports = { CAPABILITIES, PROPOSED, checkCapability };
++module.exports = { CAPABILITIES, checkCapability };
ddiff --git a/test/capabilities.test.js b/test/capabilities.test.js
nnew file mode 100644
iindex 0000000..9c83866
---- /dev/null
++++ b/test/capabilities.test.js
@@@ -0,0 +1,41 @@
++'use strict';
++/**
++ * The deals.* capabilities are released by openvibe-contracts, so the service guards delegate to
++ * the library's grant rule rather than deciding a proposed id locally (the fallback that existed
++ * while the ids were only proposals). These pin that: every guarded id is defined by the installed
++ * contracts and by the deals service manifest, and checkCapability agrees with capabilities.check
++ * for an exact grant, a prefix grant, no grant, and an id the contracts do not define.
++ */
++const assert = require('assert');
++const { capabilities, services } = require('openvibe-contracts');
++const { checkCapability, CAPABILITIES } = require('../server/auth/capabilities');
++const { check, done } = require('./helpers/boot');
++
++const GRANTED = { cap: ['deals.offer.submit'], sub: 'svc:community' };
++
++(async () => {
++ await check('every guarded capability is defined by the installed contracts and the deals manifest', async () => {
++ const manifest = services.get('deals');
++ assert.ok(manifest, 'openvibe-contracts defines the deals service manifest');
++ assert.deepStrictEqual([...manifest.capabilities].sort(), Object.values(CAPABILITIES).sort());
++ for (const id of Object.values(CAPABILITIES)) {
++ const cap = capabilities.get(id);
++ assert.ok(cap, `${id} is defined by openvibe-contracts`);
++ assert.strictEqual(cap.owner, 'deals', `${id} is owned by deals`);
++ }
++ });
++
++ await check('checkCapability follows the contracts grant rule (exact, prefix, denied, unknown)', async () => {
++ // An exact grant and a `prefix.*` grant the library's matching rule accepts.
++ assert.deepStrictEqual(checkCapability(GRANTED, 'deals.offer.submit'), { allowed: true, code: null, reason: null });
++ assert.deepStrictEqual(checkCapability({ cap: ['deals.*'] }, 'deals.offer.submit'), { allowed: true, code: null, reason: null });
++ // A grant of a sibling capability, or no cap at all, is denied with the library's code.
++ assert.strictEqual(checkCapability({ cap: ['deals.offer.update'] }, 'deals.offer.submit').code, 'capability.denied');
++ assert.strictEqual(checkCapability(null, 'deals.offer.submit').code, 'capability.denied');
++ // An id the contracts do not define still answers capability.unknown — the guard delegates,
++ // it never decides an unknown id on its own.
++ assert.strictEqual(checkCapability({ cap: ['deals.*'] }, 'deals.not.a.capability').code, 'capability.unknown');
++ });
++
++ done();
++})();
ddiff --git a/test/contracts.test.js b/test/contracts.test.js
iindex 41bd5f4..8406a73 100644
---- a/test/contracts.test.js
++++ b/test/contracts.test.js
@@@ -1,14 +1,17 @@
'use strict';
/**
-- * The proposals the lead releases in the next openvibe-contracts version are valid against the
-- * released schemas, match what the code enforces and emits, and do not collide with released ids.
++ * The capability and service-manifest proposals — now released in openvibe-contracts — stay valid
++ * against the installed schemas, match what the code enforces and emits, and do not collide with
++ * released ids.
*/
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const contracts = require('openvibe-contracts');
const { check, done } = require('./helpers/boot');
--const { PROPOSED } = require('../server/auth/capabilities');
++const { CAPABILITIES } = require('../server/auth/capabilities');
++
++const CAPS = new Set(Object.values(CAPABILITIES));
const DIR = path.join(__dirname, '..', 'docs', 'capabilities-proposal');
const SRC = path.join(__dirname, '..', 'server');
@@@ -35,14 +38,14 @@ function sources(dir) {
});
await check('the charter capabilities are all there', async () => {
-- for (const id of ['deals.offer.submit', 'deals.offer.update', 'deals.offer.expire', 'deals.vote.set', 'deals.vote.remove', 'deals.watch.create', 'deals.watch.delete', 'deals.product.resolve']) assert.ok(PROPOSED.has(id), id);
++ for (const id of ['deals.offer.submit', 'deals.offer.update', 'deals.offer.expire', 'deals.vote.set', 'deals.vote.remove', 'deals.watch.create', 'deals.watch.delete', 'deals.product.resolve']) assert.ok(CAPS.has(id), id);
});
await check('the proposals are exactly the capabilities the code enforces', async () => {
-- assert.deepStrictEqual(caps.map((c) => c.id).sort(), [...PROPOSED].sort());
-- assert.deepStrictEqual([...manifest.capabilities].sort(), [...PROPOSED].sort());
++ assert.deepStrictEqual(caps.map((c) => c.id).sort(), [...CAPS].sort());
++ assert.deepStrictEqual([...manifest.capabilities].sort(), [...CAPS].sort());
const guarded = new Set([...code.matchAll(/guard\('([a-z.]+)'\)/g)].map((m) => m[1]));
-- assert.deepStrictEqual([...guarded].sort(), [...PROPOSED].sort());
++ assert.deepStrictEqual([...guarded].sort(), [...CAPS].sort());
});
await check('the service manifest proposal is a valid registry.service-manifest@1', async () => {