openvibers/openvibe.contracts Commits
v0.28.0: developer-app events (ADR-014) — public capabilities events.app.publish, events.app.read, events.app.subscribe (project-scoped; app.<project_key>.* types, source app-<lowercased ULID>, envelope unchanged); Events manifest 0.2.0
Changes
@@@ -68,6 +68,18 @@ Manifests: `manifests/services/` (the 7 running services plus the 21 charter rep`manifests/namespaces/*.json` define per-subject module namespaces (`modules.namespace@1`): owner, data schema, writers (`owner` service and/or the `user`), public fields, quota and what happens when the owner is retired. OpenVibe.Network stores the records (`modules.module-record@1`, revision-checked writes). `contracts.modules.validateData / publicView / canWrite` apply the same rules everywhere. Modules hold portable preferences and summaries, never domain truth, money or authoritative game inventory.++## Developer-app events (v0.28)++++Three `public` capabilities let a developer app (ADR-014) use OpenVibe.Events with a scoped app token, without any first-party `events.*` capability (those stay `internal`):++++| Capability | What the app may do |++|---|---|++| `events.app.publish` | publish event types `app.<project_key>.<name>[.<more>]` only |++| `events.app.read` | pull its own project's events (same environment) plus first-party `public` events |++| `events.app.subscribe` | webhook subscriptions in the same scope, to public https endpoints only |++++`project_key` is `p` followed by the project's ULID in lowercase (`prj_01JAB…` → `p01jab…`), so it fits an event-type segment. An app event's `source` is `app-` followed by the app's ULID in lowercase (`app:app_01JAB…` → `app-01jab…`), which keeps `events.event-envelope@1` unchanged (its `source` pattern already allows it). OpenVibe.Events enforces the scope, the sandbox separation and per-project quotas.++## Versioning and compatibility- A contract id is permanent. Minor versions only add optional fields.nnew file mode 100644@@@ -0,0 +1,19 @@++{++ "event_id": "evt_01JAB2C3D4E5F6G7H8J9K0MNPR",++ "event_type": "app.p01jab2c3d4e5f6g7h8j9k0mnpq.order.created",++ "version": 1,++ "source": "app-01jab2c3d4e5f6g7h8j9k0mnps",++ "actor": {++ "type": "app",++ "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPS"++ },++ "timestamp": "2026-09-23T12:00:00Z",++ "visibility": "internal",++ "subject": {++ "type": "order",++ "id": "42"++ },++ "payload": {++ "total": 3++ }++}nnew file mode 100644@@@ -0,0 +1,20 @@++{++ "id": "events.app.publish",++ "version": "1.0",++ "owner": "events",++ "status": "active",++ "visibility": "public",++ "description": "Publish durable events (events.event-envelope@1) as a developer app (ADR-014). The app token (sub app:app_<ULID>, project_id prj_<ULID>, env) may publish only event types app.<project_key>.<name>[.<more>], where project_key is 'p' followed by the project's ULID in lowercase (prj_01JAB... -> p01jab...). source must be 'app-' followed by the app's ULID in lowercase; actor is the app (or the on_behalf_of user). Events stores the token's env with each event: sandbox events are never delivered to production subscriptions nor shown to production apps. Rate and retained bytes are limited per project and environment by OpenVibe.Events.",++ "inputSchema": "events.event-envelope@1",++ "permissions": [++ "events:write"++ ],++ "resourceConstraints": [++ "project"++ ],++ "quotaClass": "events-app-publish",++ "events": [],++ "implementedBy": [++ "POST /api/v1/events"++ ]++}nnew file mode 100644@@@ -0,0 +1,22 @@++{++ "id": "events.app.read",++ "version": "1.0",++ "owner": "events",++ "status": "active",++ "visibility": "public",++ "description": "Pull events as a developer app (ADR-014): only the app's own project's events (app.<project_key>.*, same environment as the token) plus first-party platform events with visibility public. Topic patterns must start with a literal segment; app.* patterns must name the caller's own project_key. Checkpoints are stored per app.",++ "permissions": [++ "events:read"++ ],++ "resourceConstraints": [++ "project"++ ],++ "quotaClass": "events-app-read",++ "events": [],++ "implementedBy": [++ "GET /api/v1/events",++ "GET /api/v1/events/:event_id",++ "GET /api/v1/checkpoints",++ "PUT /api/v1/checkpoints"++ ]++}nnew file mode 100644@@@ -0,0 +1,23 @@++{++ "id": "events.app.subscribe",++ "version": "1.0",++ "owner": "events",++ "status": "active",++ "visibility": "public",++ "description": "Manage webhook subscriptions as a developer app (ADR-014). Topic patterns are limited to the scope of events.app.read (own project's app.<project_key>.* events in the token's environment, plus public platform events). Endpoints must be https URLs whose host resolves only to public IP addresses; OpenVibe.Events re-checks the resolved address when it delivers and never follows redirects. Deliveries are signed with the subscription secret (X-OpenVibe-Signature: sha256=<HMAC of the raw body>). Subscriptions carry project_id and env; sandbox events reach sandbox subscriptions only.",++ "permissions": [++ "events:subscribe"++ ],++ "resourceConstraints": [++ "project"++ ],++ "quotaClass": "events-app-subscribe",++ "events": [],++ "implementedBy": [++ "POST /api/v1/subscriptions",++ "GET /api/v1/subscriptions",++ "GET /api/v1/subscriptions/:id",++ "POST /api/v1/subscriptions/:id/disable",++ "POST /api/v1/subscriptions/:id/enable"++ ]++}@@@ -1,5 +1,5 @@{-- "version": "0.1.0",++ "version": "0.2.0","contractRanges": {"openvibe-contracts": ">=0.5.1 <1.0.0"},@@@ -17,7 +17,10 @@"events.event.publish","events.event.read","events.subscription.manage",-- "events.delivery.admin"++ "events.delivery.admin",++ "events.app.publish",++ "events.app.read",++ "events.app.subscribe"],"eventsProduced": [],"eventsConsumed": [],@@@ -1,6 +1,6 @@{"name": "openvibe-contracts",-- "version": "0.26.0",++ "version": "0.28.0","description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.","main": "index.js","types": "generated/typescript/index.d.ts",@@@ -58,6 +58,21 @@ for (const cap of capabilities.manifests) {for (const k of ['inputSchema', 'outputSchema']) if (cap[k]) ok(contracts.resolve(cap[k]), `${cap.id} ${k} resolves`);ok(cap.status !== 'active' || cap.implementedBy.length > 0, `active ${cap.id} names the route that implements it`);}++// Developer-app events (v0.28, ADR-014): grantable to apps, owned by Events, project-scoped. The++// existing service-level events.* capabilities stay internal.++for (const id of ['events.app.publish', 'events.app.read', 'events.app.subscribe']) {++ const c = capabilities.get(id);++ ok(c && c.owner === 'events' && c.visibility === 'public' && c.status === 'active' && c.resourceConstraints.includes('project'), `${id} is a public, active, project-scoped Events capability`);++}++for (const id of ['events.event.publish', 'events.event.read', 'events.subscription.manage', 'events.delivery.admin']) {++ ok(capabilities.get(id).visibility === 'internal', `${id} stays internal`);++}++{++ // An app event keeps source and event_type inside the envelope's existing patterns.++ const env = JSON.parse(fs.readFileSync(path.join(ROOT, 'fixtures/events.event-envelope/valid/app-event.json'), 'utf8'));++ ok(contracts.validate('events.event-envelope@1', { ...env, source: `app-${'01JAB2C3D4E5F6G7H8J9K0MNPS'.toLowerCase()}` }).valid, 'app-<lowercased ULID> is a valid source');++ ok(!contracts.validate('events.event-envelope@1', { ...env, source: 'app:app_01JAB2C3D4E5F6G7H8J9K0MNPS' }).valid, 'the raw app subject is not a valid source');++}// ── Ids ──────────────────────────────────────────────────────────────────for (const kind of ['user', 'guest', 'app', 'mod']) {
@@ -68,6 +68,18 @@ Manifests: `manifests/services/` (the 7 running services plus the 21 charter rep
@@ -68,6 +68,18 @@ Manifests: `manifests/services/` (the 7 running services plus the 21 charter rep
`manifests/namespaces/*.json` define per-subject module namespaces (`modules.namespace@1`): owner, data schema, writers (`owner` service and/or the `user`), public fields, quota and what happens when the owner is retired. OpenVibe.Network stores the records (`modules.module-record@1`, revision-checked writes). `contracts.modules.validateData / publicView / canWrite` apply the same rules everywhere. Modules hold portable preferences and summaries, never domain truth, money or authoritative game inventory.
`manifests/namespaces/*.json` define per-subject module namespaces (`modules.namespace@1`): owner, data schema, writers (`owner` service and/or the `user`), public fields, quota and what happens when the owner is retired. OpenVibe.Network stores the records (`modules.module-record@1`, revision-checked writes). `contracts.modules.validateData / publicView / canWrite` apply the same rules everywhere. Modules hold portable preferences and summaries, never domain truth, money or authoritative game inventory.
+## Developer-app events (v0.28)
+
+Three `public` capabilities let a developer app (ADR-014) use OpenVibe.Events with a scoped app token, without any first-party `events.*` capability (those stay `internal`):
+
+| Capability | What the app may do |
+|---|---|
+| `events.app.publish` | publish event types `app.<project_key>.<name>[.<more>]` only |
+| `events.app.read` | pull its own project's events (same environment) plus first-party `public` events |
+| `events.app.subscribe` | webhook subscriptions in the same scope, to public https endpoints only |
+
+`project_key` is `p` followed by the project's ULID in lowercase (`prj_01JAB…` → `p01jab…`), so it fits an event-type segment. An app event's `source` is `app-` followed by the app's ULID in lowercase (`app:app_01JAB…` → `app-01jab…`), which keeps `events.event-envelope@1` unchanged (its `source` pattern already allows it). OpenVibe.Events enforces the scope, the sandbox separation and per-project quotas.
+
## Versioning and compatibility
## Versioning and compatibility
- A contract id is permanent. Minor versions only add optional fields.
- A contract id is permanent. Minor versions only add optional fields.
new file mode 100644
new file mode 100644
@@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
+{
+ "event_id": "evt_01JAB2C3D4E5F6G7H8J9K0MNPR",
+ "event_type": "app.p01jab2c3d4e5f6g7h8j9k0mnpq.order.created",
+ "version": 1,
+ "source": "app-01jab2c3d4e5f6g7h8j9k0mnps",
+ "actor": {
+ "type": "app",
+ "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPS"
+ },
+ "timestamp": "2026-09-23T12:00:00Z",
+ "visibility": "internal",
+ "subject": {
+ "type": "order",
+ "id": "42"
+ },
+ "payload": {
+ "total": 3
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
+{
+ "id": "events.app.publish",
+ "version": "1.0",
+ "owner": "events",
+ "status": "active",
+ "visibility": "public",
+ "description": "Publish durable events (events.event-envelope@1) as a developer app (ADR-014). The app token (sub app:app_<ULID>, project_id prj_<ULID>, env) may publish only event types app.<project_key>.<name>[.<more>], where project_key is 'p' followed by the project's ULID in lowercase (prj_01JAB... -> p01jab...). source must be 'app-' followed by the app's ULID in lowercase; actor is the app (or the on_behalf_of user). Events stores the token's env with each event: sandbox events are never delivered to production subscriptions nor shown to production apps. Rate and retained bytes are limited per project and environment by OpenVibe.Events.",
+ "inputSchema": "events.event-envelope@1",
+ "permissions": [
+ "events:write"
+ ],
+ "resourceConstraints": [
+ "project"
+ ],
+ "quotaClass": "events-app-publish",
+ "events": [],
+ "implementedBy": [
+ "POST /api/v1/events"
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
+{
+ "id": "events.app.read",
+ "version": "1.0",
+ "owner": "events",
+ "status": "active",
+ "visibility": "public",
+ "description": "Pull events as a developer app (ADR-014): only the app's own project's events (app.<project_key>.*, same environment as the token) plus first-party platform events with visibility public. Topic patterns must start with a literal segment; app.* patterns must name the caller's own project_key. Checkpoints are stored per app.",
+ "permissions": [
+ "events:read"
+ ],
+ "resourceConstraints": [
+ "project"
+ ],
+ "quotaClass": "events-app-read",
+ "events": [],
+ "implementedBy": [
+ "GET /api/v1/events",
+ "GET /api/v1/events/:event_id",
+ "GET /api/v1/checkpoints",
+ "PUT /api/v1/checkpoints"
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
+{
+ "id": "events.app.subscribe",
+ "version": "1.0",
+ "owner": "events",
+ "status": "active",
+ "visibility": "public",
+ "description": "Manage webhook subscriptions as a developer app (ADR-014). Topic patterns are limited to the scope of events.app.read (own project's app.<project_key>.* events in the token's environment, plus public platform events). Endpoints must be https URLs whose host resolves only to public IP addresses; OpenVibe.Events re-checks the resolved address when it delivers and never follows redirects. Deliveries are signed with the subscription secret (X-OpenVibe-Signature: sha256=<HMAC of the raw body>). Subscriptions carry project_id and env; sandbox events reach sandbox subscriptions only.",
+ "permissions": [
+ "events:subscribe"
+ ],
+ "resourceConstraints": [
+ "project"
+ ],
+ "quotaClass": "events-app-subscribe",
+ "events": [],
+ "implementedBy": [
+ "POST /api/v1/subscriptions",
+ "GET /api/v1/subscriptions",
+ "GET /api/v1/subscriptions/:id",
+ "POST /api/v1/subscriptions/:id/disable",
+ "POST /api/v1/subscriptions/:id/enable"
+ ]
+}
@@ -1,5 +1,5 @@
@@ -1,5 +1,5 @@
{
{
- "version": "0.1.0",
+ "version": "0.2.0",
"contractRanges": {
"contractRanges": {
"openvibe-contracts": ">=0.5.1 <1.0.0"
"openvibe-contracts": ">=0.5.1 <1.0.0"
},
},
@@ -17,7 +17,10 @@
@@ -17,7 +17,10 @@
"events.event.publish",
"events.event.publish",
"events.event.read",
"events.event.read",
"events.subscription.manage",
"events.subscription.manage",
- "events.delivery.admin"
+ "events.delivery.admin",
+ "events.app.publish",
+ "events.app.read",
+ "events.app.subscribe"
],
],
"eventsProduced": [],
"eventsProduced": [],
"eventsConsumed": [],
"eventsConsumed": [],
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-contracts",
"name": "openvibe-contracts",
- "version": "0.26.0",
+ "version": "0.28.0",
"description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.",
"description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.",
"main": "index.js",
"main": "index.js",
"types": "generated/typescript/index.d.ts",
"types": "generated/typescript/index.d.ts",
@@ -58,6 +58,21 @@ for (const cap of capabilities.manifests) {
@@ -58,6 +58,21 @@ for (const cap of capabilities.manifests) {
for (const k of ['inputSchema', 'outputSchema']) if (cap[k]) ok(contracts.resolve(cap[k]), `${cap.id} ${k} resolves`);
for (const k of ['inputSchema', 'outputSchema']) if (cap[k]) ok(contracts.resolve(cap[k]), `${cap.id} ${k} resolves`);
ok(cap.status !== 'active' || cap.implementedBy.length > 0, `active ${cap.id} names the route that implements it`);
ok(cap.status !== 'active' || cap.implementedBy.length > 0, `active ${cap.id} names the route that implements it`);
}
}
+// Developer-app events (v0.28, ADR-014): grantable to apps, owned by Events, project-scoped. The
+// existing service-level events.* capabilities stay internal.
+for (const id of ['events.app.publish', 'events.app.read', 'events.app.subscribe']) {
+ const c = capabilities.get(id);
+ ok(c && c.owner === 'events' && c.visibility === 'public' && c.status === 'active' && c.resourceConstraints.includes('project'), `${id} is a public, active, project-scoped Events capability`);
+}
+for (const id of ['events.event.publish', 'events.event.read', 'events.subscription.manage', 'events.delivery.admin']) {
+ ok(capabilities.get(id).visibility === 'internal', `${id} stays internal`);
+}
+{
+ // An app event keeps source and event_type inside the envelope's existing patterns.
+ const env = JSON.parse(fs.readFileSync(path.join(ROOT, 'fixtures/events.event-envelope/valid/app-event.json'), 'utf8'));
+ ok(contracts.validate('events.event-envelope@1', { ...env, source: `app-${'01JAB2C3D4E5F6G7H8J9K0MNPS'.toLowerCase()}` }).valid, 'app-<lowercased ULID> is a valid source');
+ ok(!contracts.validate('events.event-envelope@1', { ...env, source: 'app:app_01JAB2C3D4E5F6G7H8J9K0MNPS' }).valid, 'the raw app subject is not a valid source');
+}
// ── Ids ──────────────────────────────────────────────────────────────────
// ── Ids ──────────────────────────────────────────────────────────────────
for (const kind of ['user', 'guest', 'app', 'mod']) {
for (const kind of ['user', 'guest', 'app', 'mod']) {