openvibers/openvibe.contracts Commits
v0.69.0: account merge (roadmap WS-B task 5, ADR-029). Adds the event network.subject.merged (merge_id, from, into, merged_at, initiated_by person|staff, split_until; visibility internal; each service repoints its own rows from the alias to the survivor, once per merge_id, dropping and counting what it cannot move), network.account-merge-result@1 (what Network moved as counts, replayed for a retry) and staff map 1.2.0 with staff.identity.merge (owner, since 1.2.0: account-recovery merges with a reason and an audit row). Network's manifest lists the event.
Changes
@@@ -6,6 +6,13 @@ the notes were in the tag and commit messages (`git tag -n1`).## Unreleased++## 0.69.0 — 2026-09-27++++**Account merge** (roadmap WS-B task 5, ADR-029).++- **`network.subject.merged@1`** (event, first-party, visibility internal): `{ merge_id, from, into, merged_at, initiated_by, split_until? }`. `from` is now an alias of `into`. Network has moved what it owns (providers, sessions, OAuth grants, developer projects, OpenCoins, user modules) in the transaction that queues it. Each service repoints its own rows from `from` to `into`; a row it cannot move keeps the survivor's and drops the other, counted. Apply once per `merge_id`. Listed in Network's manifest `eventsProduced`.++- **`network.account-merge-result@1`**: the answer of a merge, what Network moved as counts, and `replayed` for a retried merge.++- **Staff map 1.2.0: `staff.identity.merge`** (owner, since 1.2.0): an account-recovery merge with a written reason and an audit row. People merge their own accounts without it, signed in to both. An owner token issued from map 1.1.0 holds it by role.++## 0.68.0 — 2026-09-26**Creator analytics from events** (roadmap WS-E task 6).@@@ -2406,6 +2406,16 @@"compatibility": "backward","status": "active"},++ {++ "id": "network.subject.merged",++ "version": "1.0.0",++ "owner": "network",++ "schema": "events/payloads/network.subject.merged.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active",++ "adr": "ADR-029"++ },{"id": "network.follow.created","version": "1.0.0",@@@ -2545,6 +2555,16 @@"status": "active","adr": "ADR-021"},++ {++ "id": "network.account-merge-result",++ "version": "1.0.0",++ "owner": "network",++ "schema": "network/account-merge-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active",++ "adr": "ADR-029"++ },{"id": "network.follow-list-result","version": "1.0.0",nnew file mode 100644@@@ -0,0 +1,17 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/network.subject.merged.v1.json",++ "title": "NetworkSubjectMergedPayload",++ "description": "network.subject.merged v1 (OpenVibe.Network; roadmap WS-B task 5, ADR-029). Two accounts became one: `from`, the folded-in subject, is now an alias of `into`, the survivor, which keeps its subject, username, profile and settings. Network has already moved what it owns (linked providers, sessions, OAuth grants, developer projects, OpenCoins and user modules) in the transaction that writes this event to its outbox. Every service that stores subjects repoints its own rows from `from` to `into` in its own transaction and keeps no copy under `from`; a row it cannot move (a unique conflict, such as both accounts following the same channel) keeps the survivor's row and drops the other, as a counted, logged outcome. Until a service has applied it, its rows stay under `from`, which still resolves to `into` through Network's aliases. Apply once per merge_id. Envelope: subject { type: user, id: <into> }, visibility internal, actor the person who merged (or the staff member, for an account-recovery merge).",++ "type": "object",++ "required": ["merge_id", "from", "into", "merged_at", "initiated_by"],++ "additionalProperties": false,++ "properties": {++ "merge_id": { "type": "string", "pattern": "^mrg_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The merge; a redelivery carries the same id." },++ "from": { "type": "string", "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The folded-in subject, now an alias." },++ "into": { "type": "string", "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The survivor." },++ "merged_at": { "type": "string", "format": "date-time" },++ "initiated_by": { "enum": ["person", "staff"], "description": "person: the account holder, signed in to both accounts. staff: an account-recovery merge (staff.identity.merge, with a written reason and an audit row)." },++ "split_until": { "type": "string", "format": "date-time", "description": "Until then Network keeps the folded-in account's pre-merge state, so staff can split a mistaken merge by hand (30 days)." }++ }++}nnew file mode 100644@@@ -0,0 +1,30 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/network/account-merge-result.v1.json",++ "title": "NetworkAccountMergeResult",++ "description": "network.account-merge-result@1 (OpenVibe.Network; roadmap WS-B task 5, ADR-029): the answer of a merge, POST /api/v1/account/merge (the person, signed in to the survivor, with a fresh sign-in proof of the other account) or POST /api/admin/account-merges (staff.identity.merge, with a reason). What Network moved in its one transaction, as counts; everything else follows network.subject.merged. A retried merge of the same pair answers the first merge (replayed: true) and moves nothing again.",++ "type": "object",++ "required": ["merge_id", "from", "into", "merged_at", "split_until", "moved", "replayed"],++ "additionalProperties": false,++ "properties": {++ "merge_id": { "type": "string", "pattern": "^mrg_[0-9A-HJKMNP-TV-Z]{26}$" },++ "from": { "type": "string", "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$" },++ "into": { "type": "string", "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$" },++ "merged_at": { "type": "string", "format": "date-time" },++ "split_until": { "type": "string", "format": "date-time" },++ "moved": {++ "type": "object",++ "additionalProperties": false,++ "required": ["providers", "sessions", "oauth_grants", "projects", "coins", "modules"],++ "properties": {++ "providers": { "type": "integer", "minimum": 0, "description": "Linked sign-in providers now on the survivor." },++ "sessions": { "type": "integer", "minimum": 0, "description": "Sessions and devices moved; both sets stay valid until they expire." },++ "oauth_grants": { "type": "integer", "minimum": 0 },++ "projects": { "type": "integer", "minimum": 0, "description": "Developer projects, still owned, now by the survivor." },++ "coins": { "type": "integer", "minimum": 0, "description": "OpenCoins added to the survivor (one ledger entry per side, reason account_merge)." },++ "modules": { "type": "integer", "minimum": 0, "description": "User-module records changed on the survivor (its own fields win)." }++ }++ },++ "replayed": { "type": "boolean" }++ }++}nnew file mode 100644@@@ -0,0 +1 @@++{"merge_id":"mrg_01JAB2C3D4E5F6G7H8J9K0MNPS","from":"usr_01JAB2C3D4E5F6G7H8J9K0MNPR","into":"usr_01JAB2C3D4E5F6G7H8J9K0MNPQ","merged_at":"2026-09-27T21:00:00.000Z","split_until":"2026-10-27T21:00:00.000Z","moved":{"providers":2,"sessions":3,"oauth_grants":1,"projects":0,"coins":-5,"modules":2},"replayed":false}nnew file mode 100644@@@ -0,0 +1 @@++{"merge_id":"mrg_01JAB2C3D4E5F6G7H8J9K0MNPS","from":"usr_01JAB2C3D4E5F6G7H8J9K0MNPR","into":"usr_01JAB2C3D4E5F6G7H8J9K0MNPQ","merged_at":"2026-09-27T21:00:00.000Z","split_until":"2026-10-27T21:00:00.000Z","moved":{"providers":2,"sessions":3,"oauth_grants":1,"projects":0,"coins":150,"modules":2},"replayed":false}nnew file mode 100644@@@ -0,0 +1 @@++{"merge_id":"mrg_01JAB2C3D4E5F6G7H8J9K0MNPS","from":"usr_01JAB2C3D4E5F6G7H8J9K0MNPR","into":"usr_01JAB2C3D4E5F6G7H8J9K0MNPQ","merged_at":"2026-09-27T21:00:00.000Z","initiated_by":"person","username":"bob"}nnew file mode 100644@@@ -0,0 +1 @@++{"merge_id":"mrg_01JAB2C3D4E5F6G7H8J9K0MNPS","from":"42","into":"usr_01JAB2C3D4E5F6G7H8J9K0MNPQ","merged_at":"2026-09-27T21:00:00.000Z","initiated_by":"person"}nnew file mode 100644@@@ -0,0 +1 @@++{"merge_id":"mrg_01JAB2C3D4E5F6G7H8J9K0MNPS","from":"usr_01JAB2C3D4E5F6G7H8J9K0MNPR","into":"usr_01JAB2C3D4E5F6G7H8J9K0MNPQ","merged_at":"2026-09-27T21:00:00.000Z","initiated_by":"person","split_until":"2026-10-27T21:00:00.000Z"}nnew file mode 100644@@@ -0,0 +1 @@++{"merge_id":"mrg_01JAB2C3D4E5F6G7H8J9K0MNPS","from":"usr_01JAB2C3D4E5F6G7H8J9K0MNPR","into":"usr_01JAB2C3D4E5F6G7H8J9K0MNPQ","merged_at":"2026-09-27T21:00:00.000Z","initiated_by":"staff"}@@@ -25379,6 +25379,59 @@}}},++ "network.subject.merged": {++ "version": "1.0.0",++ "owner": "network",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/network.subject.merged.v1.json",++ "title": "NetworkSubjectMergedPayload",++ "description": "network.subject.merged v1 (OpenVibe.Network; roadmap WS-B task 5, ADR-029). Two accounts became one: `from`, the folded-in subject, is now an alias of `into`, the survivor, which keeps its subject, username, profile and settings. Network has already moved what it owns (linked providers, sessions, OAuth grants, developer projects, OpenCoins and user modules) in the transaction that writes this event to its outbox. Every service that stores subjects repoints its own rows from `from` to `into` in its own transaction and keeps no copy under `from`; a row it cannot move (a unique conflict, such as both accounts following the same channel) keeps the survivor's row and drops the other, as a counted, logged outcome. Until a service has applied it, its rows stay under `from`, which still resolves to `into` through Network's aliases. Apply once per merge_id. Envelope: subject { type: user, id: <into> }, visibility internal, actor the person who merged (or the staff member, for an account-recovery merge).",++ "type": "object",++ "required": [++ "merge_id",++ "from",++ "into",++ "merged_at",++ "initiated_by"++ ],++ "additionalProperties": false,++ "properties": {++ "merge_id": {++ "type": "string",++ "pattern": "^mrg_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The merge; a redelivery carries the same id."++ },++ "from": {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The folded-in subject, now an alias."++ },++ "into": {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The survivor."++ },++ "merged_at": {++ "type": "string",++ "format": "date-time"++ },++ "initiated_by": {++ "enum": [++ "person",++ "staff"++ ],++ "description": "person: the account holder, signed in to both accounts. staff: an account-recovery merge (staff.identity.merge, with a written reason and an audit row)."++ },++ "split_until": {++ "type": "string",++ "format": "date-time",++ "description": "Until then Network keeps the folded-in account's pre-merge state, so staff can split a mistaken merge by hand (30 days)."++ }++ }++ }++ },"network.follow.created": {"version": "1.0.0","owner": "network",@@@ -26485,6 +26538,96 @@}}},++ "network.account-merge-result": {++ "version": "1.0.0",++ "owner": "network",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/network/account-merge-result.v1.json",++ "title": "NetworkAccountMergeResult",++ "description": "network.account-merge-result@1 (OpenVibe.Network; roadmap WS-B task 5, ADR-029): the answer of a merge, POST /api/v1/account/merge (the person, signed in to the survivor, with a fresh sign-in proof of the other account) or POST /api/admin/account-merges (staff.identity.merge, with a reason). What Network moved in its one transaction, as counts; everything else follows network.subject.merged. A retried merge of the same pair answers the first merge (replayed: true) and moves nothing again.",++ "type": "object",++ "required": [++ "merge_id",++ "from",++ "into",++ "merged_at",++ "split_until",++ "moved",++ "replayed"++ ],++ "additionalProperties": false,++ "properties": {++ "merge_id": {++ "type": "string",++ "pattern": "^mrg_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "from": {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "into": {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "merged_at": {++ "type": "string",++ "format": "date-time"++ },++ "split_until": {++ "type": "string",++ "format": "date-time"++ },++ "moved": {++ "type": "object",++ "additionalProperties": false,++ "required": [++ "providers",++ "sessions",++ "oauth_grants",++ "projects",++ "coins",++ "modules"++ ],++ "properties": {++ "providers": {++ "type": "integer",++ "minimum": 0,++ "description": "Linked sign-in providers now on the survivor."++ },++ "sessions": {++ "type": "integer",++ "minimum": 0,++ "description": "Sessions and devices moved; both sets stay valid until they expire."++ },++ "oauth_grants": {++ "type": "integer",++ "minimum": 0++ },++ "projects": {++ "type": "integer",++ "minimum": 0,++ "description": "Developer projects, still owned, now by the survivor."++ },++ "coins": {++ "type": "integer",++ "minimum": 0,++ "description": "OpenCoins added to the survivor (one ledger entry per side, reason account_merge)."++ },++ "modules": {++ "type": "integer",++ "minimum": 0,++ "description": "User-module records changed on the survivor (its own fields win)."++ }++ }++ },++ "replayed": {++ "type": "boolean"++ }++ }++ }++ },"network.follow-list-result": {"version": "1.0.0","owner": "network",@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.AI API",-- "version": "0.68.0",-- "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.69.0",++ "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Billing API",-- "version": "0.68.0",-- "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.69.0",++ "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Blog API",-- "version": "0.68.0",-- "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.69.0",++ "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Chat API",-- "version": "0.68.0",-- "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.69.0",++ "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Codes API",-- "version": "0.68.0",-- "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.69.0",++ "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Community API",-- "version": "0.68.0",-- "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.69.0",++ "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Coupons API",-- "version": "0.68.0",-- "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.69.0",++ "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Deals API",-- "version": "0.68.0",-- "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.69.0",++ "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Events API",-- "version": "0.68.0",-- "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.69.0",++ "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Games API",-- "version": "0.68.0",-- "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.69.0",++ "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Host API",-- "version": "0.68.0",-- "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.69.0",++ "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Live API",-- "version": "0.68.0",-- "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.69.0",++ "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Media API",-- "version": "0.68.0",-- "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.69.0",++ "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Network API",-- "version": "0.68.0",-- "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.69.0",++ "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.News API",-- "version": "0.68.0",-- "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.69.0",++ "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenRe.Stream API",-- "version": "0.68.0",-- "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.69.0",++ "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Reviews API",-- "version": "0.68.0",-- "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.69.0",++ "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Search API",-- "version": "0.68.0",-- "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.69.0",++ "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Sources API",-- "version": "0.68.0",-- "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.69.0",++ "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Tips API",-- "version": "0.68.0",-- "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.69.0",++ "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Tools API",-- "version": "0.68.0",-- "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.69.0",++ "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Trade API",-- "version": "0.68.0",-- "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.69.0",++ "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.VIP API",-- "version": "0.68.0",-- "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.69.0",++ "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Wiki API",-- "version": "0.68.0",-- "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.69.0",++ "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -28636,6 +28636,34 @@ export interface NetworkPrincipalGrantChangedPayload {actor_subject?: string | null;}++/** [email protected] (owner: network) */++/**++ * network.subject.merged v1 (OpenVibe.Network; roadmap WS-B task 5, ADR-029). Two accounts became one: `from`, the folded-in subject, is now an alias of `into`, the survivor, which keeps its subject, username, profile and settings. Network has already moved what it owns (linked providers, sessions, OAuth grants, developer projects, OpenCoins and user modules) in the transaction that writes this event to its outbox. Every service that stores subjects repoints its own rows from `from` to `into` in its own transaction and keeps no copy under `from`; a row it cannot move (a unique conflict, such as both accounts following the same channel) keeps the survivor's row and drops the other, as a counted, logged outcome. Until a service has applied it, its rows stay under `from`, which still resolves to `into` through Network's aliases. Apply once per merge_id. Envelope: subject { type: user, id: <into> }, visibility internal, actor the person who merged (or the staff member, for an account-recovery merge).++ */++export interface NetworkSubjectMergedPayload {++ /**++ * The merge; a redelivery carries the same id.++ */++ merge_id: string;++ /**++ * The folded-in subject, now an alias.++ */++ from: string;++ /**++ * The survivor.++ */++ into: string;++ merged_at: string;++ /**++ * person: the account holder, signed in to both accounts. staff: an account-recovery merge (staff.identity.merge, with a written reason and an audit row).++ */++ initiated_by: "person" | "staff";++ /**++ * Until then Network keeps the folded-in account's pre-merge state, so staff can split a mistaken merge by hand (30 days).++ */++ split_until?: string;++}++/** [email protected] (owner: network) *//*** network.follow.created v1 (OpenVibe.Network; roadmap WS-E task 4, ADR-030). A person followed something: today a Live channel, whose target id is the channel owner's subject. Follows are the network's, kept by Network and keyed by subjects; products such as Live keep a projection they can rebuild. Written to Network's outbox in the transaction that stores the follow. A consumer applies the latest revision per (follower, target) and ignores an older one. Following again what is already followed changes nothing and sends nothing. Envelope: subject { type: user, id: <follower> }, visibility subject, actor the follower.@@@ -29101,6 +29129,42 @@ export interface NetworkCreatorAnalyticsResult {}[];}++/** [email protected] (owner: network) */++/**++ * network.account-merge-result@1 (OpenVibe.Network; roadmap WS-B task 5, ADR-029): the answer of a merge, POST /api/v1/account/merge (the person, signed in to the survivor, with a fresh sign-in proof of the other account) or POST /api/admin/account-merges (staff.identity.merge, with a reason). What Network moved in its one transaction, as counts; everything else follows network.subject.merged. A retried merge of the same pair answers the first merge (replayed: true) and moves nothing again.++ */++export interface NetworkAccountMergeResult {++ merge_id: string;++ from: string;++ into: string;++ merged_at: string;++ split_until: string;++ moved: {++ /**++ * Linked sign-in providers now on the survivor.++ */++ providers: number;++ /**++ * Sessions and devices moved; both sets stay valid until they expire.++ */++ sessions: number;++ oauth_grants: number;++ /**++ * Developer projects, still owned, now by the survivor.++ */++ projects: number;++ /**++ * OpenCoins added to the survivor (one ledger entry per side, reason account_merge).++ */++ coins: number;++ /**++ * User-module records changed on the survivor (its own fields win).++ */++ modules: number;++ };++ replayed: boolean;++}++/** [email protected] (owner: network) *//*** network.follow-list-result@1 (ADR-030): a page of follows on OpenVibe.Network, newest first. GET /api/v1/me/follows (what the caller follows), GET /api/v1/follows/:type/:target/followers (who follows a target: the target's owner, or a service with network.follows.read). Lists are never public; counts are.@@@ -1,5 +1,5 @@{-- "version": "1.1.0",++ "version": "1.2.0","roles": [{"id": "user",@@@ -457,6 +457,17 @@]}},++ {++ "id": "staff.identity.merge",++ "minRole": "owner",++ "since": "1.2.0",++ "description": "Merge two accounts for account recovery (ADR-029), with a written reason and an audit row; never silently. People merge their own accounts without it, signed in to both.",++ "gates": {++ "network": [++ "identity/account-merge.js (POST /api/admin/account-merges)"++ ]++ }++ },{"id": "staff.roles.grant_admin","minRole": "owner",@@@ -59,6 +59,7 @@"network.credential.rotated","network.follow.created","network.follow.deleted",++ "network.subject.merged","network.grant.changed","network.module.updated","network.notification.created",@@@ -1,6 +1,6 @@{"name": "openvibe-contracts",-- "version": "0.68.0",++ "version": "0.69.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",@@@ -398,6 +398,10 @@ for (const id of ['events.event.publish', 'events.event.read', 'events.subscriptok(!staff.can({ ...oldAdmin, staff_map: '1.1.0' }, 'staff.editorial.manage'), 'a token from map 1.1.0 holds only what was issued');ok(!staff.can({ role: 'global_mod', staff_caps: ['staff.moderation.chat'] }, 'staff.editorial.manage') && staff.can({ role: 'global_mod', staff_caps: ['staff.moderation.chat'] }, 'staff.content.moderate'), 'the role decides, never more than it holds');ok(!staff.can({ role: 'admin', staff_caps: ['staff.moderation.chat'] }, 'staff.users.manage'), 'capabilities from map 1.0.0 still come only from what was issued');++ // Map 1.2.0 (v0.69.0, ADR-029): account-recovery merges are the owner's alone, and reach an owner's older token by role.++ ok(staff.get('staff.identity.merge').since === '1.2.0' && staff.get('staff.identity.merge').minRole === 'owner', 'staff.identity.merge: owner, since 1.2.0');++ ok(staff.can({ role: 'admin', is_owner: true, staff_caps: [], staff_map: '1.1.0' }, 'staff.identity.merge'), 'an owner token (admin + is_owner) from map 1.1.0 merges by role');++ ok(!staff.can({ role: 'admin', staff_caps: staff.capabilitiesOf('admin'), staff_map: '1.2.0' }, 'staff.identity.merge'), 'an admin never merges accounts');ok(map.rules.some(x => /never delegated/.test(x)) && map.rules.some(x => /equal or higher/.test(x)) && map.rules.some(x => /owner's account/.test(x)), 'rules: no delegation, rank protection, owner protection');const adr = fs.readFileSync(path.join(ROOT, 'docs/adr/ADR-022-moderation-console.md'), 'utf8');ok(/manifests\/policy\/staff-roles\.json/.test(adr), 'ADR-022 points at the staff map');
@@ -6,6 +6,13 @@ the notes were in the tag and commit messages (`git tag -n1`).
@@ -6,6 +6,13 @@ the notes were in the tag and commit messages (`git tag -n1`).
## Unreleased
## Unreleased
+## 0.69.0 — 2026-09-27
+
+**Account merge** (roadmap WS-B task 5, ADR-029).
+- **`network.subject.merged@1`** (event, first-party, visibility internal): `{ merge_id, from, into, merged_at, initiated_by, split_until? }`. `from` is now an alias of `into`. Network has moved what it owns (providers, sessions, OAuth grants, developer projects, OpenCoins, user modules) in the transaction that queues it. Each service repoints its own rows from `from` to `into`; a row it cannot move keeps the survivor's and drops the other, counted. Apply once per `merge_id`. Listed in Network's manifest `eventsProduced`.
+- **`network.account-merge-result@1`**: the answer of a merge, what Network moved as counts, and `replayed` for a retried merge.
+- **Staff map 1.2.0: `staff.identity.merge`** (owner, since 1.2.0): an account-recovery merge with a written reason and an audit row. People merge their own accounts without it, signed in to both. An owner token issued from map 1.1.0 holds it by role.
+
## 0.68.0 — 2026-09-26
## 0.68.0 — 2026-09-26
**Creator analytics from events** (roadmap WS-E task 6).
**Creator analytics from events** (roadmap WS-E task 6).
@@ -2406,6 +2406,16 @@
@@ -2406,6 +2406,16 @@
"compatibility": "backward",
"compatibility": "backward",
"status": "active"
"status": "active"
},
},
+ {
+ "id": "network.subject.merged",
+ "version": "1.0.0",
+ "owner": "network",
+ "schema": "events/payloads/network.subject.merged.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active",
+ "adr": "ADR-029"
+ },
{
{
"id": "network.follow.created",
"id": "network.follow.created",
"version": "1.0.0",
"version": "1.0.0",
@@ -2545,6 +2555,16 @@
@@ -2545,6 +2555,16 @@
"status": "active",
"status": "active",
"adr": "ADR-021"
"adr": "ADR-021"
},
},
+ {
+ "id": "network.account-merge-result",
+ "version": "1.0.0",
+ "owner": "network",
+ "schema": "network/account-merge-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active",
+ "adr": "ADR-029"
+ },
{
{
"id": "network.follow-list-result",
"id": "network.follow-list-result",
"version": "1.0.0",
"version": "1.0.0",
new file mode 100644
new file mode 100644
@@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/network.subject.merged.v1.json",
+ "title": "NetworkSubjectMergedPayload",
+ "description": "network.subject.merged v1 (OpenVibe.Network; roadmap WS-B task 5, ADR-029). Two accounts became one: `from`, the folded-in subject, is now an alias of `into`, the survivor, which keeps its subject, username, profile and settings. Network has already moved what it owns (linked providers, sessions, OAuth grants, developer projects, OpenCoins and user modules) in the transaction that writes this event to its outbox. Every service that stores subjects repoints its own rows from `from` to `into` in its own transaction and keeps no copy under `from`; a row it cannot move (a unique conflict, such as both accounts following the same channel) keeps the survivor's row and drops the other, as a counted, logged outcome. Until a service has applied it, its rows stay under `from`, which still resolves to `into` through Network's aliases. Apply once per merge_id. Envelope: subject { type: user, id: <into> }, visibility internal, actor the person who merged (or the staff member, for an account-recovery merge).",
+ "type": "object",
+ "required": ["merge_id", "from", "into", "merged_at", "initiated_by"],
+ "additionalProperties": false,
+ "properties": {
+ "merge_id": { "type": "string", "pattern": "^mrg_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The merge; a redelivery carries the same id." },
+ "from": { "type": "string", "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The folded-in subject, now an alias." },
+ "into": { "type": "string", "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The survivor." },
+ "merged_at": { "type": "string", "format": "date-time" },
+ "initiated_by": { "enum": ["person", "staff"], "description": "person: the account holder, signed in to both accounts. staff: an account-recovery merge (staff.identity.merge, with a written reason and an audit row)." },
+ "split_until": { "type": "string", "format": "date-time", "description": "Until then Network keeps the folded-in account's pre-merge state, so staff can split a mistaken merge by hand (30 days)." }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,30 @@
@@ -0,0 +1,30 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/network/account-merge-result.v1.json",
+ "title": "NetworkAccountMergeResult",
+ "description": "network.account-merge-result@1 (OpenVibe.Network; roadmap WS-B task 5, ADR-029): the answer of a merge, POST /api/v1/account/merge (the person, signed in to the survivor, with a fresh sign-in proof of the other account) or POST /api/admin/account-merges (staff.identity.merge, with a reason). What Network moved in its one transaction, as counts; everything else follows network.subject.merged. A retried merge of the same pair answers the first merge (replayed: true) and moves nothing again.",
+ "type": "object",
+ "required": ["merge_id", "from", "into", "merged_at", "split_until", "moved", "replayed"],
+ "additionalProperties": false,
+ "properties": {
+ "merge_id": { "type": "string", "pattern": "^mrg_[0-9A-HJKMNP-TV-Z]{26}$" },
+ "from": { "type": "string", "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$" },
+ "into": { "type": "string", "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$" },
+ "merged_at": { "type": "string", "format": "date-time" },
+ "split_until": { "type": "string", "format": "date-time" },
+ "moved": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["providers", "sessions", "oauth_grants", "projects", "coins", "modules"],
+ "properties": {
+ "providers": { "type": "integer", "minimum": 0, "description": "Linked sign-in providers now on the survivor." },
+ "sessions": { "type": "integer", "minimum": 0, "description": "Sessions and devices moved; both sets stay valid until they expire." },
+ "oauth_grants": { "type": "integer", "minimum": 0 },
+ "projects": { "type": "integer", "minimum": 0, "description": "Developer projects, still owned, now by the survivor." },
+ "coins": { "type": "integer", "minimum": 0, "description": "OpenCoins added to the survivor (one ledger entry per side, reason account_merge)." },
+ "modules": { "type": "integer", "minimum": 0, "description": "User-module records changed on the survivor (its own fields win)." }
+ }
+ },
+ "replayed": { "type": "boolean" }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{"merge_id":"mrg_01JAB2C3D4E5F6G7H8J9K0MNPS","from":"usr_01JAB2C3D4E5F6G7H8J9K0MNPR","into":"usr_01JAB2C3D4E5F6G7H8J9K0MNPQ","merged_at":"2026-09-27T21:00:00.000Z","split_until":"2026-10-27T21:00:00.000Z","moved":{"providers":2,"sessions":3,"oauth_grants":1,"projects":0,"coins":-5,"modules":2},"replayed":false}
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{"merge_id":"mrg_01JAB2C3D4E5F6G7H8J9K0MNPS","from":"usr_01JAB2C3D4E5F6G7H8J9K0MNPR","into":"usr_01JAB2C3D4E5F6G7H8J9K0MNPQ","merged_at":"2026-09-27T21:00:00.000Z","split_until":"2026-10-27T21:00:00.000Z","moved":{"providers":2,"sessions":3,"oauth_grants":1,"projects":0,"coins":150,"modules":2},"replayed":false}
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{"merge_id":"mrg_01JAB2C3D4E5F6G7H8J9K0MNPS","from":"usr_01JAB2C3D4E5F6G7H8J9K0MNPR","into":"usr_01JAB2C3D4E5F6G7H8J9K0MNPQ","merged_at":"2026-09-27T21:00:00.000Z","initiated_by":"person","username":"bob"}
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{"merge_id":"mrg_01JAB2C3D4E5F6G7H8J9K0MNPS","from":"42","into":"usr_01JAB2C3D4E5F6G7H8J9K0MNPQ","merged_at":"2026-09-27T21:00:00.000Z","initiated_by":"person"}
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{"merge_id":"mrg_01JAB2C3D4E5F6G7H8J9K0MNPS","from":"usr_01JAB2C3D4E5F6G7H8J9K0MNPR","into":"usr_01JAB2C3D4E5F6G7H8J9K0MNPQ","merged_at":"2026-09-27T21:00:00.000Z","initiated_by":"person","split_until":"2026-10-27T21:00:00.000Z"}
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{"merge_id":"mrg_01JAB2C3D4E5F6G7H8J9K0MNPS","from":"usr_01JAB2C3D4E5F6G7H8J9K0MNPR","into":"usr_01JAB2C3D4E5F6G7H8J9K0MNPQ","merged_at":"2026-09-27T21:00:00.000Z","initiated_by":"staff"}
@@ -25379,6 +25379,59 @@
@@ -25379,6 +25379,59 @@
}
}
}
}
},
},
+ "network.subject.merged": {
+ "version": "1.0.0",
+ "owner": "network",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/network.subject.merged.v1.json",
+ "title": "NetworkSubjectMergedPayload",
+ "description": "network.subject.merged v1 (OpenVibe.Network; roadmap WS-B task 5, ADR-029). Two accounts became one: `from`, the folded-in subject, is now an alias of `into`, the survivor, which keeps its subject, username, profile and settings. Network has already moved what it owns (linked providers, sessions, OAuth grants, developer projects, OpenCoins and user modules) in the transaction that writes this event to its outbox. Every service that stores subjects repoints its own rows from `from` to `into` in its own transaction and keeps no copy under `from`; a row it cannot move (a unique conflict, such as both accounts following the same channel) keeps the survivor's row and drops the other, as a counted, logged outcome. Until a service has applied it, its rows stay under `from`, which still resolves to `into` through Network's aliases. Apply once per merge_id. Envelope: subject { type: user, id: <into> }, visibility internal, actor the person who merged (or the staff member, for an account-recovery merge).",
+ "type": "object",
+ "required": [
+ "merge_id",
+ "from",
+ "into",
+ "merged_at",
+ "initiated_by"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "merge_id": {
+ "type": "string",
+ "pattern": "^mrg_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The merge; a redelivery carries the same id."
+ },
+ "from": {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The folded-in subject, now an alias."
+ },
+ "into": {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The survivor."
+ },
+ "merged_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "initiated_by": {
+ "enum": [
+ "person",
+ "staff"
+ ],
+ "description": "person: the account holder, signed in to both accounts. staff: an account-recovery merge (staff.identity.merge, with a written reason and an audit row)."
+ },
+ "split_until": {
+ "type": "string",
+ "format": "date-time",
+ "description": "Until then Network keeps the folded-in account's pre-merge state, so staff can split a mistaken merge by hand (30 days)."
+ }
+ }
+ }
+ },
"network.follow.created": {
"network.follow.created": {
"version": "1.0.0",
"version": "1.0.0",
"owner": "network",
"owner": "network",
@@ -26485,6 +26538,96 @@
@@ -26485,6 +26538,96 @@
}
}
}
}
},
},
+ "network.account-merge-result": {
+ "version": "1.0.0",
+ "owner": "network",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/network/account-merge-result.v1.json",
+ "title": "NetworkAccountMergeResult",
+ "description": "network.account-merge-result@1 (OpenVibe.Network; roadmap WS-B task 5, ADR-029): the answer of a merge, POST /api/v1/account/merge (the person, signed in to the survivor, with a fresh sign-in proof of the other account) or POST /api/admin/account-merges (staff.identity.merge, with a reason). What Network moved in its one transaction, as counts; everything else follows network.subject.merged. A retried merge of the same pair answers the first merge (replayed: true) and moves nothing again.",
+ "type": "object",
+ "required": [
+ "merge_id",
+ "from",
+ "into",
+ "merged_at",
+ "split_until",
+ "moved",
+ "replayed"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "merge_id": {
+ "type": "string",
+ "pattern": "^mrg_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "from": {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "into": {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "merged_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "split_until": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "moved": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "providers",
+ "sessions",
+ "oauth_grants",
+ "projects",
+ "coins",
+ "modules"
+ ],
+ "properties": {
+ "providers": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Linked sign-in providers now on the survivor."
+ },
+ "sessions": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Sessions and devices moved; both sets stay valid until they expire."
+ },
+ "oauth_grants": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "projects": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Developer projects, still owned, now by the survivor."
+ },
+ "coins": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "OpenCoins added to the survivor (one ledger entry per side, reason account_merge)."
+ },
+ "modules": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "User-module records changed on the survivor (its own fields win)."
+ }
+ }
+ },
+ "replayed": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
"network.follow-list-result": {
"network.follow-list-result": {
"version": "1.0.0",
"version": "1.0.0",
"owner": "network",
"owner": "network",
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.AI API",
"title": "OpenVibe.AI API",
- "version": "0.68.0",
- "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.69.0",
+ "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Billing API",
"title": "OpenVibe.Billing API",
- "version": "0.68.0",
- "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.69.0",
+ "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Blog API",
"title": "OpenVibe.Blog API",
- "version": "0.68.0",
- "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.69.0",
+ "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Chat API",
"title": "OpenVibe.Chat API",
- "version": "0.68.0",
- "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.69.0",
+ "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Codes API",
"title": "OpenVibe.Codes API",
- "version": "0.68.0",
- "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.69.0",
+ "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Community API",
"title": "OpenVibe.Community API",
- "version": "0.68.0",
- "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.69.0",
+ "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Coupons API",
"title": "OpenVibe.Coupons API",
- "version": "0.68.0",
- "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.69.0",
+ "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Deals API",
"title": "OpenVibe.Deals API",
- "version": "0.68.0",
- "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.69.0",
+ "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Events API",
"title": "OpenVibe.Events API",
- "version": "0.68.0",
- "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.69.0",
+ "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Games API",
"title": "OpenVibe.Games API",
- "version": "0.68.0",
- "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.69.0",
+ "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Host API",
"title": "OpenVibe.Host API",
- "version": "0.68.0",
- "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.69.0",
+ "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Live API",
"title": "OpenVibe.Live API",
- "version": "0.68.0",
- "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.69.0",
+ "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Media API",
"title": "OpenVibe.Media API",
- "version": "0.68.0",
- "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.69.0",
+ "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Network API",
"title": "OpenVibe.Network API",
- "version": "0.68.0",
- "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.69.0",
+ "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.News API",
"title": "OpenVibe.News API",
- "version": "0.68.0",
- "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.69.0",
+ "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenRe.Stream API",
"title": "OpenRe.Stream API",
- "version": "0.68.0",
- "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.69.0",
+ "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Reviews API",
"title": "OpenVibe.Reviews API",
- "version": "0.68.0",
- "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.69.0",
+ "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Search API",
"title": "OpenVibe.Search API",
- "version": "0.68.0",
- "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.69.0",
+ "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Sources API",
"title": "OpenVibe.Sources API",
- "version": "0.68.0",
- "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.69.0",
+ "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Tips API",
"title": "OpenVibe.Tips API",
- "version": "0.68.0",
- "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.69.0",
+ "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Tools API",
"title": "OpenVibe.Tools API",
- "version": "0.68.0",
- "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.69.0",
+ "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Trade API",
"title": "OpenVibe.Trade API",
- "version": "0.68.0",
- "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.69.0",
+ "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.VIP API",
"title": "OpenVibe.VIP API",
- "version": "0.68.0",
- "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.69.0",
+ "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Wiki API",
"title": "OpenVibe.Wiki API",
- "version": "0.68.0",
- "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.69.0",
+ "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.69.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -28636,6 +28636,34 @@ export interface NetworkPrincipalGrantChangedPayload {
@@ -28636,6 +28636,34 @@ export interface NetworkPrincipalGrantChangedPayload {
actor_subject?: string | null;
actor_subject?: string | null;
}
}
+/** [email protected] (owner: network) */
+/**
+ * network.subject.merged v1 (OpenVibe.Network; roadmap WS-B task 5, ADR-029). Two accounts became one: `from`, the folded-in subject, is now an alias of `into`, the survivor, which keeps its subject, username, profile and settings. Network has already moved what it owns (linked providers, sessions, OAuth grants, developer projects, OpenCoins and user modules) in the transaction that writes this event to its outbox. Every service that stores subjects repoints its own rows from `from` to `into` in its own transaction and keeps no copy under `from`; a row it cannot move (a unique conflict, such as both accounts following the same channel) keeps the survivor's row and drops the other, as a counted, logged outcome. Until a service has applied it, its rows stay under `from`, which still resolves to `into` through Network's aliases. Apply once per merge_id. Envelope: subject { type: user, id: <into> }, visibility internal, actor the person who merged (or the staff member, for an account-recovery merge).
+ */
+export interface NetworkSubjectMergedPayload {
+ /**
+ * The merge; a redelivery carries the same id.
+ */
+ merge_id: string;
+ /**
+ * The folded-in subject, now an alias.
+ */
+ from: string;
+ /**
+ * The survivor.
+ */
+ into: string;
+ merged_at: string;
+ /**
+ * person: the account holder, signed in to both accounts. staff: an account-recovery merge (staff.identity.merge, with a written reason and an audit row).
+ */
+ initiated_by: "person" | "staff";
+ /**
+ * Until then Network keeps the folded-in account's pre-merge state, so staff can split a mistaken merge by hand (30 days).
+ */
+ split_until?: string;
+}
+
/** [email protected] (owner: network) */
/** [email protected] (owner: network) */
/**
/**
* network.follow.created v1 (OpenVibe.Network; roadmap WS-E task 4, ADR-030). A person followed something: today a Live channel, whose target id is the channel owner's subject. Follows are the network's, kept by Network and keyed by subjects; products such as Live keep a projection they can rebuild. Written to Network's outbox in the transaction that stores the follow. A consumer applies the latest revision per (follower, target) and ignores an older one. Following again what is already followed changes nothing and sends nothing. Envelope: subject { type: user, id: <follower> }, visibility subject, actor the follower.
* network.follow.created v1 (OpenVibe.Network; roadmap WS-E task 4, ADR-030). A person followed something: today a Live channel, whose target id is the channel owner's subject. Follows are the network's, kept by Network and keyed by subjects; products such as Live keep a projection they can rebuild. Written to Network's outbox in the transaction that stores the follow. A consumer applies the latest revision per (follower, target) and ignores an older one. Following again what is already followed changes nothing and sends nothing. Envelope: subject { type: user, id: <follower> }, visibility subject, actor the follower.
@@ -29101,6 +29129,42 @@ export interface NetworkCreatorAnalyticsResult {
@@ -29101,6 +29129,42 @@ export interface NetworkCreatorAnalyticsResult {
}[];
}[];
}
}
+/** [email protected] (owner: network) */
+/**
+ * network.account-merge-result@1 (OpenVibe.Network; roadmap WS-B task 5, ADR-029): the answer of a merge, POST /api/v1/account/merge (the person, signed in to the survivor, with a fresh sign-in proof of the other account) or POST /api/admin/account-merges (staff.identity.merge, with a reason). What Network moved in its one transaction, as counts; everything else follows network.subject.merged. A retried merge of the same pair answers the first merge (replayed: true) and moves nothing again.
+ */
+export interface NetworkAccountMergeResult {
+ merge_id: string;
+ from: string;
+ into: string;
+ merged_at: string;
+ split_until: string;
+ moved: {
+ /**
+ * Linked sign-in providers now on the survivor.
+ */
+ providers: number;
+ /**
+ * Sessions and devices moved; both sets stay valid until they expire.
+ */
+ sessions: number;
+ oauth_grants: number;
+ /**
+ * Developer projects, still owned, now by the survivor.
+ */
+ projects: number;
+ /**
+ * OpenCoins added to the survivor (one ledger entry per side, reason account_merge).
+ */
+ coins: number;
+ /**
+ * User-module records changed on the survivor (its own fields win).
+ */
+ modules: number;
+ };
+ replayed: boolean;
+}
+
/** [email protected] (owner: network) */
/** [email protected] (owner: network) */
/**
/**
* network.follow-list-result@1 (ADR-030): a page of follows on OpenVibe.Network, newest first. GET /api/v1/me/follows (what the caller follows), GET /api/v1/follows/:type/:target/followers (who follows a target: the target's owner, or a service with network.follows.read). Lists are never public; counts are.
* network.follow-list-result@1 (ADR-030): a page of follows on OpenVibe.Network, newest first. GET /api/v1/me/follows (what the caller follows), GET /api/v1/follows/:type/:target/followers (who follows a target: the target's owner, or a service with network.follows.read). Lists are never public; counts are.
@@ -1,5 +1,5 @@
@@ -1,5 +1,5 @@
{
{
- "version": "1.1.0",
+ "version": "1.2.0",
"roles": [
"roles": [
{
{
"id": "user",
"id": "user",
@@ -457,6 +457,17 @@
@@ -457,6 +457,17 @@
]
]
}
}
},
},
+ {
+ "id": "staff.identity.merge",
+ "minRole": "owner",
+ "since": "1.2.0",
+ "description": "Merge two accounts for account recovery (ADR-029), with a written reason and an audit row; never silently. People merge their own accounts without it, signed in to both.",
+ "gates": {
+ "network": [
+ "identity/account-merge.js (POST /api/admin/account-merges)"
+ ]
+ }
+ },
{
{
"id": "staff.roles.grant_admin",
"id": "staff.roles.grant_admin",
"minRole": "owner",
"minRole": "owner",
@@ -59,6 +59,7 @@
@@ -59,6 +59,7 @@
"network.credential.rotated",
"network.credential.rotated",
"network.follow.created",
"network.follow.created",
"network.follow.deleted",
"network.follow.deleted",
+ "network.subject.merged",
"network.grant.changed",
"network.grant.changed",
"network.module.updated",
"network.module.updated",
"network.notification.created",
"network.notification.created",
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-contracts",
"name": "openvibe-contracts",
- "version": "0.68.0",
+ "version": "0.69.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",
@@ -398,6 +398,10 @@ for (const id of ['events.event.publish', 'events.event.read', 'events.subscript
@@ -398,6 +398,10 @@ for (const id of ['events.event.publish', 'events.event.read', 'events.subscript
ok(!staff.can({ ...oldAdmin, staff_map: '1.1.0' }, 'staff.editorial.manage'), 'a token from map 1.1.0 holds only what was issued');
ok(!staff.can({ ...oldAdmin, staff_map: '1.1.0' }, 'staff.editorial.manage'), 'a token from map 1.1.0 holds only what was issued');
ok(!staff.can({ role: 'global_mod', staff_caps: ['staff.moderation.chat'] }, 'staff.editorial.manage') && staff.can({ role: 'global_mod', staff_caps: ['staff.moderation.chat'] }, 'staff.content.moderate'), 'the role decides, never more than it holds');
ok(!staff.can({ role: 'global_mod', staff_caps: ['staff.moderation.chat'] }, 'staff.editorial.manage') && staff.can({ role: 'global_mod', staff_caps: ['staff.moderation.chat'] }, 'staff.content.moderate'), 'the role decides, never more than it holds');
ok(!staff.can({ role: 'admin', staff_caps: ['staff.moderation.chat'] }, 'staff.users.manage'), 'capabilities from map 1.0.0 still come only from what was issued');
ok(!staff.can({ role: 'admin', staff_caps: ['staff.moderation.chat'] }, 'staff.users.manage'), 'capabilities from map 1.0.0 still come only from what was issued');
+ // Map 1.2.0 (v0.69.0, ADR-029): account-recovery merges are the owner's alone, and reach an owner's older token by role.
+ ok(staff.get('staff.identity.merge').since === '1.2.0' && staff.get('staff.identity.merge').minRole === 'owner', 'staff.identity.merge: owner, since 1.2.0');
+ ok(staff.can({ role: 'admin', is_owner: true, staff_caps: [], staff_map: '1.1.0' }, 'staff.identity.merge'), 'an owner token (admin + is_owner) from map 1.1.0 merges by role');
+ ok(!staff.can({ role: 'admin', staff_caps: staff.capabilitiesOf('admin'), staff_map: '1.2.0' }, 'staff.identity.merge'), 'an admin never merges accounts');
ok(map.rules.some(x => /never delegated/.test(x)) && map.rules.some(x => /equal or higher/.test(x)) && map.rules.some(x => /owner's account/.test(x)), 'rules: no delegation, rank protection, owner protection');
ok(map.rules.some(x => /never delegated/.test(x)) && map.rules.some(x => /equal or higher/.test(x)) && map.rules.some(x => /owner's account/.test(x)), 'rules: no delegation, rank protection, owner protection');
const adr = fs.readFileSync(path.join(ROOT, 'docs/adr/ADR-022-moderation-console.md'), 'utf8');
const adr = fs.readFileSync(path.join(ROOT, 'docs/adr/ADR-022-moderation-console.md'), 'utf8');
ok(/manifests\/policy\/staff-roles\.json/.test(adr), 'ADR-022 points at the staff map');
ok(/manifests\/policy\/staff-roles\.json/.test(adr), 'ADR-022 points at the staff map');