openvibers/openvibe.blog Commits
Pin openvibe-contracts v0.97.0 and drop the proposed-capability shim (#8)
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 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).@@@ -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 notesposts on the official blog ([below](#the-network-changelog-and-patch-notes)); the seed post stays adraft 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 serviceacts 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 productionhost 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 isnothing 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 Networkhub entry (see the launch rule above).@@@ -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",@@@ -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",@@@ -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",@@@ -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 };nnew file mode 100644@@@ -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();++})();@@@ -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 () => {
@@ -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 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).
- 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).
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
**Status:** alpha (roadmap Wave 16, Blog half). **Public at https://openvibe.blog since 2026-09-23**
**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
(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
posts on the official blog ([below](#the-network-changelog-and-patch-notes)); the seed post stays a
draft until a person reviews it.
draft until a person reviews it.
**Domain:** `openvibe.blog` · **Port:** 4810 · **Service id:** `blog`
**Domain:** `openvibe.blog` · **Port:** 4810 · **Service id:** `blog`
@@ -224,7 +224,7 @@ OpenVibe.Network proxies this feed at `openvibe.network/api/v1/changelog`. Every
@@ -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.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. |
| `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
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:
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:
@@ -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.post.schedule`, `blog.post.unpublish`, `blog.post.delete`
- `blog.feed.read`
- `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
Called elsewhere, as the service principal `blog`: `identity.subject.resolve` and
`network.integration.github.read` (Network), `events.event.publish` (Events),
`network.integration.github.read` (Network), `events.event.publish` (Events),
@@ -251,9 +252,9 @@ Called elsewhere, as the service principal `blog`: `identity.subject.resolve` an
@@ -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
- **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** (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`
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-sdk` v0.26.0 (sso, db, PostgreSQL events outbox, service tokens, per-actor limits, testing).
- **OpenVibe.Network:**
- **OpenVibe.Network:**
- SSO: the OAuth client `blog` is already seeded with redirect
- SSO: the OAuth client `blog` is already seeded with redirect
@@ -302,6 +303,7 @@ Each grant is `[client, capability, audience]`:
@@ -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` |
| 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 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` |
| 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` |
| 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
## Launch rule
@@ -315,7 +317,7 @@ existed; the launch release went out on 2026-09-23. Status against each point:
@@ -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.
3. **SSR public routes useful without JS:** done.
4. **Persistence and end-to-end workflows:** done; `ovhost drill blog` restored it on the production
4. **Persistence and end-to-end workflows:** done; `ovhost drill blog` restored it on the production
host on 2026-09-23.
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).
(capabilities and service manifest; no `blog.*` event payload schemas yet).
6. **Migration and seed strategy, threat review, sitemap/robots/feed behaviour:** done. There is
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.
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):
@@ -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`).
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.
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.
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
8. **Launch:** in the same release, remove `openvibe.blog` from OpenVibe.Sites and flip the Network
hub entry (see the launch rule above).
hub entry (see the launch rule above).
@@ -7,10 +7,10 @@
@@ -7,10 +7,10 @@
"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": 4810,
"port": 4810,
"domain": "openvibe.blog",
"domain": "openvibe.blog",
- "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.9.0",
"openvibe-shared v2.9.0",
"openvibe-sdk v0.26.0"
"openvibe-sdk v0.26.0"
],
],
@@ -35,7 +35,7 @@
@@ -35,7 +35,7 @@
"deployed": true,
"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.",
"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,
"placeholderInSites": false,
- "updated": "2026-09-29",
+ "updated": "2026-10-04",
"deployedAt": "2026-09-23",
"deployedAt": "2026-09-23",
"features": [
"features": [
"the official blog and a blog for every member, with owners, editors and authors",
"the official blog and a blog for every member, with owners, editors and authors",
@@ -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.9.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.9.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",
@@ -27,7 +27,7 @@
@@ -27,7 +27,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.9.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.9.0",
@@ -1,14 +1,10 @@
@@ -1,14 +1,10 @@
'use strict';
'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
* 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
* membership (domain/access.js). A service token is judged by its capability AND by the membership
@@ -30,16 +26,10 @@ const CAPABILITIES = Object.freeze({
@@ -30,16 +26,10 @@ const CAPABILITIES = Object.freeze({
POST_DELETE: 'blog.post.delete',
POST_DELETE: 'blog.post.delete',
FEED_READ: 'blog.feed.read',
FEED_READ: 'blog.feed.read',
});
});
-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 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();
+})();
@@ -8,7 +8,7 @@ const fs = require('fs');
@@ -8,7 +8,7 @@ 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 DIR = path.join(__dirname, '..', 'docs', 'capabilities-proposal');
const DIR = path.join(__dirname, '..', 'docs', 'capabilities-proposal');
@@ -29,8 +29,8 @@ 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 () => {
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 () => {
await check('the service manifest proposal is a valid registry.service-manifest@1', async () => {