openvibers/openvibe.contracts Commits
v0.34.0: mods.mod-manifest@1 1.0.0 -> 1.1.0 (roadmap §28.3 m6, ADR-013 amendment): optional permissions.readGrants and writeGrants split module and Media namespace access into read-only and read-write ({ modules?, mediaNamespaces? }, same patterns as the older lists, which still mean read and write); optional billingHooks [{ kind entitlement|checkout, key, description? }] declare the entitlement checks and platform checkouts a mod uses, with keys in the entitlement-key form Search and VIP use and never a price; optional dependencies [{ id mod_…, version range, optional? }]. Every 1.0.0 manifest stays valid. Fixtures: a manifest with every new field; invalid empty grants, capabilities inside writeGrants, a price hook kind, a hook carrying an amount, a dependency that is not a mod id or has no range. test/run.js checks the fields are optional, the split uses the same namespace patterns, hook keys match acl.entitlements and ADR-013 names signing and review as open owner decisions
Changes
@@@ -168,7 +168,7 @@},{"id": "mods.mod-manifest",-- "version": "1.0.0",++ "version": "1.1.0","owner": "contracts","schema": "mods/mod-manifest.v1.json","visibility": "public",@@@ -2,7 +2,7 @@"$schema": "https://json-schema.org/draft/2020-12/schema","$id": "https://openvibe.network/contracts/mods/mod-manifest.v1.json","title": "ModManifest",-- "description": "A mod as the platform knows it (ADR-013): who publishes it, which runtime runs it, which capabilities it asks for and the resources it may use. Requested capabilities are only a request: the install's approved subset is the grant, and trust tiers are install metadata that never change a grant check. The runtime-specific payload (a data pack, a script bundle) is not part of the manifest.",++ "description": "A mod as the platform knows it (ADR-013): who publishes it, which runtime runs it, which capabilities it asks for and the resources it may use. Requested capabilities are only a request: the install's approved subset is the grant, and trust tiers are install metadata that never change a grant check. The runtime-specific payload (a data pack, a script bundle) is not part of the manifest. 1.1.0 (openvibe-contracts v0.34.0) adds the optional permissions.readGrants and writeGrants, billingHooks and dependencies (ADR-013 amendment of 2026-09-24); every 1.0.0 manifest stays valid.","type": "object","required": ["id",@@@ -63,18 +63,62 @@"items": { "type": "string", "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$" }},"modules": {-- "description": "User-module namespaces the mod wants to read or write (a trailing .* names a family).",++ "description": "User-module namespaces the mod wants to read or write (a trailing .* names a family). A namespace listed here may be read and written; readGrants and writeGrants (1.1.0) say which.","type": "array","maxItems": 32,"uniqueItems": true,"items": { "type": "string", "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)*(\\.\\*)?$" }},"mediaNamespaces": {-- "description": "Media namespaces the mod wants to read or write.",++ "description": "Media namespaces the mod wants to read or write. A namespace listed here may be read and written; readGrants and writeGrants (1.1.0) say which.","type": "array","maxItems": 32,"uniqueItems": true,"items": { "type": "string", "pattern": "^[a-z][a-z0-9_-]*(\\.[a-z0-9_-]+)*$" }++ },++ "readGrants": {++ "description": "1.1.0: namespaces the mod may only read. A namespace that is only here is read-only. The install's approved subset is still the grant.",++ "type": "object",++ "additionalProperties": false,++ "minProperties": 1,++ "properties": {++ "modules": {++ "description": "User-module namespaces (a trailing .* names a family).",++ "type": "array",++ "maxItems": 32,++ "uniqueItems": true,++ "items": { "type": "string", "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)*(\\.\\*)?$" }++ },++ "mediaNamespaces": {++ "description": "Media namespaces.",++ "type": "array",++ "maxItems": 32,++ "uniqueItems": true,++ "items": { "type": "string", "pattern": "^[a-z][a-z0-9_-]*(\\.[a-z0-9_-]+)*$" }++ }++ }++ },++ "writeGrants": {++ "description": "1.1.0: namespaces the mod may read and write. A write needs the namespace here, or in the older modules and mediaNamespaces lists, which mean read and write.",++ "type": "object",++ "additionalProperties": false,++ "minProperties": 1,++ "properties": {++ "modules": {++ "description": "User-module namespaces (a trailing .* names a family).",++ "type": "array",++ "maxItems": 32,++ "uniqueItems": true,++ "items": { "type": "string", "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)*(\\.\\*)?$" }++ },++ "mediaNamespaces": {++ "description": "Media namespaces.",++ "type": "array",++ "maxItems": 32,++ "uniqueItems": true,++ "items": { "type": "string", "pattern": "^[a-z][a-z0-9_-]*(\\.[a-z0-9_-]+)*$" }++ }++ }}}},@@@ -129,6 +173,46 @@}}},-- "homepage": { "type": "string", "pattern": "^https://[^\\s]{1,2000}$" }++ "homepage": { "type": "string", "pattern": "^https://[^\\s]{1,2000}$" },++ "billingHooks": {++ "description": "1.1.0: how the mod takes part in money, only through Billing and VIP primitives (ADR-013 amendment, ADR-012). entitlement: the mod checks that the person holds this entitlement (billing.entitlement.check or vip.entitlement.check, granted like any other capability). checkout: the mod sends the person to the platform's own checkout for this plan (vip.membership.checkout or a Billing intent). A mod never takes payment, sees payment details or holds a balance. Prices live in Billing and VIP, never in the manifest.",++ "type": "array",++ "maxItems": 32,++ "items": {++ "type": "object",++ "required": ["kind", "key"],++ "additionalProperties": false,++ "properties": {++ "kind": { "enum": ["entitlement", "checkout"] },++ "key": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_.:-]{0,127}$",++ "description": "The entitlement or plan key as Billing and VIP name it, e.g. vip.plan:pln_01jab… (lowercase, the form search.index-document@1 acl.entitlements uses)."++ },++ "description": { "type": "string", "minLength": 1, "maxLength": 200, "description": "What it unlocks or sells, shown at install." }++ }++ }++ },++ "dependencies": {++ "description": "1.1.0: other mods this release needs. A runtime refuses to enable the mod while a required dependency is not installed, not enabled or outside its range. An optional one is used only when present.",++ "type": "array",++ "maxItems": 32,++ "uniqueItems": true,++ "items": {++ "type": "object",++ "required": ["id", "version"],++ "additionalProperties": false,++ "properties": {++ "id": { "type": "string", "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$" },++ "version": {++ "type": "string",++ "minLength": 1,++ "maxLength": 100,++ "description": "Semver range of the dependency's releases this release works with, e.g. \">=1.2.0 <2.0.0\"."++ },++ "optional": { "type": "boolean", "default": false }++ }++ }++ }}}nnew file mode 100644@@@ -0,0 +1 @@++{ "id": "mod_01JABCDEFGHJKMNPQRSTVWXYZ1", "name": "X", "version": "1.0.0", "publisher": { "type": "user", "id": "usr_01JABCDEFGHJKMNPQRSTVWXYZ0" }, "target": "games.browser", "runtime": "games-content@1", "resources": { "cpuMs": 1, "memoryMb": 0, "storageMb": 0 }, "compatibility": { "runtime": ">=1.0.0" }, "permissions": { "capabilities": [], "writeGrants": { "capabilities": ["games.prop.place"] } } }nnew file mode 100644@@@ -0,0 +1 @@++{ "id": "mod_01JABCDEFGHJKMNPQRSTVWXYZ1", "name": "X", "version": "1.0.0", "publisher": { "type": "user", "id": "usr_01JABCDEFGHJKMNPQRSTVWXYZ0" }, "target": "games.browser", "runtime": "games-content@1", "resources": { "cpuMs": 1, "memoryMb": 0, "storageMb": 0 }, "compatibility": { "runtime": ">=1.0.0" }, "permissions": { "capabilities": [] }, "dependencies": [{ "id": "town-square", "version": ">=1.0.0" }] }nnew file mode 100644@@@ -0,0 +1 @@++{ "id": "mod_01JABCDEFGHJKMNPQRSTVWXYZ1", "name": "X", "version": "1.0.0", "publisher": { "type": "user", "id": "usr_01JABCDEFGHJKMNPQRSTVWXYZ0" }, "target": "games.browser", "runtime": "games-content@1", "resources": { "cpuMs": 1, "memoryMb": 0, "storageMb": 0 }, "compatibility": { "runtime": ">=1.0.0" }, "permissions": { "capabilities": [] }, "dependencies": [{ "id": "mod_01JABCDEFGHJKMNPQRSTVWXYZ0" }] }nnew file mode 100644@@@ -0,0 +1 @@++{ "id": "mod_01JABCDEFGHJKMNPQRSTVWXYZ1", "name": "X", "version": "1.0.0", "publisher": { "type": "user", "id": "usr_01JABCDEFGHJKMNPQRSTVWXYZ0" }, "target": "games.browser", "runtime": "games-content@1", "resources": { "cpuMs": 1, "memoryMb": 0, "storageMb": 0 }, "compatibility": { "runtime": ">=1.0.0" }, "permissions": { "capabilities": [], "readGrants": {} } }nnew file mode 100644@@@ -0,0 +1 @@++{ "id": "mod_01JABCDEFGHJKMNPQRSTVWXYZ1", "name": "X", "version": "1.0.0", "publisher": { "type": "user", "id": "usr_01JABCDEFGHJKMNPQRSTVWXYZ0" }, "target": "games.browser", "runtime": "games-content@1", "resources": { "cpuMs": 1, "memoryMb": 0, "storageMb": 0 }, "compatibility": { "runtime": ">=1.0.0" }, "permissions": { "capabilities": [] }, "billingHooks": [{ "kind": "price", "key": "vip.plan:pln_01jabcdefghjkmnpqrstvwxyz2" }] }nnew file mode 100644@@@ -0,0 +1 @@++{ "id": "mod_01JABCDEFGHJKMNPQRSTVWXYZ1", "name": "X", "version": "1.0.0", "publisher": { "type": "user", "id": "usr_01JABCDEFGHJKMNPQRSTVWXYZ0" }, "target": "games.browser", "runtime": "games-content@1", "resources": { "cpuMs": 1, "memoryMb": 0, "storageMb": 0 }, "compatibility": { "runtime": ">=1.0.0" }, "permissions": { "capabilities": [] }, "billingHooks": [{ "kind": "checkout", "key": "vip.plan:pln_01jabcdefghjkmnpqrstvwxyz2", "amount_cents": 499 }] }nnew file mode 100644@@@ -0,0 +1,24 @@++{++ "id": "mod_01JABCDEFGHJKMNPQRSTVWXYZ1",++ "name": "Market Stall",++ "version": "1.2.0",++ "description": "A members-only stall that reads the player's profile summary and keeps its own notes.",++ "publisher": { "type": "user", "id": "usr_01JABCDEFGHJKMNPQRSTVWXYZ0" },++ "target": "games.browser",++ "runtime": "games-content@1",++ "permissions": {++ "capabilities": ["games.prop.place", "vip.entitlement.check"],++ "readGrants": { "modules": ["games.progress.summary"], "mediaNamespaces": ["games.maps"] },++ "writeGrants": { "modules": ["games.market_stall.*"] }++ },++ "resources": { "cpuMs": 2, "memoryMb": 16, "storageMb": 5 },++ "compatibility": { "runtime": ">=1.0.0 <2.0.0", "contracts": ">=0.34.0 <1.0.0" },++ "billingHooks": [++ { "kind": "entitlement", "key": "vip.plan:pln_01jabcdefghjkmnpqrstvwxyz2", "description": "Opens the stall for the creator's VIP members." },++ { "kind": "checkout", "key": "vip.plan:pln_01jabcdefghjkmnpqrstvwxyz2" }++ ],++ "dependencies": [++ { "id": "mod_01JABCDEFGHJKMNPQRSTVWXYZ0", "version": ">=1.0.0 <2.0.0" },++ { "id": "mod_01JABCDEFGHJKMNPQRSTVWXYZ3", "version": "^0.3.0", "optional": true }++ ]++}@@@ -2188,14 +2188,14 @@}},"mods.mod-manifest": {-- "version": "1.0.0",++ "version": "1.1.0","owner": "contracts","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema","$id": "https://openvibe.network/contracts/mods/mod-manifest.v1.json","title": "ModManifest",-- "description": "A mod as the platform knows it (ADR-013): who publishes it, which runtime runs it, which capabilities it asks for and the resources it may use. Requested capabilities are only a request: the install's approved subset is the grant, and trust tiers are install metadata that never change a grant check. The runtime-specific payload (a data pack, a script bundle) is not part of the manifest.",++ "description": "A mod as the platform knows it (ADR-013): who publishes it, which runtime runs it, which capabilities it asks for and the resources it may use. Requested capabilities are only a request: the install's approved subset is the grant, and trust tiers are install metadata that never change a grant check. The runtime-specific payload (a data pack, a script bundle) is not part of the manifest. 1.1.0 (openvibe-contracts v0.34.0) adds the optional permissions.readGrants and writeGrants, billingHooks and dependencies (ADR-013 amendment of 2026-09-24); every 1.0.0 manifest stays valid.","type": "object","required": ["id",@@@ -2271,7 +2271,7 @@}},"modules": {-- "description": "User-module namespaces the mod wants to read or write (a trailing .* names a family).",++ "description": "User-module namespaces the mod wants to read or write (a trailing .* names a family). A namespace listed here may be read and written; readGrants and writeGrants (1.1.0) say which.","type": "array","maxItems": 32,"uniqueItems": true,@@@ -2281,7 +2281,7 @@}},"mediaNamespaces": {-- "description": "Media namespaces the mod wants to read or write.",++ "description": "Media namespaces the mod wants to read or write. A namespace listed here may be read and written; readGrants and writeGrants (1.1.0) say which.","type": "array","maxItems": 32,"uniqueItems": true,@@@ -2289,6 +2289,62 @@"type": "string","pattern": "^[a-z][a-z0-9_-]*(\\.[a-z0-9_-]+)*$"}++ },++ "readGrants": {++ "description": "1.1.0: namespaces the mod may only read. A namespace that is only here is read-only. The install's approved subset is still the grant.",++ "type": "object",++ "additionalProperties": false,++ "minProperties": 1,++ "properties": {++ "modules": {++ "description": "User-module namespaces (a trailing .* names a family).",++ "type": "array",++ "maxItems": 32,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)*(\\.\\*)?$"++ }++ },++ "mediaNamespaces": {++ "description": "Media namespaces.",++ "type": "array",++ "maxItems": 32,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_-]*(\\.[a-z0-9_-]+)*$"++ }++ }++ }++ },++ "writeGrants": {++ "description": "1.1.0: namespaces the mod may read and write. A write needs the namespace here, or in the older modules and mediaNamespaces lists, which mean read and write.",++ "type": "object",++ "additionalProperties": false,++ "minProperties": 1,++ "properties": {++ "modules": {++ "description": "User-module namespaces (a trailing .* names a family).",++ "type": "array",++ "maxItems": 32,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)*(\\.\\*)?$"++ }++ },++ "mediaNamespaces": {++ "description": "Media namespaces.",++ "type": "array",++ "maxItems": 32,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_-]*(\\.[a-z0-9_-]+)*$"++ }++ }++ }}}},@@@ -2362,6 +2418,68 @@"homepage": {"type": "string","pattern": "^https://[^\\s]{1,2000}$"++ },++ "billingHooks": {++ "description": "1.1.0: how the mod takes part in money, only through Billing and VIP primitives (ADR-013 amendment, ADR-012). entitlement: the mod checks that the person holds this entitlement (billing.entitlement.check or vip.entitlement.check, granted like any other capability). checkout: the mod sends the person to the platform's own checkout for this plan (vip.membership.checkout or a Billing intent). A mod never takes payment, sees payment details or holds a balance. Prices live in Billing and VIP, never in the manifest.",++ "type": "array",++ "maxItems": 32,++ "items": {++ "type": "object",++ "required": [++ "kind",++ "key"++ ],++ "additionalProperties": false,++ "properties": {++ "kind": {++ "enum": [++ "entitlement",++ "checkout"++ ]++ },++ "key": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_.:-]{0,127}$",++ "description": "The entitlement or plan key as Billing and VIP name it, e.g. vip.plan:pln_01jab… (lowercase, the form search.index-document@1 acl.entitlements uses)."++ },++ "description": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200,++ "description": "What it unlocks or sells, shown at install."++ }++ }++ }++ },++ "dependencies": {++ "description": "1.1.0: other mods this release needs. A runtime refuses to enable the mod while a required dependency is not installed, not enabled or outside its range. An optional one is used only when present.",++ "type": "array",++ "maxItems": 32,++ "uniqueItems": true,++ "items": {++ "type": "object",++ "required": [++ "id",++ "version"++ ],++ "additionalProperties": false,++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "version": {++ "type": "string",++ "minLength": 1,++ "maxLength": 100,++ "description": "Semver range of the dependency's releases this release works with, e.g. \">=1.2.0 <2.0.0\"."++ },++ "optional": {++ "type": "boolean",++ "default": false++ }++ }++ }}}}@@@ -3451,9 +3451,9 @@ export interface SourceItem {revisions?: unknown[];}--/** [email protected] (owner: contracts) */++/** [email protected] (owner: contracts) *//**-- * A mod as the platform knows it (ADR-013): who publishes it, which runtime runs it, which capabilities it asks for and the resources it may use. Requested capabilities are only a request: the install's approved subset is the grant, and trust tiers are install metadata that never change a grant check. The runtime-specific payload (a data pack, a script bundle) is not part of the manifest.++ * A mod as the platform knows it (ADR-013): who publishes it, which runtime runs it, which capabilities it asks for and the resources it may use. Requested capabilities are only a request: the install's approved subset is the grant, and trust tiers are install metadata that never change a grant check. The runtime-specific payload (a data pack, a script bundle) is not part of the manifest. 1.1.0 (openvibe-contracts v0.34.0) adds the optional permissions.readGrants and writeGrants, billingHooks and dependencies (ADR-013 amendment of 2026-09-24); every 1.0.0 manifest stays valid.*/export interface ModManifest {/**@@@ -3489,17 +3489,51 @@ export interface ModManifest {*/events?: string[];/**-- * User-module namespaces the mod wants to read or write (a trailing .* names a family).++ * User-module namespaces the mod wants to read or write (a trailing .* names a family). A namespace listed here may be read and written; readGrants and writeGrants (1.1.0) say which.** @maxItems 32*/modules?: string[];/**-- * Media namespaces the mod wants to read or write.++ * Media namespaces the mod wants to read or write. A namespace listed here may be read and written; readGrants and writeGrants (1.1.0) say which.** @maxItems 32*/mediaNamespaces?: string[];++ /**++ * 1.1.0: namespaces the mod may only read. A namespace that is only here is read-only. The install's approved subset is still the grant.++ */++ readGrants?: {++ /**++ * User-module namespaces (a trailing .* names a family).++ *++ * @maxItems 32++ */++ modules?: string[];++ /**++ * Media namespaces.++ *++ * @maxItems 32++ */++ mediaNamespaces?: string[];++ };++ /**++ * 1.1.0: namespaces the mod may read and write. A write needs the namespace here, or in the older modules and mediaNamespaces lists, which mean read and write.++ */++ writeGrants?: {++ /**++ * User-module namespaces (a trailing .* names a family).++ *++ * @maxItems 32++ */++ modules?: string[];++ /**++ * Media namespaces.++ *++ * @maxItems 32++ */++ mediaNamespaces?: string[];++ };};/*** The budget the mod asks for. Runtimes meter it; the sandbox that enforces it lives in OpenVibe.Host (Stage C).@@@ -3535,6 +3569,35 @@ export interface ModManifest {contracts?: string;};homepage?: string;++ /**++ * 1.1.0: how the mod takes part in money, only through Billing and VIP primitives (ADR-013 amendment, ADR-012). entitlement: the mod checks that the person holds this entitlement (billing.entitlement.check or vip.entitlement.check, granted like any other capability). checkout: the mod sends the person to the platform's own checkout for this plan (vip.membership.checkout or a Billing intent). A mod never takes payment, sees payment details or holds a balance. Prices live in Billing and VIP, never in the manifest.++ *++ * @maxItems 32++ */++ billingHooks?: {++ kind: "entitlement" | "checkout";++ /**++ * The entitlement or plan key as Billing and VIP name it, e.g. vip.plan:pln_01jab… (lowercase, the form search.index-document@1 acl.entitlements uses).++ */++ key: string;++ /**++ * What it unlocks or sells, shown at install.++ */++ description?: string;++ }[];++ /**++ * 1.1.0: other mods this release needs. A runtime refuses to enable the mod while a required dependency is not installed, not enabled or outside its range. An optional one is used only when present.++ *++ * @maxItems 32++ */++ dependencies?: {++ id: string;++ /**++ * Semver range of the dependency's releases this release works with, e.g. ">=1.2.0 <2.0.0".++ */++ version: string;++ optional?: boolean;++ }[];}/** [email protected] (owner: codes) */@@@ -305,6 +305,30 @@ for (const id of ['events.event.publish', 'events.event.read', 'events.subscript}}++// ── Mod manifest 1.1.0 (v0.34.0, ADR-013 amendment) ──────────────────────++// Read/write grants, billing hooks and dependencies are optional, so every 1.0.0 manifest stays++// valid; the split uses exactly the namespace patterns of the older lists; money goes only through++// Billing/VIP keys, never a price in the manifest.++{++ const fixture = (kind, f) => JSON.parse(fs.readFileSync(path.join(ROOT, 'fixtures/mods.mod-manifest', kind, f), 'utf8'));++ const M = (v) => contracts.validate('mods.mod-manifest@1', v).valid;++ const s = contracts.schema('mods.mod-manifest');++ ok(contracts.resolve('mods.mod-manifest').version === '1.1.0', 'mods.mod-manifest is 1.1.0');++ const full = fixture('valid', 'market-stall-1.1.json');++ ok(M(full) && full.billingHooks && full.dependencies && full.permissions.readGrants && full.permissions.writeGrants, 'a 1.1.0 manifest with every new field is valid');++ const { billingHooks, dependencies, ...v10 } = full;++ const { readGrants, writeGrants, ...perms10 } = full.permissions;++ ok(M({ ...v10, permissions: perms10 }) && M(fixture('valid', 'town-square.json')), 'a manifest without the 1.1.0 fields (1.0.0) stays valid');++ ok(['billingHooks', 'dependencies'].every(k => !s.required.includes(k)) && !s.properties.permissions.required.some(k => /Grants$/.test(k)), 'the 1.1.0 fields are optional');++ const p = s.properties.permissions.properties;++ for (const k of ['readGrants', 'writeGrants']) ok(p[k].properties.modules.items.pattern === p.modules.items.pattern && p[k].properties.mediaNamespaces.items.pattern === p.mediaNamespaces.items.pattern, `${k} takes the same namespace patterns as modules/mediaNamespaces`);++ ok(s.properties.billingHooks.items.properties.key.pattern === contracts.schema('search.index-document').properties.acl.properties.entitlements.items.pattern, 'a billing hook key is an entitlement key as Search and VIP write it');++ ok(JSON.stringify(s.properties.billingHooks.items.properties.kind.enum) === '["entitlement","checkout"]' && !M({ ...full, billingHooks: [{ ...full.billingHooks[1], amount_cents: 499 }] }), 'a billing hook checks an entitlement or opens a platform checkout; it never carries a price');++ ok(!M({ ...full, dependencies: [{ id: full.id }] }) && M({ ...full, dependencies: [{ id: full.dependencies[0].id, version: '*', optional: false }] }), 'a dependency names a mod and a range');++ const adr = fs.readFileSync(path.join(ROOT, 'docs/adr/ADR-013-mod-manifest.md'), 'utf8');++ ok(/Package signing/.test(adr) && /Review process/.test(adr) && /open decisions for the owner/.test(adr) && /Monetization only through Billing/.test(adr), 'ADR-013 names signing and review as open owner decisions and monetization through Billing');++}++// ── Retired services (v0.34.0) ───────────────────────────────────────────// A retired manifest offers nothing: no domain, capability, event or namespace. Realtime was closed// by ADR-005 (realtime runs inside OpenVibe.Events), so it is retired, not a placeholder.
@@ -168,7 +168,7 @@
@@ -168,7 +168,7 @@
},
},
{
{
"id": "mods.mod-manifest",
"id": "mods.mod-manifest",
- "version": "1.0.0",
+ "version": "1.1.0",
"owner": "contracts",
"owner": "contracts",
"schema": "mods/mod-manifest.v1.json",
"schema": "mods/mod-manifest.v1.json",
"visibility": "public",
"visibility": "public",
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/mods/mod-manifest.v1.json",
"$id": "https://openvibe.network/contracts/mods/mod-manifest.v1.json",
"title": "ModManifest",
"title": "ModManifest",
- "description": "A mod as the platform knows it (ADR-013): who publishes it, which runtime runs it, which capabilities it asks for and the resources it may use. Requested capabilities are only a request: the install's approved subset is the grant, and trust tiers are install metadata that never change a grant check. The runtime-specific payload (a data pack, a script bundle) is not part of the manifest.",
+ "description": "A mod as the platform knows it (ADR-013): who publishes it, which runtime runs it, which capabilities it asks for and the resources it may use. Requested capabilities are only a request: the install's approved subset is the grant, and trust tiers are install metadata that never change a grant check. The runtime-specific payload (a data pack, a script bundle) is not part of the manifest. 1.1.0 (openvibe-contracts v0.34.0) adds the optional permissions.readGrants and writeGrants, billingHooks and dependencies (ADR-013 amendment of 2026-09-24); every 1.0.0 manifest stays valid.",
"type": "object",
"type": "object",
"required": [
"required": [
"id",
"id",
@@ -63,18 +63,62 @@
@@ -63,18 +63,62 @@
"items": { "type": "string", "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$" }
"items": { "type": "string", "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$" }
},
},
"modules": {
"modules": {
- "description": "User-module namespaces the mod wants to read or write (a trailing .* names a family).",
+ "description": "User-module namespaces the mod wants to read or write (a trailing .* names a family). A namespace listed here may be read and written; readGrants and writeGrants (1.1.0) say which.",
"type": "array",
"type": "array",
"maxItems": 32,
"maxItems": 32,
"uniqueItems": true,
"uniqueItems": true,
"items": { "type": "string", "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)*(\\.\\*)?$" }
"items": { "type": "string", "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)*(\\.\\*)?$" }
},
},
"mediaNamespaces": {
"mediaNamespaces": {
- "description": "Media namespaces the mod wants to read or write.",
+ "description": "Media namespaces the mod wants to read or write. A namespace listed here may be read and written; readGrants and writeGrants (1.1.0) say which.",
"type": "array",
"type": "array",
"maxItems": 32,
"maxItems": 32,
"uniqueItems": true,
"uniqueItems": true,
"items": { "type": "string", "pattern": "^[a-z][a-z0-9_-]*(\\.[a-z0-9_-]+)*$" }
"items": { "type": "string", "pattern": "^[a-z][a-z0-9_-]*(\\.[a-z0-9_-]+)*$" }
+ },
+ "readGrants": {
+ "description": "1.1.0: namespaces the mod may only read. A namespace that is only here is read-only. The install's approved subset is still the grant.",
+ "type": "object",
+ "additionalProperties": false,
+ "minProperties": 1,
+ "properties": {
+ "modules": {
+ "description": "User-module namespaces (a trailing .* names a family).",
+ "type": "array",
+ "maxItems": 32,
+ "uniqueItems": true,
+ "items": { "type": "string", "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)*(\\.\\*)?$" }
+ },
+ "mediaNamespaces": {
+ "description": "Media namespaces.",
+ "type": "array",
+ "maxItems": 32,
+ "uniqueItems": true,
+ "items": { "type": "string", "pattern": "^[a-z][a-z0-9_-]*(\\.[a-z0-9_-]+)*$" }
+ }
+ }
+ },
+ "writeGrants": {
+ "description": "1.1.0: namespaces the mod may read and write. A write needs the namespace here, or in the older modules and mediaNamespaces lists, which mean read and write.",
+ "type": "object",
+ "additionalProperties": false,
+ "minProperties": 1,
+ "properties": {
+ "modules": {
+ "description": "User-module namespaces (a trailing .* names a family).",
+ "type": "array",
+ "maxItems": 32,
+ "uniqueItems": true,
+ "items": { "type": "string", "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)*(\\.\\*)?$" }
+ },
+ "mediaNamespaces": {
+ "description": "Media namespaces.",
+ "type": "array",
+ "maxItems": 32,
+ "uniqueItems": true,
+ "items": { "type": "string", "pattern": "^[a-z][a-z0-9_-]*(\\.[a-z0-9_-]+)*$" }
+ }
+ }
}
}
}
}
},
},
@@ -129,6 +173,46 @@
@@ -129,6 +173,46 @@
}
}
}
}
},
},
- "homepage": { "type": "string", "pattern": "^https://[^\\s]{1,2000}$" }
+ "homepage": { "type": "string", "pattern": "^https://[^\\s]{1,2000}$" },
+ "billingHooks": {
+ "description": "1.1.0: how the mod takes part in money, only through Billing and VIP primitives (ADR-013 amendment, ADR-012). entitlement: the mod checks that the person holds this entitlement (billing.entitlement.check or vip.entitlement.check, granted like any other capability). checkout: the mod sends the person to the platform's own checkout for this plan (vip.membership.checkout or a Billing intent). A mod never takes payment, sees payment details or holds a balance. Prices live in Billing and VIP, never in the manifest.",
+ "type": "array",
+ "maxItems": 32,
+ "items": {
+ "type": "object",
+ "required": ["kind", "key"],
+ "additionalProperties": false,
+ "properties": {
+ "kind": { "enum": ["entitlement", "checkout"] },
+ "key": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_.:-]{0,127}$",
+ "description": "The entitlement or plan key as Billing and VIP name it, e.g. vip.plan:pln_01jab… (lowercase, the form search.index-document@1 acl.entitlements uses)."
+ },
+ "description": { "type": "string", "minLength": 1, "maxLength": 200, "description": "What it unlocks or sells, shown at install." }
+ }
+ }
+ },
+ "dependencies": {
+ "description": "1.1.0: other mods this release needs. A runtime refuses to enable the mod while a required dependency is not installed, not enabled or outside its range. An optional one is used only when present.",
+ "type": "array",
+ "maxItems": 32,
+ "uniqueItems": true,
+ "items": {
+ "type": "object",
+ "required": ["id", "version"],
+ "additionalProperties": false,
+ "properties": {
+ "id": { "type": "string", "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$" },
+ "version": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Semver range of the dependency's releases this release works with, e.g. \">=1.2.0 <2.0.0\"."
+ },
+ "optional": { "type": "boolean", "default": false }
+ }
+ }
+ }
}
}
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "id": "mod_01JABCDEFGHJKMNPQRSTVWXYZ1", "name": "X", "version": "1.0.0", "publisher": { "type": "user", "id": "usr_01JABCDEFGHJKMNPQRSTVWXYZ0" }, "target": "games.browser", "runtime": "games-content@1", "resources": { "cpuMs": 1, "memoryMb": 0, "storageMb": 0 }, "compatibility": { "runtime": ">=1.0.0" }, "permissions": { "capabilities": [], "writeGrants": { "capabilities": ["games.prop.place"] } } }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "id": "mod_01JABCDEFGHJKMNPQRSTVWXYZ1", "name": "X", "version": "1.0.0", "publisher": { "type": "user", "id": "usr_01JABCDEFGHJKMNPQRSTVWXYZ0" }, "target": "games.browser", "runtime": "games-content@1", "resources": { "cpuMs": 1, "memoryMb": 0, "storageMb": 0 }, "compatibility": { "runtime": ">=1.0.0" }, "permissions": { "capabilities": [] }, "dependencies": [{ "id": "town-square", "version": ">=1.0.0" }] }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "id": "mod_01JABCDEFGHJKMNPQRSTVWXYZ1", "name": "X", "version": "1.0.0", "publisher": { "type": "user", "id": "usr_01JABCDEFGHJKMNPQRSTVWXYZ0" }, "target": "games.browser", "runtime": "games-content@1", "resources": { "cpuMs": 1, "memoryMb": 0, "storageMb": 0 }, "compatibility": { "runtime": ">=1.0.0" }, "permissions": { "capabilities": [] }, "dependencies": [{ "id": "mod_01JABCDEFGHJKMNPQRSTVWXYZ0" }] }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "id": "mod_01JABCDEFGHJKMNPQRSTVWXYZ1", "name": "X", "version": "1.0.0", "publisher": { "type": "user", "id": "usr_01JABCDEFGHJKMNPQRSTVWXYZ0" }, "target": "games.browser", "runtime": "games-content@1", "resources": { "cpuMs": 1, "memoryMb": 0, "storageMb": 0 }, "compatibility": { "runtime": ">=1.0.0" }, "permissions": { "capabilities": [], "readGrants": {} } }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "id": "mod_01JABCDEFGHJKMNPQRSTVWXYZ1", "name": "X", "version": "1.0.0", "publisher": { "type": "user", "id": "usr_01JABCDEFGHJKMNPQRSTVWXYZ0" }, "target": "games.browser", "runtime": "games-content@1", "resources": { "cpuMs": 1, "memoryMb": 0, "storageMb": 0 }, "compatibility": { "runtime": ">=1.0.0" }, "permissions": { "capabilities": [] }, "billingHooks": [{ "kind": "price", "key": "vip.plan:pln_01jabcdefghjkmnpqrstvwxyz2" }] }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "id": "mod_01JABCDEFGHJKMNPQRSTVWXYZ1", "name": "X", "version": "1.0.0", "publisher": { "type": "user", "id": "usr_01JABCDEFGHJKMNPQRSTVWXYZ0" }, "target": "games.browser", "runtime": "games-content@1", "resources": { "cpuMs": 1, "memoryMb": 0, "storageMb": 0 }, "compatibility": { "runtime": ">=1.0.0" }, "permissions": { "capabilities": [] }, "billingHooks": [{ "kind": "checkout", "key": "vip.plan:pln_01jabcdefghjkmnpqrstvwxyz2", "amount_cents": 499 }] }
new file mode 100644
new file mode 100644
@@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
+{
+ "id": "mod_01JABCDEFGHJKMNPQRSTVWXYZ1",
+ "name": "Market Stall",
+ "version": "1.2.0",
+ "description": "A members-only stall that reads the player's profile summary and keeps its own notes.",
+ "publisher": { "type": "user", "id": "usr_01JABCDEFGHJKMNPQRSTVWXYZ0" },
+ "target": "games.browser",
+ "runtime": "games-content@1",
+ "permissions": {
+ "capabilities": ["games.prop.place", "vip.entitlement.check"],
+ "readGrants": { "modules": ["games.progress.summary"], "mediaNamespaces": ["games.maps"] },
+ "writeGrants": { "modules": ["games.market_stall.*"] }
+ },
+ "resources": { "cpuMs": 2, "memoryMb": 16, "storageMb": 5 },
+ "compatibility": { "runtime": ">=1.0.0 <2.0.0", "contracts": ">=0.34.0 <1.0.0" },
+ "billingHooks": [
+ { "kind": "entitlement", "key": "vip.plan:pln_01jabcdefghjkmnpqrstvwxyz2", "description": "Opens the stall for the creator's VIP members." },
+ { "kind": "checkout", "key": "vip.plan:pln_01jabcdefghjkmnpqrstvwxyz2" }
+ ],
+ "dependencies": [
+ { "id": "mod_01JABCDEFGHJKMNPQRSTVWXYZ0", "version": ">=1.0.0 <2.0.0" },
+ { "id": "mod_01JABCDEFGHJKMNPQRSTVWXYZ3", "version": "^0.3.0", "optional": true }
+ ]
+}
@@ -2188,14 +2188,14 @@
@@ -2188,14 +2188,14 @@
}
}
},
},
"mods.mod-manifest": {
"mods.mod-manifest": {
- "version": "1.0.0",
+ "version": "1.1.0",
"owner": "contracts",
"owner": "contracts",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/mods/mod-manifest.v1.json",
"$id": "https://openvibe.network/contracts/mods/mod-manifest.v1.json",
"title": "ModManifest",
"title": "ModManifest",
- "description": "A mod as the platform knows it (ADR-013): who publishes it, which runtime runs it, which capabilities it asks for and the resources it may use. Requested capabilities are only a request: the install's approved subset is the grant, and trust tiers are install metadata that never change a grant check. The runtime-specific payload (a data pack, a script bundle) is not part of the manifest.",
+ "description": "A mod as the platform knows it (ADR-013): who publishes it, which runtime runs it, which capabilities it asks for and the resources it may use. Requested capabilities are only a request: the install's approved subset is the grant, and trust tiers are install metadata that never change a grant check. The runtime-specific payload (a data pack, a script bundle) is not part of the manifest. 1.1.0 (openvibe-contracts v0.34.0) adds the optional permissions.readGrants and writeGrants, billingHooks and dependencies (ADR-013 amendment of 2026-09-24); every 1.0.0 manifest stays valid.",
"type": "object",
"type": "object",
"required": [
"required": [
"id",
"id",
@@ -2271,7 +2271,7 @@
@@ -2271,7 +2271,7 @@
}
}
},
},
"modules": {
"modules": {
- "description": "User-module namespaces the mod wants to read or write (a trailing .* names a family).",
+ "description": "User-module namespaces the mod wants to read or write (a trailing .* names a family). A namespace listed here may be read and written; readGrants and writeGrants (1.1.0) say which.",
"type": "array",
"type": "array",
"maxItems": 32,
"maxItems": 32,
"uniqueItems": true,
"uniqueItems": true,
@@ -2281,7 +2281,7 @@
@@ -2281,7 +2281,7 @@
}
}
},
},
"mediaNamespaces": {
"mediaNamespaces": {
- "description": "Media namespaces the mod wants to read or write.",
+ "description": "Media namespaces the mod wants to read or write. A namespace listed here may be read and written; readGrants and writeGrants (1.1.0) say which.",
"type": "array",
"type": "array",
"maxItems": 32,
"maxItems": 32,
"uniqueItems": true,
"uniqueItems": true,
@@ -2289,6 +2289,62 @@
@@ -2289,6 +2289,62 @@
"type": "string",
"type": "string",
"pattern": "^[a-z][a-z0-9_-]*(\\.[a-z0-9_-]+)*$"
"pattern": "^[a-z][a-z0-9_-]*(\\.[a-z0-9_-]+)*$"
}
}
+ },
+ "readGrants": {
+ "description": "1.1.0: namespaces the mod may only read. A namespace that is only here is read-only. The install's approved subset is still the grant.",
+ "type": "object",
+ "additionalProperties": false,
+ "minProperties": 1,
+ "properties": {
+ "modules": {
+ "description": "User-module namespaces (a trailing .* names a family).",
+ "type": "array",
+ "maxItems": 32,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)*(\\.\\*)?$"
+ }
+ },
+ "mediaNamespaces": {
+ "description": "Media namespaces.",
+ "type": "array",
+ "maxItems": 32,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_-]*(\\.[a-z0-9_-]+)*$"
+ }
+ }
+ }
+ },
+ "writeGrants": {
+ "description": "1.1.0: namespaces the mod may read and write. A write needs the namespace here, or in the older modules and mediaNamespaces lists, which mean read and write.",
+ "type": "object",
+ "additionalProperties": false,
+ "minProperties": 1,
+ "properties": {
+ "modules": {
+ "description": "User-module namespaces (a trailing .* names a family).",
+ "type": "array",
+ "maxItems": 32,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)*(\\.\\*)?$"
+ }
+ },
+ "mediaNamespaces": {
+ "description": "Media namespaces.",
+ "type": "array",
+ "maxItems": 32,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_-]*(\\.[a-z0-9_-]+)*$"
+ }
+ }
+ }
}
}
}
}
},
},
@@ -2362,6 +2418,68 @@
@@ -2362,6 +2418,68 @@
"homepage": {
"homepage": {
"type": "string",
"type": "string",
"pattern": "^https://[^\\s]{1,2000}$"
"pattern": "^https://[^\\s]{1,2000}$"
+ },
+ "billingHooks": {
+ "description": "1.1.0: how the mod takes part in money, only through Billing and VIP primitives (ADR-013 amendment, ADR-012). entitlement: the mod checks that the person holds this entitlement (billing.entitlement.check or vip.entitlement.check, granted like any other capability). checkout: the mod sends the person to the platform's own checkout for this plan (vip.membership.checkout or a Billing intent). A mod never takes payment, sees payment details or holds a balance. Prices live in Billing and VIP, never in the manifest.",
+ "type": "array",
+ "maxItems": 32,
+ "items": {
+ "type": "object",
+ "required": [
+ "kind",
+ "key"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "enum": [
+ "entitlement",
+ "checkout"
+ ]
+ },
+ "key": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_.:-]{0,127}$",
+ "description": "The entitlement or plan key as Billing and VIP name it, e.g. vip.plan:pln_01jab… (lowercase, the form search.index-document@1 acl.entitlements uses)."
+ },
+ "description": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200,
+ "description": "What it unlocks or sells, shown at install."
+ }
+ }
+ }
+ },
+ "dependencies": {
+ "description": "1.1.0: other mods this release needs. A runtime refuses to enable the mod while a required dependency is not installed, not enabled or outside its range. An optional one is used only when present.",
+ "type": "array",
+ "maxItems": 32,
+ "uniqueItems": true,
+ "items": {
+ "type": "object",
+ "required": [
+ "id",
+ "version"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "version": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Semver range of the dependency's releases this release works with, e.g. \">=1.2.0 <2.0.0\"."
+ },
+ "optional": {
+ "type": "boolean",
+ "default": false
+ }
+ }
+ }
}
}
}
}
}
}
@@ -3451,9 +3451,9 @@ export interface SourceItem {
@@ -3451,9 +3451,9 @@ export interface SourceItem {
revisions?: unknown[];
revisions?: unknown[];
}
}
-/** [email protected] (owner: contracts) */
+/** [email protected] (owner: contracts) */
/**
/**
- * A mod as the platform knows it (ADR-013): who publishes it, which runtime runs it, which capabilities it asks for and the resources it may use. Requested capabilities are only a request: the install's approved subset is the grant, and trust tiers are install metadata that never change a grant check. The runtime-specific payload (a data pack, a script bundle) is not part of the manifest.
+ * A mod as the platform knows it (ADR-013): who publishes it, which runtime runs it, which capabilities it asks for and the resources it may use. Requested capabilities are only a request: the install's approved subset is the grant, and trust tiers are install metadata that never change a grant check. The runtime-specific payload (a data pack, a script bundle) is not part of the manifest. 1.1.0 (openvibe-contracts v0.34.0) adds the optional permissions.readGrants and writeGrants, billingHooks and dependencies (ADR-013 amendment of 2026-09-24); every 1.0.0 manifest stays valid.
*/
*/
export interface ModManifest {
export interface ModManifest {
/**
/**
@@ -3489,17 +3489,51 @@ export interface ModManifest {
@@ -3489,17 +3489,51 @@ export interface ModManifest {
*/
*/
events?: string[];
events?: string[];
/**
/**
- * User-module namespaces the mod wants to read or write (a trailing .* names a family).
+ * User-module namespaces the mod wants to read or write (a trailing .* names a family). A namespace listed here may be read and written; readGrants and writeGrants (1.1.0) say which.
*
*
* @maxItems 32
* @maxItems 32
*/
*/
modules?: string[];
modules?: string[];
/**
/**
- * Media namespaces the mod wants to read or write.
+ * Media namespaces the mod wants to read or write. A namespace listed here may be read and written; readGrants and writeGrants (1.1.0) say which.
*
*
* @maxItems 32
* @maxItems 32
*/
*/
mediaNamespaces?: string[];
mediaNamespaces?: string[];
+ /**
+ * 1.1.0: namespaces the mod may only read. A namespace that is only here is read-only. The install's approved subset is still the grant.
+ */
+ readGrants?: {
+ /**
+ * User-module namespaces (a trailing .* names a family).
+ *
+ * @maxItems 32
+ */
+ modules?: string[];
+ /**
+ * Media namespaces.
+ *
+ * @maxItems 32
+ */
+ mediaNamespaces?: string[];
+ };
+ /**
+ * 1.1.0: namespaces the mod may read and write. A write needs the namespace here, or in the older modules and mediaNamespaces lists, which mean read and write.
+ */
+ writeGrants?: {
+ /**
+ * User-module namespaces (a trailing .* names a family).
+ *
+ * @maxItems 32
+ */
+ modules?: string[];
+ /**
+ * Media namespaces.
+ *
+ * @maxItems 32
+ */
+ mediaNamespaces?: string[];
+ };
};
};
/**
/**
* The budget the mod asks for. Runtimes meter it; the sandbox that enforces it lives in OpenVibe.Host (Stage C).
* The budget the mod asks for. Runtimes meter it; the sandbox that enforces it lives in OpenVibe.Host (Stage C).
@@ -3535,6 +3569,35 @@ export interface ModManifest {
@@ -3535,6 +3569,35 @@ export interface ModManifest {
contracts?: string;
contracts?: string;
};
};
homepage?: string;
homepage?: string;
+ /**
+ * 1.1.0: how the mod takes part in money, only through Billing and VIP primitives (ADR-013 amendment, ADR-012). entitlement: the mod checks that the person holds this entitlement (billing.entitlement.check or vip.entitlement.check, granted like any other capability). checkout: the mod sends the person to the platform's own checkout for this plan (vip.membership.checkout or a Billing intent). A mod never takes payment, sees payment details or holds a balance. Prices live in Billing and VIP, never in the manifest.
+ *
+ * @maxItems 32
+ */
+ billingHooks?: {
+ kind: "entitlement" | "checkout";
+ /**
+ * The entitlement or plan key as Billing and VIP name it, e.g. vip.plan:pln_01jab… (lowercase, the form search.index-document@1 acl.entitlements uses).
+ */
+ key: string;
+ /**
+ * What it unlocks or sells, shown at install.
+ */
+ description?: string;
+ }[];
+ /**
+ * 1.1.0: other mods this release needs. A runtime refuses to enable the mod while a required dependency is not installed, not enabled or outside its range. An optional one is used only when present.
+ *
+ * @maxItems 32
+ */
+ dependencies?: {
+ id: string;
+ /**
+ * Semver range of the dependency's releases this release works with, e.g. ">=1.2.0 <2.0.0".
+ */
+ version: string;
+ optional?: boolean;
+ }[];
}
}
/** [email protected] (owner: codes) */
/** [email protected] (owner: codes) */
@@ -305,6 +305,30 @@ for (const id of ['events.event.publish', 'events.event.read', 'events.subscript
@@ -305,6 +305,30 @@ for (const id of ['events.event.publish', 'events.event.read', 'events.subscript
}
}
}
}
+// ── Mod manifest 1.1.0 (v0.34.0, ADR-013 amendment) ──────────────────────
+// Read/write grants, billing hooks and dependencies are optional, so every 1.0.0 manifest stays
+// valid; the split uses exactly the namespace patterns of the older lists; money goes only through
+// Billing/VIP keys, never a price in the manifest.
+{
+ const fixture = (kind, f) => JSON.parse(fs.readFileSync(path.join(ROOT, 'fixtures/mods.mod-manifest', kind, f), 'utf8'));
+ const M = (v) => contracts.validate('mods.mod-manifest@1', v).valid;
+ const s = contracts.schema('mods.mod-manifest');
+ ok(contracts.resolve('mods.mod-manifest').version === '1.1.0', 'mods.mod-manifest is 1.1.0');
+ const full = fixture('valid', 'market-stall-1.1.json');
+ ok(M(full) && full.billingHooks && full.dependencies && full.permissions.readGrants && full.permissions.writeGrants, 'a 1.1.0 manifest with every new field is valid');
+ const { billingHooks, dependencies, ...v10 } = full;
+ const { readGrants, writeGrants, ...perms10 } = full.permissions;
+ ok(M({ ...v10, permissions: perms10 }) && M(fixture('valid', 'town-square.json')), 'a manifest without the 1.1.0 fields (1.0.0) stays valid');
+ ok(['billingHooks', 'dependencies'].every(k => !s.required.includes(k)) && !s.properties.permissions.required.some(k => /Grants$/.test(k)), 'the 1.1.0 fields are optional');
+ const p = s.properties.permissions.properties;
+ for (const k of ['readGrants', 'writeGrants']) ok(p[k].properties.modules.items.pattern === p.modules.items.pattern && p[k].properties.mediaNamespaces.items.pattern === p.mediaNamespaces.items.pattern, `${k} takes the same namespace patterns as modules/mediaNamespaces`);
+ ok(s.properties.billingHooks.items.properties.key.pattern === contracts.schema('search.index-document').properties.acl.properties.entitlements.items.pattern, 'a billing hook key is an entitlement key as Search and VIP write it');
+ ok(JSON.stringify(s.properties.billingHooks.items.properties.kind.enum) === '["entitlement","checkout"]' && !M({ ...full, billingHooks: [{ ...full.billingHooks[1], amount_cents: 499 }] }), 'a billing hook checks an entitlement or opens a platform checkout; it never carries a price');
+ ok(!M({ ...full, dependencies: [{ id: full.id }] }) && M({ ...full, dependencies: [{ id: full.dependencies[0].id, version: '*', optional: false }] }), 'a dependency names a mod and a range');
+ const adr = fs.readFileSync(path.join(ROOT, 'docs/adr/ADR-013-mod-manifest.md'), 'utf8');
+ ok(/Package signing/.test(adr) && /Review process/.test(adr) && /open decisions for the owner/.test(adr) && /Monetization only through Billing/.test(adr), 'ADR-013 names signing and review as open owner decisions and monetization through Billing');
+}
+
// ── Retired services (v0.34.0) ───────────────────────────────────────────
// ── Retired services (v0.34.0) ───────────────────────────────────────────
// A retired manifest offers nothing: no domain, capability, event or namespace. Realtime was closed
// A retired manifest offers nothing: no domain, capability, event or namespace. Realtime was closed
// by ADR-005 (realtime runs inside OpenVibe.Events), so it is retired, not a placeholder.
// by ADR-005 (realtime runs inside OpenVibe.Events), so it is retired, not a placeholder.