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

OP OpenVibers committed 24 Sept 2026 · 2240b39566 · 12 files, +332 −13


Changes

ddiff --git a/contracts/catalog.json b/contracts/catalog.json
iindex 9bb417ba..37fb035c 100644
---- a/contracts/catalog.json
++++ b/contracts/catalog.json
@@@ -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",
ddiff --git a/contracts/mods/mod-manifest.v1.json b/contracts/mods/mod-manifest.v1.json
iindex 2f5ae9bd..24712ecd 100644
---- a/contracts/mods/mod-manifest.v1.json
++++ b/contracts/mods/mod-manifest.v1.json
@@@ -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 }
++ }
++ }
++ }
}
}
ddiff --git a/fixtures/mods.mod-manifest/invalid/capabilities-in-write-grants.json b/fixtures/mods.mod-manifest/invalid/capabilities-in-write-grants.json
nnew file mode 100644
iindex 00000000..5ba0b011
---- /dev/null
++++ b/fixtures/mods.mod-manifest/invalid/capabilities-in-write-grants.json
@@@ -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"] } } }
ddiff --git a/fixtures/mods.mod-manifest/invalid/dependency-not-a-mod-id.json b/fixtures/mods.mod-manifest/invalid/dependency-not-a-mod-id.json
nnew file mode 100644
iindex 00000000..cb79a0be
---- /dev/null
++++ b/fixtures/mods.mod-manifest/invalid/dependency-not-a-mod-id.json
@@@ -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" }] }
ddiff --git a/fixtures/mods.mod-manifest/invalid/dependency-without-range.json b/fixtures/mods.mod-manifest/invalid/dependency-without-range.json
nnew file mode 100644
iindex 00000000..58deac73
---- /dev/null
++++ b/fixtures/mods.mod-manifest/invalid/dependency-without-range.json
@@@ -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" }] }
ddiff --git a/fixtures/mods.mod-manifest/invalid/empty-read-grants.json b/fixtures/mods.mod-manifest/invalid/empty-read-grants.json
nnew file mode 100644
iindex 00000000..eb37a5e0
---- /dev/null
++++ b/fixtures/mods.mod-manifest/invalid/empty-read-grants.json
@@@ -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": {} } }
ddiff --git a/fixtures/mods.mod-manifest/invalid/hook-kind-price.json b/fixtures/mods.mod-manifest/invalid/hook-kind-price.json
nnew file mode 100644
iindex 00000000..5f910d02
---- /dev/null
++++ b/fixtures/mods.mod-manifest/invalid/hook-kind-price.json
@@@ -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" }] }
ddiff --git a/fixtures/mods.mod-manifest/invalid/hook-with-price.json b/fixtures/mods.mod-manifest/invalid/hook-with-price.json
nnew file mode 100644
iindex 00000000..ecb6b138
---- /dev/null
++++ b/fixtures/mods.mod-manifest/invalid/hook-with-price.json
@@@ -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 }] }
ddiff --git a/fixtures/mods.mod-manifest/valid/market-stall-1.1.json b/fixtures/mods.mod-manifest/valid/market-stall-1.1.json
nnew file mode 100644
iindex 00000000..1aa090fc
---- /dev/null
++++ b/fixtures/mods.mod-manifest/valid/market-stall-1.1.json
@@@ -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 }
++ ]
++}
ddiff --git a/generated/json-schema/bundle.json b/generated/json-schema/bundle.json
iindex cd44bfaa..05941cd1 100644
---- a/generated/json-schema/bundle.json
++++ b/generated/json-schema/bundle.json
@@@ -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
++ }
++ }
++ }
}
}
}
ddiff --git a/generated/typescript/index.d.ts b/generated/typescript/index.d.ts
iindex 5a2cf346..7cd0a3f2 100644
---- a/generated/typescript/index.d.ts
++++ b/generated/typescript/index.d.ts
@@@ -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) */
ddiff --git a/test/run.js b/test/run.js
iindex acea4954..21a07f73 100644
---- a/test/run.js
++++ b/test/run.js
@@@ -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.