openvibers/openvibe.blog Commits

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

OP OpenVibers committed 5 Oct 2026 · deae9d2d20 · 8 files, +70 −36

## Files changed

- **server/auth/capabilities.js** — removed the proposed-id shim (the `PROPOSED` set, the `!capabilities.get()` fallback branch, and `PROPOSED` from the exports); `checkCapability` now delegates straight to `capabilities.check`, because openvibe-contracts v0.97.0 defines all twelve `blog.*` ids. `CAPABILITIES` is kept as the one shared id list.
- **test/capabilities.test.js** (new, per Reviews 440a865) — pins that every guarded id is defined by the installed contracts and by the `blog` service manifest, and that `checkCapability` agrees with `capabilities.check` for an exact grant, a `blog.*` prefix grant, a sibling/no grant (`capability.denied`), and an undefined id (`capability.unknown`, proving it delegates rather than deciding locally).
- **test/contracts.test.js** — swapped the now-removed `PROPOSED` import/usages for `CAPABILITIES` (`Object.values`) so the proposal-validation test keeps working.
- **STATUS.json** — `contracts` and the `packages` entry v0.79.0 → v0.97.0; `updated` 2026-09-29 → 2026-10-04.
- **README.md** — version text v0.18.0/v0.33.0/v0.79.0 → v0.97.0 and rewrote the stale "proposal / decided locally" sentences to say the ids and manifest are released and decided by the library's grant rule; corrected the adjacent stale package pins `openvibe-publishing` v1.2.0 → v1.3.0 and `openvibe-shared` v2.6.0 → v2.9.0 (package.json already pins those); added a `test/capabilities.test.js` row to the acceptance table.
- **CHANGELOG.md** — Unreleased entry for the contracts v0.79.0 → v0.97.0 move and the guard simplification.

`package.json` / `package-lock.json` were already bumped to v0.97.0 by the harness and were **not** touched.

## Tests run

- `node test/capabilities.test.js` → **all passed** (2 ok).
- `ov test capabilities contracts changelog api-ai` → **4 passed in 10 s** (the three `ov test` selected files plus `api-ai.test.js`, which exercises the guard through `server/http/api.js`).
- `ov test members-vip` → **1 passed in 4 s** (also asserts capability codes).
- `node node_modules/openvibe-contracts/bin/check-service.js --service blog --src server` → **ok**: contracts 0.97.0, 12 capabilities enforced, 0 contract ids validated.

## Could not do / notes

- `ov test --all` was not run: the harness explicitly forbids whole-suite runs (suites overload the shared machine; CI's verify runs everything). I ran only the files this change reaches.
- There is no `test/pins*.test.js` or any docs/STATUS-version test in the repository (checked), so there was nothing of that kind to run.
- `docs/capabilities-proposal/` and `docs/service-manifest-proposal.json` are retained as the historical source proposals (now released upstream), matching the Reviews precedent rather than deleting them; `check-service` validates them fine.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 480084c..75ceed8 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 Blog, and the contracts' own service check is green. All twelve `blog.*` 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, and a blog's theme preset stays on its content surface. The shell adds `web-runtime.js`, so the home page's JS budget is raised to 5 files, 245 KB, 59 KB brotli (measured 239.1 KB, 56.3 KB brotli; 212.2 KB, 49.9 KB before).
ddiff --git a/README.md b/README.md
iindex 06f89f5..20c6466 100644
---- a/README.md
++++ b/README.md
@@@ -4,7 +4,7 @@
**Status:** alpha (roadmap Wave 16, Blog half). **Public at https://openvibe.blog since 2026-09-23**
(the launch release also removed the domain from OpenVibe.Sites). Its capabilities and service
--manifest are released in openvibe-contracts v0.18.0. The network changelog publishes Patch notes
++manifest are released in openvibe-contracts v0.97.0. The network changelog publishes Patch notes
posts on the official blog ([below](#the-network-changelog-and-patch-notes)); the seed post stays a
draft until a person reviews it.
**Domain:** `openvibe.blog` · **Port:** 4810 · **Service id:** `blog`
@@@ -224,7 +224,7 @@ OpenVibe.Network proxies this feed at `openvibe.network/api/v1/changelog`. Every
| `blog.moderation.action` | internal. Staff unpublished (`post.unpublished`) or deleted (`post.deleted`) a post that only their staff powers let them touch, for the network's moderation audit log (ADR-022, `common.moderation-action@1`). Never the content. |
| `blog.index_document.upserted` / `.deleted` | Documents and tombstones in `search.index-document@1` form, with a monotonic index revision (`createIndexSequencer`). Only published, public, listable posts are upserted; every other state is a tombstone. A post that was never indexed gets no tombstone. |
--## Capabilities (released in openvibe-contracts v0.18.0; proposal: `docs/capabilities-proposal/`)
++## Capabilities (released in openvibe-contracts v0.97.0)
Service tokens use audience `openvibe.blog`, with one capability per route. The person the service
acts for goes in `X-OV-Subject`, and membership still applies:
@@@ -235,10 +235,11 @@ acts for goes in `X-OV-Subject`, and membership still applies:
`blog.post.schedule`, `blog.post.unpublish`, `blog.post.delete`
- `blog.feed.read`
--Browser and app user JWTs are judged by blog membership. Grants for these ids are decided locally
--with the contracts library's matching rule (`server/auth/capabilities.js`). The service manifest
--(proposal: `docs/service-manifest-proposal.json`) is released in openvibe-contracts v0.18.0; this
--repo pins v0.33.0.
++Browser and app user JWTs are judged by blog membership. The ids and the service manifest are
++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.
Called elsewhere, as the service principal `blog`: `identity.subject.resolve` and
`network.integration.github.read` (Network), `events.event.publish` (Events),
@@@ -251,9 +252,9 @@ Called elsewhere, as the service principal `blog`: `identity.subject.resolve` an
- **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** (all pinned by release tarball): `openvibe-publishing` v1.2.0 (async PostgreSQL stores: revisions,
++- **Packages** (all pinned by release tarball): `openvibe-publishing` v1.3.0 (async PostgreSQL stores: revisions,
schedule, taxonomy, citations, media, discussion, seo, authorship, index-hooks, ssr, layout), `openvibe-contracts`
-- v0.79.0, `openvibe-shared` v2.6.0 (Frame, app icon, footer, legal, release, metrics, ready, theme presets),
++ v0.97.0, `openvibe-shared` v2.9.0 (Frame, app icon, footer, legal, release, metrics, ready, theme presets),
`openvibe-sdk` v0.26.0 (sso, db, PostgreSQL events outbox, service tokens, per-actor limits, testing).
- **OpenVibe.Network:**
- SSO: the OAuth client `blog` is already seeded with redirect
@@@ -302,6 +303,7 @@ Each grant is `[client, capability, audience]`:
| Capability-guarded service tokens, X-OV-Subject membership, AI drafts that need a person's review, and problem+json with request ids. | `test/api-ai.test.js` |
| The seed post, readiness, release, robots, llms.txt and the sitemap index. | `test/seed-ops.test.js` |
| The contract proposals are valid against the released schemas and match the code. | `test/contracts.test.js` |
++| Capability guards delegate to the released contracts: every guarded id is defined, and grants follow the library's rule (exact, prefix, denied, unknown). | `test/capabilities.test.js` |
| IndexNow: off without `INDEXNOW_KEY` (no key route, nothing sent); with one the key file is served at `/<key>.txt` as `text/plain` and a publish or unpublish pings the post path and the sitemap; a draft never pings. | `test/indexnow.test.js` |
## Launch rule
@@@ -315,7 +317,7 @@ existed; the launch release went out on 2026-09-23. Status against each point:
3. **SSR public routes useful without JS:** done.
4. **Persistence and end-to-end workflows:** done; `ovhost drill blog` restored it on the production
host on 2026-09-23.
--5. **Capability and event registration against OpenVibe.Contracts:** released in v0.18.0
++5. **Capability and event registration against OpenVibe.Contracts:** released in v0.97.0
(capabilities and service manifest; no `blog.*` event payload schemas yet).
6. **Migration and seed strategy, threat review, sitemap/robots/feed behaviour:** done. There is
nothing to migrate, the seed is described below and the threat review is below.
@@@ -447,7 +449,7 @@ First install (done once; kept for a rebuild):
4. **systemd:** install `deploy/systemd/openvibe-blog.service` (port 4810, `StateDirectory=openvibe-blog`).
5. **nginx:** install `deploy/nginx/openvibe.blog.conf`. `/metrics` is never proxied.
6. **Seed:** `npm run seed`. It stays a draft until someone has reviewed it.
--7. **Contracts:** done: released in openvibe-contracts v0.18.0.
++7. **Contracts:** done: released in openvibe-contracts v0.97.0.
8. **Launch:** in the same release, remove `openvibe.blog` from OpenVibe.Sites and flip the Network
hub entry (see the launch rule above).
ddiff --git a/STATUS.json b/STATUS.json
iindex f2384ef..b80f4f0 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -7,10 +7,10 @@
"runtime": "node 22, express 4, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9",
"port": 4810,
"domain": "openvibe.blog",
-- "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.9.0",
"openvibe-sdk v0.26.0"
],
@@@ -35,7 +35,7 @@
"deployed": true,
"deployment": "Public at https://openvibe.blog since 2026-09-23 (~03:00 UTC): deployed with `sudo ovhost deploy blog`, /opt/openvibe.blog, unit openvibe-blog on 127.0.0.1:4810 behind the openvibe.blog nginx vhost, principal blog. The network changelog publishes Patch notes posts on the official blog. Restore drill passed 2026-09-23.",
"placeholderInSites": false,
-- "updated": "2026-09-29",
++ "updated": "2026-10-04",
"deployedAt": "2026-09-23",
"features": [
"the official blog and a blog for every member, with owners, editors and authors",
ddiff --git a/package-lock.json b/package-lock.json
iindex 4cd47c9..6ab6e61 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.9.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 65c5e33..95dab90 100644
---- a/package.json
++++ b/package.json
@@@ -27,7 +27,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.9.0",
ddiff --git a/server/auth/capabilities.js b/server/auth/capabilities.js
iindex ce54e4f..fc2c239 100644
---- a/server/auth/capabilities.js
++++ b/server/auth/capabilities.js
@@@ -1,14 +1,10 @@
'use strict';
/**
-- * Capability checks for service tokens (audience openvibe.blog), including the ids Blog introduces
-- * before the contracts library knows them.
-- *
-- * openvibe-contracts' capabilities.check() answers capability.unknown for an id that is not in its
-- * manifests yet. Blog's 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.blog). The blog.* 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 their blog
* membership (domain/access.js). A service token is judged by its capability AND by the membership
@@@ -30,16 +26,10 @@ const CAPABILITIES = Object.freeze({
POST_DELETE: 'blog.post.delete',
FEED_READ: 'blog.feed.read',
});
--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..ff34338
---- /dev/null
++++ b/test/capabilities.test.js
@@@ -0,0 +1,41 @@
++'use strict';
++/**
++ * The blog.* 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 blog 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: ['blog.post.read'], sub: 'svc:openvibe.network' };
++
++(async () => {
++ await check('every guarded capability is defined by the installed contracts and the blog manifest', async () => {
++ const manifest = services.get('blog');
++ assert.ok(manifest, 'openvibe-contracts defines the blog 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, 'blog', `${id} is owned by blog`);
++ }
++ });
++
++ 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, 'blog.post.read'), { allowed: true, code: null, reason: null });
++ assert.deepStrictEqual(checkCapability({ cap: ['blog.*'] }, 'blog.post.read'), { 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: ['blog.post.create'] }, 'blog.post.read').code, 'capability.denied');
++ assert.strictEqual(checkCapability(null, 'blog.post.read').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: ['blog.*'] }, 'blog.not.a.capability').code, 'capability.unknown');
++ });
++
++ done();
++})();
ddiff --git a/test/contracts.test.js b/test/contracts.test.js
iindex 38da2c3..6d1ca03 100644
---- a/test/contracts.test.js
++++ b/test/contracts.test.js
@@@ -8,7 +8,7 @@ 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 DIR = path.join(__dirname, '..', 'docs', 'capabilities-proposal');
@@@ -29,8 +29,8 @@ const DIR = path.join(__dirname, '..', 'docs', 'capabilities-proposal');
});
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(), Object.values(CAPABILITIES).sort());
++ assert.deepStrictEqual([...manifest.capabilities].sort(), Object.values(CAPABILITIES).sort());
});
await check('the service manifest proposal is a valid registry.service-manifest@1', async () => {