openvibers/openvibe.host Commits
Host on openvibe-contracts v0.83.0, which releases host.site.config: the local 'proposed capability' path and docs/capabilities-proposal/ plus the service manifest proposal are gone; capabilities are checked by the library alone, and the contracts test now holds the routes, guards and emitted events to the released manifests. npm test 52/52.
Changes
@@@ -301,7 +301,7 @@ People present their Network user JWT as a Bearer token. Services and apps preseErrors are RFC 9457 problems (`application/problem+json`, with the legacy `error` field). A refused upload answers 413/422 with `deploy_id` and the log lines; `503 upload.busy` (with `Retry-After`) when `HOST_MAX_CONCURRENT_UPLOADS` uploads are already being validated; `507 storage.host_full` while the disk has less than `HOST_MIN_FREE_BYTES` free. Network staff (`role: admin`) can read every project, site, deploy and log, delete sites, projects and domains, set quotas, and **take a site or project down** (451 on every host, content kept for review, members see the reason and cannot publish or delete around it); they cannot publish into a tenant's site.--The three capability ids and the service manifest are released in `openvibe-contracts` (since v0.24.0; v0.32.0 adds the takedown routes to `host.site.manage`), and the CI contract check is blocking. [`docs/capabilities-proposal/`](docs/capabilities-proposal/) mirrors them; `host.site.manage` there also lists the four takedown routes, which the next Contracts release should add (see [docs/launch.md](docs/launch.md#the-window-in-this-order)).++The four capability ids (`host.site.config` since v0.83.0) and the service manifest are released in `openvibe-contracts`, which Host pins, and the CI contract check is blocking; `test/host-contracts-events.test.js` holds the routes, guards and emitted events to the released manifests.### Activation and rollbackddeleted file mode 100644@@@ -1,28 +0,0 @@--{-- "id": "host.deploy.create",-- "version": "1.0",-- "owner": "host",-- "status": "active",-- "visibility": "first-party",-- "description": "Upload an immutable static deploy (tar/tar.gz or multipart files; validated for path traversal, links, special and hidden files, server-side code, file types and quotas; never executed), list deploys and their upload logs, activate a deploy and roll a site back to any earlier ready deploy (one atomic pointer switch). Also judged by the acting principal's project role (deployer or above); an app principal added to a project as deployer can deploy from CI.",-- "permissions": [-- "host:deploy"-- ],-- "resourceConstraints": [-- "project"-- ],-- "quotaClass": "host-deploy",-- "events": [-- "host.deploy.created",-- "host.deploy.activated",-- "host.deploy.failed"-- ],-- "implementedBy": [-- "GET /api/v1/sites/:id/deploys",-- "POST /api/v1/sites/:id/deploys",-- "GET /api/v1/deploys/:id",-- "GET /api/v1/deploys/:id/log",-- "POST /api/v1/deploys/:id/activate",-- "POST /api/v1/sites/:id/rollback"-- ]--}ddeleted file mode 100644@@@ -1,24 +0,0 @@--{-- "id": "host.domain.manage",-- "version": "1.0",-- "owner": "host",-- "status": "active",-- "visibility": "first-party",-- "description": "Add a custom domain to a static site (never an OpenVibe domain; not in sandbox projects), read its DNS TXT verification and routing records, trigger a verification check, and remove it. A custom domain is served only after its TXT record is verified; TLS certificates are issued by an operator and never pass through this capability.",-- "permissions": [-- "host:write"-- ],-- "resourceConstraints": [-- "project"-- ],-- "quotaClass": "host-manage",-- "events": [-- "host.domain.verified"-- ],-- "implementedBy": [-- "GET /api/v1/sites/:id/domains",-- "POST /api/v1/sites/:id/domains",-- "POST /api/v1/domains/:id/verify",-- "DELETE /api/v1/domains/:id"-- ]--}ddeleted file mode 100644@@@ -1,21 +0,0 @@--{-- "id": "host.site.config",-- "version": "1.0",-- "owner": "host",-- "status": "active",-- "visibility": "first-party",-- "description": "Read, set and reset a Host site's serving configuration: response headers, local-only redirects and the SPA fallback (extensionless paths serve index.html). A capability holder must also hold the acting principal's project role (maintainer or owner to change, any role to read). Reserved headers (Content-Security-Policy, Strict-Transport-Security, Set-Cookie, X-Forwarded-*, caching and the platform's own) are refused, and a redirect target must be a local path, so a site can never become an open redirect.",-- "permissions": [-- "host:write"-- ],-- "resourceConstraints": [-- "project"-- ],-- "quotaClass": "host-manage",-- "events": [],-- "implementedBy": [-- "GET /api/v1/sites/:id/config",-- "PUT /api/v1/sites/:id/config",-- "DELETE /api/v1/sites/:id/config"-- ]--}ddeleted file mode 100644@@@ -1,35 +0,0 @@--{-- "id": "host.site.manage",-- "version": "1.0",-- "owner": "host",-- "status": "active",-- "visibility": "first-party",-- "description": "Manage OpenVibe.Host tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner role), create and delete sites, delete a deploy that is not active. Also judged by the acting principal's project role; a sandbox token (env=sandbox) is refused on production projects. Quota changes and takedowns (stop serving a site or a whole project while keeping its content for review; lift) additionally need a Network staff user.",-- "permissions": [-- "host:write"-- ],-- "resourceConstraints": [-- "project"-- ],-- "quotaClass": "host-manage",-- "events": [],-- "implementedBy": [-- "GET /api/v1/projects",-- "POST /api/v1/projects",-- "GET /api/v1/projects/:id",-- "DELETE /api/v1/projects/:id",-- "GET /api/v1/projects/:id/quota",-- "PUT /api/v1/projects/:id/quota",-- "PUT /api/v1/projects/:id/members/:principal",-- "DELETE /api/v1/projects/:id/members/:principal",-- "GET /api/v1/projects/:id/sites",-- "POST /api/v1/projects/:id/sites",-- "GET /api/v1/sites/:id",-- "DELETE /api/v1/sites/:id",-- "DELETE /api/v1/deploys/:id",-- "POST /api/v1/projects/:id/takedown",-- "DELETE /api/v1/projects/:id/takedown",-- "POST /api/v1/sites/:id/takedown",-- "DELETE /api/v1/sites/:id/takedown"-- ]--}@@@ -159,7 +159,7 @@ restart `openvibe-network`, poll `/api/ready`).(`"deployed"`, `"placeholderInSites": false`) and the Launch rule note to *launched on <date>*.**Contracts** (done in openvibe-contracts v0.32.0; Host pins v0.33.0): `host.site.manage` lists the--takedown routes from this repository's `docs/capabilities-proposal/host.site.manage.json`:++takedown routes:- `POST /api/v1/projects/:id/takedown`- `DELETE /api/v1/projects/:id/takedown`@@@ -252,7 +252,7 @@ Each launch-rule item, checked against the code at this commit:| 2 | Canonical identity (Network subjects, scoped principals) | Network SSO OAuth client `host` (Network `server/db/database.js:516`); users are `usr_` subjects from Network JWTs verified against the JWKS (`server/auth/viewer.js`); services present client-credentials tokens for audience `openvibe.host` with one capability per route; `X-OV-Subject` for first-party services only; app principals act as themselves; sandbox tokens refused on production. Principal `svc:host` has `events.event.publish` (Network `server/identity/principals.js:100`). `host-quota-auth` tests. Projects are Host's own until Network has projects (ADR-014, documented). | **met** || 3 | SSR/static public routes useful without JS | Every dashboard page is server-rendered; every action is a plain form, including folder upload. Shared chrome with `<noscript>` nav and SSR footer. Legal pages. Tenant sites are static by definition. `host-secrets-dashboard` "signed-out home page … useful without JavaScript". | **met** || 4 | Real persistence and end-to-end workflows | SQLite `host.db` (WAL) plus the content-addressed object store; upload → validate → store → activate → serve → rollback → delete, custom domains through verification and lapse, all over HTTP in the tests (`host-lifecycle`, `host-rollback`, `host-domains`). `host.db` is in `ovhost backup` and the restore drill passed on the host (2026-09-23). Objects are not backed up (accepted for alpha, threat review §6). | **met** (objects backup: follow-up) |--| 5 | Capability and event registration against Contracts | `host.site.manage`, `host.deploy.create` and `host.domain.manage` and the `host` service manifest (with all four `host.*` events in `eventsProduced`) are released in `openvibe-contracts` **v0.24.0**, which Host pins, and are identical in **v0.30.1** (checked against the tag: every field matches `docs/capabilities-proposal/` as of `8575c91`). `npx openvibe-contracts-check --service host` passes, and CI runs it as a blocking step. Missing from the release: the four takedown routes in `host.site.manage.implementedBy` (this change; the exact addition is under *Contracts* above). No `host.*` payload schemas: optional, most services have none. | **met** (Contracts follow-up for takedown routes) |++| 5 | Capability and event registration against Contracts | `host.site.manage`, `host.deploy.create` and `host.domain.manage` and the `host` service manifest (with all four `host.*` events in `eventsProduced`) are released in `openvibe-contracts` **v0.24.0**, which Host pins, and are identical in **v0.30.1** (v0.83.0 adds `host.site.config`). `npx openvibe-contracts-check --service host` passes, and CI runs it as a blocking step. No `host.*` payload schemas: optional, most services have none. | **met** || 6a | Migration/seed strategy | Bootstrap, not migration: Host replaces no legacy data (roadmap §8 register). The schema is created idempotently at every boot (`server/db.js`, `CREATE … IF NOT EXISTS`); `host_takedowns` is additive and appears on the next restart. There is no seed: a new database is an empty, valid Host. The pointer and data survive a restart (`host-rollback`). A future non-additive change needs a migration ledger, which Host does not have yet. | **met** || 6b | Security/threat review | [threat-review.md](threat-review.md): tenant isolation, uploads, custom domains, cookies, the PSL decision, quotas, abuse and takedown, Host's own surface. Seven gaps were fixed in `1e48cb2` and `0ff29d5`: takedowns, the upload concurrency cap, the CA-validation path refusal, the CDN cache cap, the realip templates, the disk floor with the owner project cap, and Clear-Site-Data. | **met** || 6c | Sitemap/robots/feed behaviour | Dashboard host: `/robots.txt` allows `/` and the legal pages and names `/sitemap.xml`; the signed-out front page is `index, follow` with a canonical URL; everything behind sign-in is `noindex` and `private, no-store`. Tenant sites: a deploy's own uploaded `robots.txt`/`sitemap.xml`, else a generated one (the manifest's HTML pages on the host the request came in on, so a verified custom domain gets its own; `host-sitemap`); sandbox projects get `Disallow: /` and `X-Robots-Tag: noindex`. No feed: a hosting control plane publishes no stream of public items, and tenants publish their own feeds. | **met** |ddeleted file mode 100644@@@ -1,33 +0,0 @@--{-- "version": "0.2.0",-- "contractRanges": {-- "openvibe-contracts": ">=0.10.0 <1.0.0"-- },-- "id": "host",-- "name": "OpenVibe.Host",-- "status": "alpha",-- "repository": "OpenVibers/OpenVibe.Host",-- "domains": [-- "openvibe.host"-- ],-- "publicOrigin": "https://openvibe.host",-- "health": "/api/health",-- "ready": "/api/ready",-- "capabilities": [-- "host.site.manage",-- "host.deploy.create",-- "host.domain.manage",-- "host.site.config"-- ],-- "eventsProduced": [-- "host.deploy.created",-- "host.deploy.activated",-- "host.deploy.failed",-- "host.domain.verified"-- ],-- "eventsConsumed": [],-- "namespacesOwned": [-- "host.*"-- ],-- "notes": "Stage A: the ovhost operator CLI (no server). Stage B: the Host API service on port 4910 (unit openvibe-host.service, state /var/lib/openvibe-host-api): tenant projects keyed by project id (prj_, network_project_id once Network has projects, ADR-014), static sites served by Host header at <site>.openvibe.host (a registrable domain separate from every other OpenVibe domain) and on TXT-verified custom domains, immutable content-addressed deploys with rollback, quotas enforced here, upload logs. Consumes OpenVibe.Network (SSO, JWKS, client-credentials) and publishes through OpenVibe.Events (events.event.publish). Stage C (sandboxed user code) is not started."--}@@@ -17,7 +17,7 @@"express": "^4.21.2","express-rate-limit": "^7.5.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.83.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.2.0","pg": "^8.23.0"@@@ -1501,9 +1501,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.83.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.83.0",++ "integrity": "sha512-KMIJfTjpjVfE/cH2VDmzbZL9KnBm9guyFRniic7kdl/dN0RV2IlgExJG13hDOO39Qq4+RfjPcwuhp06qWuD2SQ==","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -33,7 +33,7 @@"express": "^4.21.2","express-rate-limit": "^7.5.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.83.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.2.0","pg": "^8.23.0"@@@ -1,14 +1,7 @@'use strict';/**-- * Capability checks for service tokens (audience openvibe.host), including the ids Host introduces-- * before the contracts library knows them.-- *-- * openvibe-contracts' capabilities.check() answers capability.unknown for an id that is not in its-- * manifests yet. Host'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.host), by openvibe-contracts' manifests:** host.site.manage projects, members, sites, quotas (read), deleting deploys* host.deploy.create upload deploys, list them and their logs, activate, roll back@@@ -26,16 +19,10 @@ const CAPABILITIES = Object.freeze({DOMAIN_MANAGE: 'host.domain.manage',SITE_CONFIG: 'host.site.config',});--const PROPOSED = new Set(Object.values(CAPABILITIES));--/** → { allowed, code, reason } like capabilities.check(). */++/** → { allowed, code, reason } (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 };@@@ -38,9 +38,9 @@ runTests([assert.ok(!/(sk_|ghp_|BEGIN [A-Z ]*PRIVATE KEY|password\s*[:=]\s*\S)/i.test(text), 'no secret values in the example');}),-- test('the service manifest proposal is valid against the contracts schema', () => {++ test('the released service manifest is valid against the contracts schema', () => {const contracts = require('openvibe-contracts');-- const m = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'docs', 'service-manifest-proposal.json'), 'utf8'));++ const m = JSON.parse(fs.readFileSync(require.resolve('openvibe-contracts/manifests/services/host.json'), 'utf8'));contracts.assertValid('registry.service-manifest', m);assert.strictEqual(m.id, 'host');assert.deepStrictEqual(m.capabilities, ['host.site.manage', 'host.deploy.create', 'host.domain.manage', 'host.site.config'], 'Stage A has no API; the Stage B service enforces these four');@@@ -1,8 +1,8 @@'use strict';/**-- * The proposals the lead releases in the next openvibe-contracts version are valid, match what the-- * code enforces and emits, and do not collide with released ids; and the outbox relay really-- * delivers Host's events to OpenVibe.Events with a Network service token.++ * Host's capabilities and service manifest as openvibe-contracts released them match what the code++ * enforces and emits; and the outbox relay really delivers Host's events to OpenVibe.Events with a++ * Network service token.*/const assert = require('assert');const fs = require('fs');@@@ -10,30 +10,24 @@ const path = require('path');const contracts = require('openvibe-contracts');const { boot, check, done } = require('./stageb/boot');const { listen } = require('./stageb/mocks');--const { PROPOSED } = require('../server/auth/capabilities');++const { CAPABILITIES } = require('../server/auth/capabilities');const { EVENT_TYPES } = require('../server/events/outbox');--const DIR = path.join(__dirname, '..', 'docs', 'capabilities-proposal');--(async () => {-- const files = fs.readdirSync(DIR).filter((f) => f.endsWith('.json'));-- const caps = files.map((f) => JSON.parse(fs.readFileSync(path.join(DIR, f), 'utf8')));-- const manifest = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'docs', 'service-manifest-proposal.json'), 'utf8'));++ const ids = Object.values(CAPABILITIES);++ const caps = ids.map((id) => contracts.capabilities.get(id));++ const manifest = JSON.parse(fs.readFileSync(require.resolve('openvibe-contracts/manifests/services/host.json'), 'utf8'));-- await check('every capability proposal is a valid capabilities.capability@1 with 3 segments, owned by host', async () => {-- for (const c of caps) {-- const v = contracts.validate('capabilities.capability@1', c);-- assert.ok(v.valid, `${c.id}: ${JSON.stringify(v.errors)}`);++ await check('every capability Host guards is released, active and owned by host', async () => {++ for (const [i, c] of caps.entries()) {++ assert.ok(c, `${ids[i]} is not in openvibe-contracts`);assert.strictEqual(c.owner, 'host');-- assert.strictEqual(c.id.split('.').length, 3);-- assert.strictEqual(`${c.id}.json`, files[caps.indexOf(c)]);-- assert.ok(!contracts.capabilities.get(c.id) || contracts.capabilities.get(c.id).owner === 'host', `${c.id} collides with a released capability`);++ assert.strictEqual(c.status, 'active', c.id);}});-- await check('the proposals are exactly the capabilities the routes guard, and every route guards one', async () => {-- assert.deepStrictEqual(caps.map((c) => c.id).sort(), [...PROPOSED].sort());-- assert.deepStrictEqual([...manifest.capabilities].sort(), [...PROPOSED].sort());++ await check('the released capabilities are exactly the ones the routes guard, and every route guards one', async () => {++ assert.deepStrictEqual([...manifest.capabilities].sort(), [...ids].sort());const api = fs.readFileSync(path.join(__dirname, '..', 'server', 'http', 'api.js'), 'utf8');const routes = [...api.matchAll(/router\.(get|post|put|delete)\('([^']+)', guard\('([a-z.]+)'\)/g)].map((m) => ({ method: m[1].toUpperCase(), route: `/api/v1${m[2]}`, cap: m[3] }));const all = [...api.matchAll(/router\.(get|post|put|delete)\('/g)].length;@@@ -45,11 +39,9 @@ const DIR = path.join(__dirname, '..', 'docs', 'capabilities-proposal');}});-- await check('the service manifest proposal is a valid registry.service-manifest@1 declaring every emitted event', async () => {-- const v = contracts.validate('registry.service-manifest@1', manifest);-- assert.ok(v.valid, JSON.stringify(v.errors));++ await check('the released service manifest declares every event the service emits', async () => {assert.strictEqual(manifest.id, 'host');-- assert.deepStrictEqual([...manifest.eventsProduced].sort(), [...EVENT_TYPES].sort());++ for (const e of EVENT_TYPES) assert.ok(manifest.eventsProduced.includes(e), e);for (const c of caps) for (const e of c.events) assert.ok(manifest.eventsProduced.includes(e), e);const src = ['server/domain/deploys.js', 'server/domain/domains.js'].map((f) => fs.readFileSync(path.join(__dirname, '..', f), 'utf8')).join('\n');for (const m of src.matchAll(/event_type: '([a-z_.]+)'/g)) assert.ok(manifest.eventsProduced.includes(m[1]), m[1]);
@@ -301,7 +301,7 @@ People present their Network user JWT as a Bearer token. Services and apps prese
@@ -301,7 +301,7 @@ People present their Network user JWT as a Bearer token. Services and apps prese
Errors are RFC 9457 problems (`application/problem+json`, with the legacy `error` field). A refused upload answers 413/422 with `deploy_id` and the log lines; `503 upload.busy` (with `Retry-After`) when `HOST_MAX_CONCURRENT_UPLOADS` uploads are already being validated; `507 storage.host_full` while the disk has less than `HOST_MIN_FREE_BYTES` free. Network staff (`role: admin`) can read every project, site, deploy and log, delete sites, projects and domains, set quotas, and **take a site or project down** (451 on every host, content kept for review, members see the reason and cannot publish or delete around it); they cannot publish into a tenant's site.
Errors are RFC 9457 problems (`application/problem+json`, with the legacy `error` field). A refused upload answers 413/422 with `deploy_id` and the log lines; `503 upload.busy` (with `Retry-After`) when `HOST_MAX_CONCURRENT_UPLOADS` uploads are already being validated; `507 storage.host_full` while the disk has less than `HOST_MIN_FREE_BYTES` free. Network staff (`role: admin`) can read every project, site, deploy and log, delete sites, projects and domains, set quotas, and **take a site or project down** (451 on every host, content kept for review, members see the reason and cannot publish or delete around it); they cannot publish into a tenant's site.
-The three capability ids and the service manifest are released in `openvibe-contracts` (since v0.24.0; v0.32.0 adds the takedown routes to `host.site.manage`), and the CI contract check is blocking. [`docs/capabilities-proposal/`](docs/capabilities-proposal/) mirrors them; `host.site.manage` there also lists the four takedown routes, which the next Contracts release should add (see [docs/launch.md](docs/launch.md#the-window-in-this-order)).
+The four capability ids (`host.site.config` since v0.83.0) and the service manifest are released in `openvibe-contracts`, which Host pins, and the CI contract check is blocking; `test/host-contracts-events.test.js` holds the routes, guards and emitted events to the released manifests.
### Activation and rollback
### Activation and rollback
deleted file mode 100644
deleted file mode 100644
@@ -1,28 +0,0 @@
@@ -1,28 +0,0 @@
-{
- "id": "host.deploy.create",
- "version": "1.0",
- "owner": "host",
- "status": "active",
- "visibility": "first-party",
- "description": "Upload an immutable static deploy (tar/tar.gz or multipart files; validated for path traversal, links, special and hidden files, server-side code, file types and quotas; never executed), list deploys and their upload logs, activate a deploy and roll a site back to any earlier ready deploy (one atomic pointer switch). Also judged by the acting principal's project role (deployer or above); an app principal added to a project as deployer can deploy from CI.",
- "permissions": [
- "host:deploy"
- ],
- "resourceConstraints": [
- "project"
- ],
- "quotaClass": "host-deploy",
- "events": [
- "host.deploy.created",
- "host.deploy.activated",
- "host.deploy.failed"
- ],
- "implementedBy": [
- "GET /api/v1/sites/:id/deploys",
- "POST /api/v1/sites/:id/deploys",
- "GET /api/v1/deploys/:id",
- "GET /api/v1/deploys/:id/log",
- "POST /api/v1/deploys/:id/activate",
- "POST /api/v1/sites/:id/rollback"
- ]
-}
deleted file mode 100644
deleted file mode 100644
@@ -1,24 +0,0 @@
@@ -1,24 +0,0 @@
-{
- "id": "host.domain.manage",
- "version": "1.0",
- "owner": "host",
- "status": "active",
- "visibility": "first-party",
- "description": "Add a custom domain to a static site (never an OpenVibe domain; not in sandbox projects), read its DNS TXT verification and routing records, trigger a verification check, and remove it. A custom domain is served only after its TXT record is verified; TLS certificates are issued by an operator and never pass through this capability.",
- "permissions": [
- "host:write"
- ],
- "resourceConstraints": [
- "project"
- ],
- "quotaClass": "host-manage",
- "events": [
- "host.domain.verified"
- ],
- "implementedBy": [
- "GET /api/v1/sites/:id/domains",
- "POST /api/v1/sites/:id/domains",
- "POST /api/v1/domains/:id/verify",
- "DELETE /api/v1/domains/:id"
- ]
-}
deleted file mode 100644
deleted file mode 100644
@@ -1,21 +0,0 @@
@@ -1,21 +0,0 @@
-{
- "id": "host.site.config",
- "version": "1.0",
- "owner": "host",
- "status": "active",
- "visibility": "first-party",
- "description": "Read, set and reset a Host site's serving configuration: response headers, local-only redirects and the SPA fallback (extensionless paths serve index.html). A capability holder must also hold the acting principal's project role (maintainer or owner to change, any role to read). Reserved headers (Content-Security-Policy, Strict-Transport-Security, Set-Cookie, X-Forwarded-*, caching and the platform's own) are refused, and a redirect target must be a local path, so a site can never become an open redirect.",
- "permissions": [
- "host:write"
- ],
- "resourceConstraints": [
- "project"
- ],
- "quotaClass": "host-manage",
- "events": [],
- "implementedBy": [
- "GET /api/v1/sites/:id/config",
- "PUT /api/v1/sites/:id/config",
- "DELETE /api/v1/sites/:id/config"
- ]
-}
deleted file mode 100644
deleted file mode 100644
@@ -1,35 +0,0 @@
@@ -1,35 +0,0 @@
-{
- "id": "host.site.manage",
- "version": "1.0",
- "owner": "host",
- "status": "active",
- "visibility": "first-party",
- "description": "Manage OpenVibe.Host tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner role), create and delete sites, delete a deploy that is not active. Also judged by the acting principal's project role; a sandbox token (env=sandbox) is refused on production projects. Quota changes and takedowns (stop serving a site or a whole project while keeping its content for review; lift) additionally need a Network staff user.",
- "permissions": [
- "host:write"
- ],
- "resourceConstraints": [
- "project"
- ],
- "quotaClass": "host-manage",
- "events": [],
- "implementedBy": [
- "GET /api/v1/projects",
- "POST /api/v1/projects",
- "GET /api/v1/projects/:id",
- "DELETE /api/v1/projects/:id",
- "GET /api/v1/projects/:id/quota",
- "PUT /api/v1/projects/:id/quota",
- "PUT /api/v1/projects/:id/members/:principal",
- "DELETE /api/v1/projects/:id/members/:principal",
- "GET /api/v1/projects/:id/sites",
- "POST /api/v1/projects/:id/sites",
- "GET /api/v1/sites/:id",
- "DELETE /api/v1/sites/:id",
- "DELETE /api/v1/deploys/:id",
- "POST /api/v1/projects/:id/takedown",
- "DELETE /api/v1/projects/:id/takedown",
- "POST /api/v1/sites/:id/takedown",
- "DELETE /api/v1/sites/:id/takedown"
- ]
-}
@@ -159,7 +159,7 @@ restart `openvibe-network`, poll `/api/ready`).
@@ -159,7 +159,7 @@ restart `openvibe-network`, poll `/api/ready`).
(`"deployed"`, `"placeholderInSites": false`) and the Launch rule note to *launched on <date>*.
(`"deployed"`, `"placeholderInSites": false`) and the Launch rule note to *launched on <date>*.
**Contracts** (done in openvibe-contracts v0.32.0; Host pins v0.33.0): `host.site.manage` lists the
**Contracts** (done in openvibe-contracts v0.32.0; Host pins v0.33.0): `host.site.manage` lists the
-takedown routes from this repository's `docs/capabilities-proposal/host.site.manage.json`:
+takedown routes:
- `POST /api/v1/projects/:id/takedown`
- `POST /api/v1/projects/:id/takedown`
- `DELETE /api/v1/projects/:id/takedown`
- `DELETE /api/v1/projects/:id/takedown`
@@ -252,7 +252,7 @@ Each launch-rule item, checked against the code at this commit:
@@ -252,7 +252,7 @@ Each launch-rule item, checked against the code at this commit:
| 2 | Canonical identity (Network subjects, scoped principals) | Network SSO OAuth client `host` (Network `server/db/database.js:516`); users are `usr_` subjects from Network JWTs verified against the JWKS (`server/auth/viewer.js`); services present client-credentials tokens for audience `openvibe.host` with one capability per route; `X-OV-Subject` for first-party services only; app principals act as themselves; sandbox tokens refused on production. Principal `svc:host` has `events.event.publish` (Network `server/identity/principals.js:100`). `host-quota-auth` tests. Projects are Host's own until Network has projects (ADR-014, documented). | **met** |
| 2 | Canonical identity (Network subjects, scoped principals) | Network SSO OAuth client `host` (Network `server/db/database.js:516`); users are `usr_` subjects from Network JWTs verified against the JWKS (`server/auth/viewer.js`); services present client-credentials tokens for audience `openvibe.host` with one capability per route; `X-OV-Subject` for first-party services only; app principals act as themselves; sandbox tokens refused on production. Principal `svc:host` has `events.event.publish` (Network `server/identity/principals.js:100`). `host-quota-auth` tests. Projects are Host's own until Network has projects (ADR-014, documented). | **met** |
| 3 | SSR/static public routes useful without JS | Every dashboard page is server-rendered; every action is a plain form, including folder upload. Shared chrome with `<noscript>` nav and SSR footer. Legal pages. Tenant sites are static by definition. `host-secrets-dashboard` "signed-out home page … useful without JavaScript". | **met** |
| 3 | SSR/static public routes useful without JS | Every dashboard page is server-rendered; every action is a plain form, including folder upload. Shared chrome with `<noscript>` nav and SSR footer. Legal pages. Tenant sites are static by definition. `host-secrets-dashboard` "signed-out home page … useful without JavaScript". | **met** |
| 4 | Real persistence and end-to-end workflows | SQLite `host.db` (WAL) plus the content-addressed object store; upload → validate → store → activate → serve → rollback → delete, custom domains through verification and lapse, all over HTTP in the tests (`host-lifecycle`, `host-rollback`, `host-domains`). `host.db` is in `ovhost backup` and the restore drill passed on the host (2026-09-23). Objects are not backed up (accepted for alpha, threat review §6). | **met** (objects backup: follow-up) |
| 4 | Real persistence and end-to-end workflows | SQLite `host.db` (WAL) plus the content-addressed object store; upload → validate → store → activate → serve → rollback → delete, custom domains through verification and lapse, all over HTTP in the tests (`host-lifecycle`, `host-rollback`, `host-domains`). `host.db` is in `ovhost backup` and the restore drill passed on the host (2026-09-23). Objects are not backed up (accepted for alpha, threat review §6). | **met** (objects backup: follow-up) |
-| 5 | Capability and event registration against Contracts | `host.site.manage`, `host.deploy.create` and `host.domain.manage` and the `host` service manifest (with all four `host.*` events in `eventsProduced`) are released in `openvibe-contracts` **v0.24.0**, which Host pins, and are identical in **v0.30.1** (checked against the tag: every field matches `docs/capabilities-proposal/` as of `8575c91`). `npx openvibe-contracts-check --service host` passes, and CI runs it as a blocking step. Missing from the release: the four takedown routes in `host.site.manage.implementedBy` (this change; the exact addition is under *Contracts* above). No `host.*` payload schemas: optional, most services have none. | **met** (Contracts follow-up for takedown routes) |
+| 5 | Capability and event registration against Contracts | `host.site.manage`, `host.deploy.create` and `host.domain.manage` and the `host` service manifest (with all four `host.*` events in `eventsProduced`) are released in `openvibe-contracts` **v0.24.0**, which Host pins, and are identical in **v0.30.1** (v0.83.0 adds `host.site.config`). `npx openvibe-contracts-check --service host` passes, and CI runs it as a blocking step. No `host.*` payload schemas: optional, most services have none. | **met** |
| 6a | Migration/seed strategy | Bootstrap, not migration: Host replaces no legacy data (roadmap §8 register). The schema is created idempotently at every boot (`server/db.js`, `CREATE … IF NOT EXISTS`); `host_takedowns` is additive and appears on the next restart. There is no seed: a new database is an empty, valid Host. The pointer and data survive a restart (`host-rollback`). A future non-additive change needs a migration ledger, which Host does not have yet. | **met** |
| 6a | Migration/seed strategy | Bootstrap, not migration: Host replaces no legacy data (roadmap §8 register). The schema is created idempotently at every boot (`server/db.js`, `CREATE … IF NOT EXISTS`); `host_takedowns` is additive and appears on the next restart. There is no seed: a new database is an empty, valid Host. The pointer and data survive a restart (`host-rollback`). A future non-additive change needs a migration ledger, which Host does not have yet. | **met** |
| 6b | Security/threat review | [threat-review.md](threat-review.md): tenant isolation, uploads, custom domains, cookies, the PSL decision, quotas, abuse and takedown, Host's own surface. Seven gaps were fixed in `1e48cb2` and `0ff29d5`: takedowns, the upload concurrency cap, the CA-validation path refusal, the CDN cache cap, the realip templates, the disk floor with the owner project cap, and Clear-Site-Data. | **met** |
| 6b | Security/threat review | [threat-review.md](threat-review.md): tenant isolation, uploads, custom domains, cookies, the PSL decision, quotas, abuse and takedown, Host's own surface. Seven gaps were fixed in `1e48cb2` and `0ff29d5`: takedowns, the upload concurrency cap, the CA-validation path refusal, the CDN cache cap, the realip templates, the disk floor with the owner project cap, and Clear-Site-Data. | **met** |
| 6c | Sitemap/robots/feed behaviour | Dashboard host: `/robots.txt` allows `/` and the legal pages and names `/sitemap.xml`; the signed-out front page is `index, follow` with a canonical URL; everything behind sign-in is `noindex` and `private, no-store`. Tenant sites: a deploy's own uploaded `robots.txt`/`sitemap.xml`, else a generated one (the manifest's HTML pages on the host the request came in on, so a verified custom domain gets its own; `host-sitemap`); sandbox projects get `Disallow: /` and `X-Robots-Tag: noindex`. No feed: a hosting control plane publishes no stream of public items, and tenants publish their own feeds. | **met** |
| 6c | Sitemap/robots/feed behaviour | Dashboard host: `/robots.txt` allows `/` and the legal pages and names `/sitemap.xml`; the signed-out front page is `index, follow` with a canonical URL; everything behind sign-in is `noindex` and `private, no-store`. Tenant sites: a deploy's own uploaded `robots.txt`/`sitemap.xml`, else a generated one (the manifest's HTML pages on the host the request came in on, so a verified custom domain gets its own; `host-sitemap`); sandbox projects get `Disallow: /` and `X-Robots-Tag: noindex`. No feed: a hosting control plane publishes no stream of public items, and tenants publish their own feeds. | **met** |
deleted file mode 100644
deleted file mode 100644
@@ -1,33 +0,0 @@
@@ -1,33 +0,0 @@
-{
- "version": "0.2.0",
- "contractRanges": {
- "openvibe-contracts": ">=0.10.0 <1.0.0"
- },
- "id": "host",
- "name": "OpenVibe.Host",
- "status": "alpha",
- "repository": "OpenVibers/OpenVibe.Host",
- "domains": [
- "openvibe.host"
- ],
- "publicOrigin": "https://openvibe.host",
- "health": "/api/health",
- "ready": "/api/ready",
- "capabilities": [
- "host.site.manage",
- "host.deploy.create",
- "host.domain.manage",
- "host.site.config"
- ],
- "eventsProduced": [
- "host.deploy.created",
- "host.deploy.activated",
- "host.deploy.failed",
- "host.domain.verified"
- ],
- "eventsConsumed": [],
- "namespacesOwned": [
- "host.*"
- ],
- "notes": "Stage A: the ovhost operator CLI (no server). Stage B: the Host API service on port 4910 (unit openvibe-host.service, state /var/lib/openvibe-host-api): tenant projects keyed by project id (prj_, network_project_id once Network has projects, ADR-014), static sites served by Host header at <site>.openvibe.host (a registrable domain separate from every other OpenVibe domain) and on TXT-verified custom domains, immutable content-addressed deploys with rollback, quotas enforced here, upload logs. Consumes OpenVibe.Network (SSO, JWKS, client-credentials) and publishes through OpenVibe.Events (events.event.publish). Stage C (sandboxed user code) is not started."
-}
@@ -17,7 +17,7 @@
@@ -17,7 +17,7 @@
"express": "^4.21.2",
"express": "^4.21.2",
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.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.83.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.2.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.2.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
@@ -1501,9 +1501,9 @@
@@ -1501,9 +1501,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.83.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.83.0",
+ "integrity": "sha512-KMIJfTjpjVfE/cH2VDmzbZL9KnBm9guyFRniic7kdl/dN0RV2IlgExJG13hDOO39Qq4+RfjPcwuhp06qWuD2SQ==",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -33,7 +33,7 @@
@@ -33,7 +33,7 @@
"express": "^4.21.2",
"express": "^4.21.2",
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.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.83.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.2.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.2.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
@@ -1,14 +1,7 @@
@@ -1,14 +1,7 @@
'use strict';
'use strict';
/**
/**
- * Capability checks for service tokens (audience openvibe.host), including the ids Host introduces
- * before the contracts library knows them.
- *
- * openvibe-contracts' capabilities.check() answers capability.unknown for an id that is not in its
- * manifests yet. Host'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.host), by openvibe-contracts' manifests:
*
*
* host.site.manage projects, members, sites, quotas (read), deleting deploys
* host.site.manage projects, members, sites, quotas (read), deleting deploys
* host.deploy.create upload deploys, list them and their logs, activate, roll back
* host.deploy.create upload deploys, list them and their logs, activate, roll back
@@ -26,16 +19,10 @@ const CAPABILITIES = Object.freeze({
@@ -26,16 +19,10 @@ const CAPABILITIES = Object.freeze({
DOMAIN_MANAGE: 'host.domain.manage',
DOMAIN_MANAGE: 'host.domain.manage',
SITE_CONFIG: 'host.site.config',
SITE_CONFIG: 'host.site.config',
});
});
-const PROPOSED = new Set(Object.values(CAPABILITIES));
-/** → { allowed, code, reason } like capabilities.check(). */
+/** → { allowed, code, reason } (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 };
@@ -38,9 +38,9 @@ runTests([
@@ -38,9 +38,9 @@ runTests([
assert.ok(!/(sk_|ghp_|BEGIN [A-Z ]*PRIVATE KEY|password\s*[:=]\s*\S)/i.test(text), 'no secret values in the example');
assert.ok(!/(sk_|ghp_|BEGIN [A-Z ]*PRIVATE KEY|password\s*[:=]\s*\S)/i.test(text), 'no secret values in the example');
}),
}),
- test('the service manifest proposal is valid against the contracts schema', () => {
+ test('the released service manifest is valid against the contracts schema', () => {
const contracts = require('openvibe-contracts');
const contracts = require('openvibe-contracts');
- const m = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'docs', 'service-manifest-proposal.json'), 'utf8'));
+ const m = JSON.parse(fs.readFileSync(require.resolve('openvibe-contracts/manifests/services/host.json'), 'utf8'));
contracts.assertValid('registry.service-manifest', m);
contracts.assertValid('registry.service-manifest', m);
assert.strictEqual(m.id, 'host');
assert.strictEqual(m.id, 'host');
assert.deepStrictEqual(m.capabilities, ['host.site.manage', 'host.deploy.create', 'host.domain.manage', 'host.site.config'], 'Stage A has no API; the Stage B service enforces these four');
assert.deepStrictEqual(m.capabilities, ['host.site.manage', 'host.deploy.create', 'host.domain.manage', 'host.site.config'], 'Stage A has no API; the Stage B service enforces these four');
@@ -1,8 +1,8 @@
@@ -1,8 +1,8 @@
'use strict';
'use strict';
/**
/**
- * The proposals the lead releases in the next openvibe-contracts version are valid, match what the
- * code enforces and emits, and do not collide with released ids; and the outbox relay really
- * delivers Host's events to OpenVibe.Events with a Network service token.
+ * Host's capabilities and service manifest as openvibe-contracts released them match what the code
+ * enforces and emits; and the outbox relay really delivers Host's events to OpenVibe.Events with a
+ * Network service token.
*/
*/
const assert = require('assert');
const assert = require('assert');
const fs = require('fs');
const fs = require('fs');
@@ -10,30 +10,24 @@ const path = require('path');
@@ -10,30 +10,24 @@ const path = require('path');
const contracts = require('openvibe-contracts');
const contracts = require('openvibe-contracts');
const { boot, check, done } = require('./stageb/boot');
const { boot, check, done } = require('./stageb/boot');
const { listen } = require('./stageb/mocks');
const { listen } = require('./stageb/mocks');
-const { PROPOSED } = require('../server/auth/capabilities');
+const { CAPABILITIES } = require('../server/auth/capabilities');
const { EVENT_TYPES } = require('../server/events/outbox');
const { EVENT_TYPES } = require('../server/events/outbox');
-const DIR = path.join(__dirname, '..', 'docs', 'capabilities-proposal');
-
(async () => {
(async () => {
- const files = fs.readdirSync(DIR).filter((f) => f.endsWith('.json'));
- const caps = files.map((f) => JSON.parse(fs.readFileSync(path.join(DIR, f), 'utf8')));
- const manifest = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'docs', 'service-manifest-proposal.json'), 'utf8'));
+ const ids = Object.values(CAPABILITIES);
+ const caps = ids.map((id) => contracts.capabilities.get(id));
+ const manifest = JSON.parse(fs.readFileSync(require.resolve('openvibe-contracts/manifests/services/host.json'), 'utf8'));
- await check('every capability proposal is a valid capabilities.capability@1 with 3 segments, owned by host', async () => {
- for (const c of caps) {
- const v = contracts.validate('capabilities.capability@1', c);
- assert.ok(v.valid, `${c.id}: ${JSON.stringify(v.errors)}`);
+ await check('every capability Host guards is released, active and owned by host', async () => {
+ for (const [i, c] of caps.entries()) {
+ assert.ok(c, `${ids[i]} is not in openvibe-contracts`);
assert.strictEqual(c.owner, 'host');
assert.strictEqual(c.owner, 'host');
- assert.strictEqual(c.id.split('.').length, 3);
- assert.strictEqual(`${c.id}.json`, files[caps.indexOf(c)]);
- assert.ok(!contracts.capabilities.get(c.id) || contracts.capabilities.get(c.id).owner === 'host', `${c.id} collides with a released capability`);
+ assert.strictEqual(c.status, 'active', c.id);
}
}
});
});
- await check('the proposals are exactly the capabilities the routes guard, and every route guards one', async () => {
- assert.deepStrictEqual(caps.map((c) => c.id).sort(), [...PROPOSED].sort());
- assert.deepStrictEqual([...manifest.capabilities].sort(), [...PROPOSED].sort());
+ await check('the released capabilities are exactly the ones the routes guard, and every route guards one', async () => {
+ assert.deepStrictEqual([...manifest.capabilities].sort(), [...ids].sort());
const api = fs.readFileSync(path.join(__dirname, '..', 'server', 'http', 'api.js'), 'utf8');
const api = fs.readFileSync(path.join(__dirname, '..', 'server', 'http', 'api.js'), 'utf8');
const routes = [...api.matchAll(/router\.(get|post|put|delete)\('([^']+)', guard\('([a-z.]+)'\)/g)].map((m) => ({ method: m[1].toUpperCase(), route: `/api/v1${m[2]}`, cap: m[3] }));
const routes = [...api.matchAll(/router\.(get|post|put|delete)\('([^']+)', guard\('([a-z.]+)'\)/g)].map((m) => ({ method: m[1].toUpperCase(), route: `/api/v1${m[2]}`, cap: m[3] }));
const all = [...api.matchAll(/router\.(get|post|put|delete)\('/g)].length;
const all = [...api.matchAll(/router\.(get|post|put|delete)\('/g)].length;
@@ -45,11 +39,9 @@ const DIR = path.join(__dirname, '..', 'docs', 'capabilities-proposal');
@@ -45,11 +39,9 @@ const DIR = path.join(__dirname, '..', 'docs', 'capabilities-proposal');
}
}
});
});
- await check('the service manifest proposal is a valid registry.service-manifest@1 declaring every emitted event', async () => {
- const v = contracts.validate('registry.service-manifest@1', manifest);
- assert.ok(v.valid, JSON.stringify(v.errors));
+ await check('the released service manifest declares every event the service emits', async () => {
assert.strictEqual(manifest.id, 'host');
assert.strictEqual(manifest.id, 'host');
- assert.deepStrictEqual([...manifest.eventsProduced].sort(), [...EVENT_TYPES].sort());
+ for (const e of EVENT_TYPES) assert.ok(manifest.eventsProduced.includes(e), e);
for (const c of caps) for (const e of c.events) assert.ok(manifest.eventsProduced.includes(e), e);
for (const c of caps) for (const e of c.events) assert.ok(manifest.eventsProduced.includes(e), e);
const src = ['server/domain/deploys.js', 'server/domain/domains.js'].map((f) => fs.readFileSync(path.join(__dirname, '..', f), 'utf8')).join('\n');
const src = ['server/domain/deploys.js', 'server/domain/domains.js'].map((f) => fs.readFileSync(path.join(__dirname, '..', f), 'utf8')).join('\n');
for (const m of src.matchAll(/event_type: '([a-z_.]+)'/g)) assert.ok(manifest.eventsProduced.includes(m[1]), m[1]);
for (const m of src.matchAll(/event_type: '([a-z_.]+)'/g)) assert.ok(manifest.eventsProduced.includes(m[1]), m[1]);