openvibers/openvibe.deals Commits
Pin openvibe-contracts v0.97.0 and drop the proposed-capability shim (#9)
Changes
@@@ -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.@@@ -5,7 +5,7 @@**Status:** alpha (roadmap Wave 18, Deals). The service runs and its tests pass. It is **deployedinternally, 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 Sourcesclient, 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 Networkhub entry (see the launch rule above).@@@ -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",@@@ -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",@@@ -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",@@@ -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 };nnew file mode 100644@@@ -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();++})();@@@ -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 () => {
@@ -2,4 +2,5 @@
@@ -2,4 +2,5 @@
## Unreleased
## 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.
- 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.
@@ -5,7 +5,7 @@
@@ -5,7 +5,7 @@
**Status:** alpha (roadmap Wave 18, Deals). The service runs and its tests pass. It is **deployed
**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`,
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
`/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`
**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.
**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).
**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
@@ -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` |
| `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`,
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)
### 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
@@ -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
- **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).
`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
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
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).
inbox, webhook signatures v2, service tokens, per-actor limits).
- **OpenVibe.Network:** SSO (OAuth client `deals`, registered in production), JWKS,
- **OpenVibe.Network:** SSO (OAuth client `deals`, registered in production), JWKS,
`identity.subject.resolve`.
`identity.subject.resolve`.
@@ -309,6 +310,7 @@ full list with audiences is under [Grants the Network must hold](#grants-the-net
@@ -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` |
| 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` |
| 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` |
| 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
## Launch rule
@@ -320,7 +322,7 @@ exists. Status against each point:
@@ -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.
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.
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.
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.
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.
7. **Acceptance tests:** done.
@@ -400,7 +402,7 @@ First install (done once; kept for a rebuild):
@@ -400,7 +402,7 @@ First install (done once; kept for a rebuild):
5. **systemd:** install `deploy/systemd/openvibe-deals.service` (port 4840).
5. **systemd:** install `deploy/systemd/openvibe-deals.service` (port 4840).
6. **nginx:** install `deploy/nginx/openvibe.deals.conf`. `/metrics` and `/internal/` are never proxied.
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).
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.
and CI's contracts check runs against them.
9. **Launch:** in the same release, remove `openvibe.deals` from OpenVibe.Sites and flip the Network
9. **Launch:** in the same release, remove `openvibe.deals` from OpenVibe.Sites and flip the Network
hub entry (see the launch rule above).
hub entry (see the launch rule above).
@@ -7,12 +7,12 @@
@@ -7,12 +7,12 @@
"runtime": "node 22, express 4, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9",
"runtime": "node 22, express 4, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9",
"port": 4840,
"port": 4840,
"domain": "openvibe.deals",
"domain": "openvibe.deals",
- "contracts": "openvibe-contracts v0.79.0",
+ "contracts": "openvibe-contracts v0.97.0",
"packages": [
"packages": [
"openvibe-publishing v1.3.0",
"openvibe-publishing v1.3.0",
- "openvibe-contracts v0.79.0",
+ "openvibe-contracts v0.97.0",
"openvibe-shared v2.6.0",
"openvibe-shared v2.6.0",
- "openvibe-sdk v0.25.1"
+ "openvibe-sdk v0.26.0"
],
],
"capabilities": [
"capabilities": [
"deals.offer.submit",
"deals.offer.submit",
@@ -36,7 +36,7 @@
@@ -36,7 +36,7 @@
"deployed": true,
"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",
"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,
"placeholderInSites": true,
- "updated": "2026-09-29",
+ "updated": "2026-10-04",
"features": [
"features": [
"offers with timestamped price observations shown \"as of\" and stale labels",
"offers with timestamped price observations shown \"as of\" and stale labels",
"unknown prices stay unknown in pages, JSON, JSON-LD, feeds and Search",
"unknown prices stay unknown in pages, JSON, JSON-LD, feeds and Search",
@@ -15,7 +15,7 @@
@@ -15,7 +15,7 @@
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"iovalkey": "^0.4.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-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-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",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
@@ -873,9 +873,9 @@
@@ -873,9 +873,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"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",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -26,7 +26,7 @@
@@ -26,7 +26,7 @@
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"iovalkey": "^0.4.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-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-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",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
@@ -1,14 +1,10 @@
@@ -1,14 +1,10 @@
'use strict';
'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
* 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
* (signed in, submitter, moderator). A service token is judged by its capability AND, for actions a
@@ -30,16 +26,10 @@ const CAPABILITIES = Object.freeze({
@@ -30,16 +26,10 @@ const CAPABILITIES = Object.freeze({
PRODUCT_RESOLVE: 'deals.product.resolve',
PRODUCT_RESOLVE: 'deals.product.resolve',
FLAG_CREATE: 'deals.flag.create',
FLAG_CREATE: 'deals.flag.create',
});
});
-const PROPOSED = new Set(Object.values(CAPABILITIES));
/** → { allowed, code, reason } like capabilities.check(). */
/** → { allowed, code, reason } like capabilities.check(). */
function checkCapability(claims, capabilityId) {
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);
return capabilities.check(claims, capabilityId);
}
}
-module.exports = { CAPABILITIES, PROPOSED, checkCapability };
+module.exports = { CAPABILITIES, checkCapability };
new file mode 100644
new file mode 100644
@@ -0,0 +1,41 @@
@@ -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();
+})();
@@ -1,14 +1,17 @@
@@ -1,14 +1,17 @@
'use strict';
'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 assert = require('assert');
const fs = require('fs');
const fs = require('fs');
const path = require('path');
const path = require('path');
const contracts = require('openvibe-contracts');
const contracts = require('openvibe-contracts');
const { check, done } = require('./helpers/boot');
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 DIR = path.join(__dirname, '..', 'docs', 'capabilities-proposal');
const SRC = path.join(__dirname, '..', 'server');
const SRC = path.join(__dirname, '..', 'server');
@@ -35,14 +38,14 @@ function sources(dir) {
@@ -35,14 +38,14 @@ function sources(dir) {
});
});
await check('the charter capabilities are all there', async () => {
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 () => {
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]));
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 () => {
await check('the service manifest proposal is a valid registry.service-manifest@1', async () => {