openvibers/openvibe.contracts Commits

v0.71.0: account export and deletion (roadmap WS-B task 7, ADR-033). Adds the events network.account.export_requested (export_id, subject, requested_at, deadline; each service pushes its part to POST /internal/account-exports/{id}/parts, network.account-export-part@1, capability network.account.export.contribute) and network.account.deleted (deletion_id, subject, aliases, requested_at, deleted_at; each service erases its rows once per deletion_id, keeps money rows pseudonymised, held media and tombstones under others' replies, and confirms at POST /internal/account-deletions/{id}/confirmations, network.account-deletion-confirmation@1, capability network.account.deletion.confirm), network.account-export@1, network.account-deletion@1 and network.account-data-receipt@1. Network's manifest lists the events and capabilities; Live, Chat, Community, Media and Games consume both.

OP OpenVibers committed 27 Sept 2026 · 421c8107d6 · 64 files, +1402 −51


Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 138ce725..3523af48 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -6,6 +6,19 @@ the notes were in the tag and commit messages (`git tag -n1`).
## Unreleased
++## 0.71.0 — 2026-09-27
++
++**Account export and deletion** (roadmap WS-B task 7, ADR-033).
++- `network.account.export_requested@1` (internal): a person asked for a copy of their data. Each service that keeps data about people pushes its part to `POST /internal/account-exports/{export_id}/parts` (`network.account-export-part@1`: JSON files of the subject's own rows, at most 20 MB) with the new capability `network.account.export.contribute`, before the deadline (30 minutes).
++- `network.account.deleted@1` (internal): the 30-day grace ended and Network erased what it owns. Each service erases its rows once per `deletion_id`, as follows:
++ - authored content goes, except items with others' replies, which stay as authorless tombstones;
++ - votes go, and counts are recomputed;
++ - money rows stay, pseudonymised, and held media stays.
++
++ It then confirms at `POST /internal/account-deletions/{deletion_id}/confirmations` (`network.account-deletion-confirmation@1`, counts only) with `network.account.deletion.confirm`. `aliases` names subjects merged into the account earlier.
++- `network.account-export@1` (the job the person sees: pending, ready, partial, expired or failed, with each service's state), `network.account-deletion@1` (scheduled, cancelled or deleted), and `network.account-data-receipt@1` (Network's answer to a part or confirmation).
++- Live, Chat, Community, Media and Games list both events as consumed.
++
## 0.70.0 — 2026-09-27
**Account merge, the revocation reason and module policy** (roadmap WS-B task 5, ADR-029).
ddiff --git a/contracts/catalog.json b/contracts/catalog.json
iindex 1d79b5ce..7ba9ffd2 100644
---- a/contracts/catalog.json
++++ b/contracts/catalog.json
@@@ -2406,6 +2406,76 @@
"compatibility": "backward",
"status": "active"
},
++ {
++ "id": "network.account.export_requested",
++ "version": "1.0.0",
++ "owner": "network",
++ "schema": "events/payloads/network.account.export_requested.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-033"
++ },
++ {
++ "id": "network.account.deleted",
++ "version": "1.0.0",
++ "owner": "network",
++ "schema": "events/payloads/network.account.deleted.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-033"
++ },
++ {
++ "id": "network.account-export",
++ "version": "1.0.0",
++ "owner": "network",
++ "schema": "network/account-export.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-033"
++ },
++ {
++ "id": "network.account-export-part",
++ "version": "1.0.0",
++ "owner": "network",
++ "schema": "network/account-export-part.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-033"
++ },
++ {
++ "id": "network.account-deletion",
++ "version": "1.0.0",
++ "owner": "network",
++ "schema": "network/account-deletion.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-033"
++ },
++ {
++ "id": "network.account-deletion-confirmation",
++ "version": "1.0.0",
++ "owner": "network",
++ "schema": "network/account-deletion-confirmation.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-033"
++ },
++ {
++ "id": "network.account-data-receipt",
++ "version": "1.0.0",
++ "owner": "network",
++ "schema": "network/account-data-receipt.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-033"
++ },
{
"id": "network.subject.merged",
"version": "1.0.0",
ddiff --git a/contracts/events/payloads/network.account.deleted.v1.json b/contracts/events/payloads/network.account.deleted.v1.json
nnew file mode 100644
iindex 00000000..9d28713b
---- /dev/null
++++ b/contracts/events/payloads/network.account.deleted.v1.json
@@@ -0,0 +1,44 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/network.account.deleted.v1.json",
++ "title": "NetworkAccountDeletedPayload",
++ "description": "network.account.deleted v1 (OpenVibe.Network; roadmap WS-B task 7, ADR-033). An account's 30-day grace ended and Network erased what it owns; the subject is a tombstone and is never reused. Each service erases its own rows about the subject once per deletion_id: authored content is removed (an item with other people's replies beneath it stays as an authorless tombstone), likes, votes and reactions go with counts recomputed, money and ledger rows stay pseudonymised, and media under a retention hold stays. It then confirms: POST /internal/account-deletions/{deletion_id}/confirmations (network.account-deletion-confirmation@1, capability network.account.deletion.confirm).",
++ "type": "object",
++ "required": [
++ "deletion_id",
++ "subject",
++ "requested_at",
++ "deleted_at"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "deletion_id": {
++ "type": "string",
++ "pattern": "^del_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The deletion; a redelivery carries the same id."
++ },
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The deleted account."
++ },
++ "aliases": {
++ "type": "array",
++ "maxItems": 50,
++ "items": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "description": "Subjects merged into this account earlier (ADR-029); a service that still keeps rows under one erases them too."
++ },
++ "requested_at": {
++ "type": "string",
++ "format": "date-time",
++ "description": "When the person scheduled the deletion."
++ },
++ "deleted_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++}
ddiff --git a/contracts/events/payloads/network.account.export_requested.v1.json b/contracts/events/payloads/network.account.export_requested.v1.json
nnew file mode 100644
iindex 00000000..e5ce290a
---- /dev/null
++++ b/contracts/events/payloads/network.account.export_requested.v1.json
@@@ -0,0 +1,35 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/network.account.export_requested.v1.json",
++ "title": "NetworkAccountExportRequestedPayload",
++ "description": "network.account.export_requested v1 (OpenVibe.Network; roadmap WS-B task 7, ADR-033). A person asked for a copy of their data. Each service that keeps data about people pushes its part before the deadline: POST /internal/account-exports/{export_id}/parts (network.account-export-part@1, capability network.account.export.contribute), with only the subject's own rows. A redelivery carries the same export_id; a second part from the same service replaces the first.",
++ "type": "object",
++ "required": [
++ "export_id",
++ "subject",
++ "requested_at",
++ "deadline"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "export_id": {
++ "type": "string",
++ "pattern": "^exp_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The export job; a redelivery carries the same id."
++ },
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The person whose data is exported."
++ },
++ "requested_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "deadline": {
++ "type": "string",
++ "format": "date-time",
++ "description": "Network builds the archive when every expected service has answered, or at this time (30 minutes after the request); a later part is refused."
++ }
++ }
++}
ddiff --git a/contracts/network/account-data-receipt.v1.json b/contracts/network/account-data-receipt.v1.json
nnew file mode 100644
iindex 00000000..91d7ff4b
---- /dev/null
++++ b/contracts/network/account-data-receipt.v1.json
@@@ -0,0 +1,32 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/network/account-data-receipt.v1.json",
++ "title": "NetworkAccountDataReceipt",
++ "description": "network.account-data-receipt@1 (OpenVibe.Network; roadmap WS-B task 7, ADR-033): Network's answer to a service's export part (network.account.export.contribute) or deletion confirmation (network.account.deletion.confirm). replaced: this service had already sent one for the same job, and this one replaced it.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "service",
++ "received_at",
++ "replaced"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^(exp|del)_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The export or deletion."
++ },
++ "service": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]{1,31}$"
++ },
++ "received_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "replaced": {
++ "type": "boolean"
++ }
++ }
++}
ddiff --git a/contracts/network/account-deletion-confirmation.v1.json b/contracts/network/account-deletion-confirmation.v1.json
nnew file mode 100644
iindex 00000000..f5d63385
---- /dev/null
++++ b/contracts/network/account-deletion-confirmation.v1.json
@@@ -0,0 +1,42 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/network/account-deletion-confirmation.v1.json",
++ "title": "NetworkAccountDeletionConfirmation",
++ "description": "network.account-deletion-confirmation@1 (OpenVibe.Network; roadmap WS-B task 7, ADR-033): one service confirms it erased a deleted account's rows, POST /internal/account-deletions/{deletion_id}/confirmations with capability network.account.deletion.confirm (a token speaks only for its own service). Counts only: what it erased and what it keeps (money and ledger rows pseudonymised, media under a retention hold, tombstones that keep others' replies in place). A repeat confirmation replaces the first.",
++ "type": "object",
++ "required": [
++ "subject",
++ "completed_at",
++ "erased"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "Must be the deletion's subject."
++ },
++ "completed_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "erased": {
++ "type": "object",
++ "maxProperties": 64,
++ "additionalProperties": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "description": "Rows erased, by kind (messages, follows, pastes, …)."
++ },
++ "retained": {
++ "type": "object",
++ "maxProperties": 64,
++ "additionalProperties": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "description": "Rows kept, by kind, each pseudonymised or held (ledger, held_media, tombstones, …)."
++ }
++ }
++}
ddiff --git a/contracts/network/account-deletion.v1.json b/contracts/network/account-deletion.v1.json
nnew file mode 100644
iindex 00000000..6a44bb52
---- /dev/null
++++ b/contracts/network/account-deletion.v1.json
@@@ -0,0 +1,44 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/network/account-deletion.v1.json",
++ "title": "NetworkAccountDeletion",
++ "description": "network.account-deletion@1 (OpenVibe.Network; roadmap WS-B task 7, ADR-033): a person's scheduled account deletion. POST /api/v1/account/deletion (a fresh sign-in, auth_time within 10 minutes, and the username typed out) schedules it 30 days ahead; GET answers it; DELETE cancels it before delete_after.",
++ "type": "object",
++ "required": [
++ "deletion_id",
++ "status",
++ "requested_at",
++ "delete_after"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "deletion_id": {
++ "type": "string",
++ "pattern": "^del_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "status": {
++ "enum": [
++ "scheduled",
++ "cancelled",
++ "deleted"
++ ]
++ },
++ "requested_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "delete_after": {
++ "type": "string",
++ "format": "date-time",
++ "description": "Nothing leaves Network before this time; the account works normally until then."
++ },
++ "cancelled_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "deleted_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++}
ddiff --git a/contracts/network/account-export-part.v1.json b/contracts/network/account-export-part.v1.json
nnew file mode 100644
iindex 00000000..fc915c56
---- /dev/null
++++ b/contracts/network/account-export-part.v1.json
@@@ -0,0 +1,55 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/network/account-export-part.v1.json",
++ "title": "NetworkAccountExportPart",
++ "description": "network.account-export-part@1 (OpenVibe.Network; roadmap WS-B task 7, ADR-033): one service's part of a person's data export, POST /internal/account-exports/{export_id}/parts with capability network.account.export.contribute (a token speaks only for its own service). Files are JSON documents of the subject's own rows (never another person's data), at most 20 MB in all; each lands in the archive as <service>/<name>. A second part from the same service replaces the first.",
++ "type": "object",
++ "required": [
++ "subject",
++ "files"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "Must be the export's subject."
++ },
++ "files": {
++ "type": "array",
++ "maxItems": 64,
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "name",
++ "content"
++ ],
++ "properties": {
++ "name": {
++ "type": "string",
++ "pattern": "^[a-z0-9][a-z0-9_.-]{0,62}\\.json$",
++ "description": "A flat file name ending in .json."
++ },
++ "content": {
++ "description": "Any JSON value: the rows, as the service describes them."
++ }
++ }
++ }
++ },
++ "truncated": {
++ "type": "array",
++ "maxItems": 64,
++ "items": {
++ "type": "string",
++ "maxLength": 64
++ },
++ "description": "Files cut at the service's own row limit, named so the README says so."
++ },
++ "note": {
++ "type": "string",
++ "maxLength": 500,
++ "description": "A line for the README, such as where media files are downloaded."
++ }
++ }
++}
ddiff --git a/contracts/network/account-export.v1.json b/contracts/network/account-export.v1.json
nnew file mode 100644
iindex 00000000..c391f1b0
---- /dev/null
++++ b/contracts/network/account-export.v1.json
@@@ -0,0 +1,86 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/network/account-export.v1.json",
++ "title": "NetworkAccountExport",
++ "description": "network.account-export@1 (OpenVibe.Network; roadmap WS-B task 7, ADR-033): a person's data export job. POST /api/v1/account/export starts one (one open at a time, one a day) and GET /api/v1/account/export/{id} answers its state. When ready or partial, GET /api/v1/account/export/{id}/download serves the zip to the same person until expires_at.",
++ "type": "object",
++ "required": [
++ "export_id",
++ "status",
++ "requested_at",
++ "deadline",
++ "services"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "export_id": {
++ "type": "string",
++ "pattern": "^exp_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "status": {
++ "enum": [
++ "pending",
++ "ready",
++ "partial",
++ "expired",
++ "failed"
++ ],
++ "description": "pending: waiting for the services' parts. ready: every expected service answered. partial: the deadline passed without some (services[].status missing). expired: the archive is gone (7 days). failed: the archive could not be built."
++ },
++ "requested_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "deadline": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "ready_at": {
++ "type": "string",
++ "format": "date-time",
++ "description": "When the archive was built."
++ },
++ "expires_at": {
++ "type": "string",
++ "format": "date-time",
++ "description": "The archive is deleted then (7 days after ready_at)."
++ },
++ "size_bytes": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "services": {
++ "type": "array",
++ "maxItems": 64,
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "service",
++ "status"
++ ],
++ "properties": {
++ "service": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]{1,31}$"
++ },
++ "status": {
++ "enum": [
++ "received",
++ "waiting",
++ "missing"
++ ]
++ },
++ "files": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "bytes": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++ }
++ }
++ }
++}
ddiff --git a/docs/adr/ADR-033-account-export-and-deletion.md b/docs/adr/ADR-033-account-export-and-deletion.md
nnew file mode 100644
iindex 00000000..961771eb
---- /dev/null
++++ b/docs/adr/ADR-033-account-export-and-deletion.md
@@@ -0,0 +1,60 @@
++# ADR-033: Exporting and deleting an account
++
++**Status:** Accepted 2026-09-27 (roadmap WS-B task 7). Implementation follows in OpenVibe.Network and each service that keeps data about a person.
++
++## Context and current evidence
++
++- A person's data is spread across services, each owning its rows by subject (ADR-001): Network (account, providers, sessions, OAuth grants, projects, OpenCoins, modules, follows, blocks, notifications), Live (profile, follows projection, channel points, streams, tokens, money until the Billing cutover), Chat (messages, DMs, rooms), Community (pastes, threads, posts, comments, votes), Media (objects), Games (characters).
++- Network can already reach every service through one event (ADR-029's `network.subject.merged` is consumed by each owner), and every service already calls Network with a service token (principals, ADR-004).
++- The revocation reason `account_deleted` exists in `network.user.token_valid_after` but nothing emits it.
++- Money is not erasable: ledgers are kept for the retention the billing policy names (ADR-012), with the person pseudonymised.
++
++## Decision
++
++- **Export is a job the person starts, and each service contributes its own part.**
++ - `POST /api/v1/account/export` (signed in) opens a job `exp_…`. One job may be open at a time, and one may be started per day.
++ - Network emits `network.account.export_requested { export_id, subject, requested_at, deadline }` (visibility internal).
++ - Each service that keeps data about people answers by pushing its part: `POST /internal/account-exports/:export_id/parts`, carrying `network.account-export-part@1` and the capability `network.account.export.contribute`. The token may only speak for its own service. A part is JSON files (the service's rows about the subject, never anyone else's data), at most 20 MB. A second part from the same service replaces the first.
++ - Network adds its own part and builds the archive: `network/…`, `<service>/…`, and `README.txt` naming what each service sent. It is built when every expected service has answered, or at the deadline (30 minutes), whichever comes first. The expected services are the principals holding the contribute grant.
++ - The job ends `ready`, or `partial` when a service did not answer, which is named in the README and in the status. `GET /api/v1/account/export/:id` answers `network.account-export@1`. `GET /api/v1/account/export/:id/download` serves the zip to the same person for 7 days; after that the archive is deleted.
++- **Deletion is scheduled, has a grace period, and every service confirms it.**
++ - `POST /api/v1/account/deletion` needs a fresh sign-in (`auth_time` within 10 minutes, as for a merge) and the username typed out. It schedules the deletion 30 days ahead (`del_…`, `network.account-deletion@1`).
++ - Until then the account works normally and shows the date. `DELETE /api/v1/account/deletion` cancels it; nothing leaves Network before the date.
++ - At the date, one Network transaction erases what Network owns:
++ - linked providers, sessions and devices, OAuth tokens, codes and grants;
++ - developer projects the person alone owns, with their credentials;
++ - user modules, follows both ways, blocks both ways, notifications, preferences, push subscriptions, effects, username history and pending merge intents.
++ - The OpenCoins balance is closed with one ledger entry (`reason: account_deleted`); ledger rows stay.
++ - The user row becomes a tombstone: the username is released, and the email, password and profile are cleared. The subject is never reused, and it resolves as `deleted`, as do aliases merged into it.
++ - Tokens are revoked strictly (`network.user.token_valid_after`, reason `account_deleted`). Network then emits `network.account.deleted { deletion_id, subject, requested_at, deleted_at }` (visibility internal).
++ - **Each service erases its own rows about the subject in its own transaction**, once per `deletion_id`:
++ - content the person authored is removed, except that an item with other people's replies beneath it stays as an authorless tombstone, so the replies keep their place;
++ - likes, votes and reactions go, and cached counts are recomputed;
++ - moderation records about the person's actions keep the pseudonymous subject;
++ - money and ledger rows stay, pseudonymised (no name, email or free text of the person), for the billing policy's retention;
++ - media under a retention hold (ADR-006) is kept and counted as retained.
++
++ It then confirms with `POST /internal/account-deletions/:deletion_id/confirmations`, carrying `network.account-deletion-confirmation@1` and the capability `network.account.deletion.confirm`, with counts of what it erased and retained.
++ - Network records each confirmation. Staff see deletions still waiting on a service; Events redelivers until the service answers.
++- **Nothing is exported or deleted for anyone but the signed-in person.** Staff do not start either flow. A person who lost access recovers the account first (ADR-029's staff merge, or provider recovery), then uses the same flow.
++
++## Alternatives considered
++
++- **Network calls each service's export endpoint:** rejected. It needs a Network → service credential and a route in every service; pushing parts reuses the service → Network tokens every service already has.
++- **Immediate deletion:** rejected. A mistaken or coerced request cannot be undone, and services would erase before a person could change their mind.
++- **Deleting ledger rows:** rejected by ADR-012; pseudonymised rows keep the books balanced.
++
++## Migration consequences
++
++Network gains the `account_exports`, `account_export_parts`, `account_deletions` and `account_deletion_confirmations` tables, a users `deleted_at`, and an hourly due-deletion check. Contracts gain two events, four schemas and two capabilities. Every service that stores data about people consumes both events. Until a service does, its export part is missing (the export says so) and its deletion confirmation is outstanding (staff see it).
++
++## Rollback
++
++Stop offering export and deletion (the account page hides both cards). Scheduled deletions can be cancelled by staff before their date. A deletion already carried out is not reversible, by design.
++
++## Acceptance tests
++
++- Export: a job gathers every expected service's part, the zip holds only the person's data, a missing service makes it `partial` and says which one, another person cannot download it, and it expires after 7 days.
++- Deletion: it refuses without a fresh sign-in or the typed username, cancellation stops it, and nothing happens before the date.
++- At the date, Network's rows are erased, the subject resolves as `deleted`, the old tokens are refused, and the event is emitted once.
++- Each service's test erases its rows on `network.account.deleted`, keeps others' replies and money rows pseudonymised, confirms with counts, and applies a redelivery once.
ddiff --git a/docs/adr/README.md b/docs/adr/README.md
iindex 01467c53..b6fc2b21 100644
---- a/docs/adr/README.md
++++ b/docs/adr/README.md
@@@ -36,3 +36,4 @@ Platform-wide decisions from the OpenVibe development roadmap (section 10). Each
| [ADR-030](ADR-030-follow-graph.md) | Follow graph: Network owns follows by subject, Live keeps a projection; expand/migrate/contract with reconciliation | Accepted 2026-09-26; migration pending |
| [ADR-031](ADR-031-s3-compatible-surface.md) | S3-compatible subset for Media: bucket = project namespace, object CRUD + ListObjectsV2 + presign, per-project keys | Accepted 2026-09-26; implementation pending |
| [ADR-032](ADR-032-containers.md) | Containers: platform services stay systemd units (one adapter in ovhost, a reserved `runtime` field); tenant code isolation belongs to Stage C | Accepted 2026-09-26 |
++| [ADR-033](ADR-033-account-export-and-deletion.md) | Account export and deletion: services push their export parts to Network; deletion after a 30-day grace, each service erases its rows and confirms; money rows kept pseudonymised | Accepted 2026-09-27; implementation in progress |
ddiff --git a/fixtures/network.account-data-receipt/invalid/merge-id.json b/fixtures/network.account-data-receipt/invalid/merge-id.json
nnew file mode 100644
iindex 00000000..a62e8614
---- /dev/null
++++ b/fixtures/network.account-data-receipt/invalid/merge-id.json
@@@ -0,0 +1 @@
++{"id":"mrg_01JAB2C3D4E5F6G7H8J9K0MNPS","service":"live","received_at":"2026-09-27T21:01:00.000Z","replaced":false}
ddiff --git a/fixtures/network.account-data-receipt/valid/export-part.json b/fixtures/network.account-data-receipt/valid/export-part.json
nnew file mode 100644
iindex 00000000..8c098caf
---- /dev/null
++++ b/fixtures/network.account-data-receipt/valid/export-part.json
@@@ -0,0 +1 @@
++{"id":"exp_01JAB2C3D4E5F6G7H8J9K0MNPS","service":"live","received_at":"2026-09-27T21:01:00.000Z","replaced":false}
ddiff --git a/fixtures/network.account-deletion-confirmation/invalid/negative.json b/fixtures/network.account-deletion-confirmation/invalid/negative.json
nnew file mode 100644
iindex 00000000..fc84bf8a
---- /dev/null
++++ b/fixtures/network.account-deletion-confirmation/invalid/negative.json
@@@ -0,0 +1 @@
++{"subject":"usr_01JAB2C3D4E5F6G7H8J9K0MNPQ","completed_at":"2026-09-27T21:05:00.000Z","erased":{"messages":-1}}
ddiff --git a/fixtures/network.account-deletion-confirmation/valid/chat.json b/fixtures/network.account-deletion-confirmation/valid/chat.json
nnew file mode 100644
iindex 00000000..1edbaeef
---- /dev/null
++++ b/fixtures/network.account-deletion-confirmation/valid/chat.json
@@@ -0,0 +1 @@
++{"subject":"usr_01JAB2C3D4E5F6G7H8J9K0MNPQ","completed_at":"2026-09-27T21:05:00.000Z","erased":{"messages":120,"dm_messages":8,"blocks":1},"retained":{"moderation_actions":2}}
ddiff --git a/fixtures/network.account-deletion/invalid/no-date.json b/fixtures/network.account-deletion/invalid/no-date.json
nnew file mode 100644
iindex 00000000..8ea85c0d
---- /dev/null
++++ b/fixtures/network.account-deletion/invalid/no-date.json
@@@ -0,0 +1 @@
++{"deletion_id":"del_01JAB2C3D4E5F6G7H8J9K0MNPT","status":"scheduled","requested_at":"2026-09-27T21:00:00.000Z"}
ddiff --git a/fixtures/network.account-deletion/valid/scheduled.json b/fixtures/network.account-deletion/valid/scheduled.json
nnew file mode 100644
iindex 00000000..59c29eb4
---- /dev/null
++++ b/fixtures/network.account-deletion/valid/scheduled.json
@@@ -0,0 +1 @@
++{"deletion_id":"del_01JAB2C3D4E5F6G7H8J9K0MNPT","status":"scheduled","requested_at":"2026-09-27T21:00:00.000Z","delete_after":"2026-10-27T21:00:00.000Z"}
ddiff --git a/fixtures/network.account-export-part/invalid/not-json-name.json b/fixtures/network.account-export-part/invalid/not-json-name.json
nnew file mode 100644
iindex 00000000..6e1304a2
---- /dev/null
++++ b/fixtures/network.account-export-part/invalid/not-json-name.json
@@@ -0,0 +1 @@
++{"subject":"usr_01JAB2C3D4E5F6G7H8J9K0MNPQ","files":[{"name":"avatar.png","content":"iVBOR"}]}
ddiff --git a/fixtures/network.account-export-part/invalid/path-in-name.json b/fixtures/network.account-export-part/invalid/path-in-name.json
nnew file mode 100644
iindex 00000000..3c133fc3
---- /dev/null
++++ b/fixtures/network.account-export-part/invalid/path-in-name.json
@@@ -0,0 +1 @@
++{"subject":"usr_01JAB2C3D4E5F6G7H8J9K0MNPQ","files":[{"name":"../network/profile.json","content":{}}]}
ddiff --git a/fixtures/network.account-export-part/valid/live.json b/fixtures/network.account-export-part/valid/live.json
nnew file mode 100644
iindex 00000000..ef21db46
---- /dev/null
++++ b/fixtures/network.account-export-part/valid/live.json
@@@ -0,0 +1 @@
++{"subject":"usr_01JAB2C3D4E5F6G7H8J9K0MNPQ","files":[{"name":"follows.json","content":[{"channel":"xena","since":"2026-01-02T03:04:05.000Z"}]},{"name":"profile.json","content":{"username":"keep","bio":"hi"}}],"truncated":[],"note":"Stream recordings are Media's part."}
ddiff --git a/fixtures/network.account-export/invalid/unknown-status.json b/fixtures/network.account-export/invalid/unknown-status.json
nnew file mode 100644
iindex 00000000..afd4187a
---- /dev/null
++++ b/fixtures/network.account-export/invalid/unknown-status.json
@@@ -0,0 +1 @@
++{"export_id":"exp_01JAB2C3D4E5F6G7H8J9K0MNPS","status":"done","requested_at":"2026-09-27T21:00:00.000Z","deadline":"2026-09-27T21:30:00.000Z","services":[]}
ddiff --git a/fixtures/network.account-export/valid/partial.json b/fixtures/network.account-export/valid/partial.json
nnew file mode 100644
iindex 00000000..bff56f78
---- /dev/null
++++ b/fixtures/network.account-export/valid/partial.json
@@@ -0,0 +1 @@
++{"export_id":"exp_01JAB2C3D4E5F6G7H8J9K0MNPS","status":"partial","requested_at":"2026-09-27T21:00:00.000Z","deadline":"2026-09-27T21:30:00.000Z","ready_at":"2026-09-27T21:30:02.000Z","expires_at":"2026-10-04T21:30:02.000Z","size_bytes":48213,"services":[{"service":"network","status":"received","files":9,"bytes":30100},{"service":"live","status":"received","files":6,"bytes":18113},{"service":"games","status":"missing"}]}
ddiff --git a/fixtures/network.account.deleted/invalid/leaks-username.json b/fixtures/network.account.deleted/invalid/leaks-username.json
nnew file mode 100644
iindex 00000000..a407e579
---- /dev/null
++++ b/fixtures/network.account.deleted/invalid/leaks-username.json
@@@ -0,0 +1 @@
++{"deletion_id":"del_01JAB2C3D4E5F6G7H8J9K0MNPT","subject":"usr_01JAB2C3D4E5F6G7H8J9K0MNPQ","username":"keep","requested_at":"2026-08-28T21:00:00.000Z","deleted_at":"2026-09-27T21:00:00.000Z"}
ddiff --git a/fixtures/network.account.deleted/valid/deleted.json b/fixtures/network.account.deleted/valid/deleted.json
nnew file mode 100644
iindex 00000000..da1d5bb6
---- /dev/null
++++ b/fixtures/network.account.deleted/valid/deleted.json
@@@ -0,0 +1 @@
++{"deletion_id":"del_01JAB2C3D4E5F6G7H8J9K0MNPT","subject":"usr_01JAB2C3D4E5F6G7H8J9K0MNPQ","requested_at":"2026-08-28T21:00:00.000Z","deleted_at":"2026-09-27T21:00:00.000Z"}
ddiff --git a/fixtures/network.account.deleted/valid/with-aliases.json b/fixtures/network.account.deleted/valid/with-aliases.json
nnew file mode 100644
iindex 00000000..9a8505bb
---- /dev/null
++++ b/fixtures/network.account.deleted/valid/with-aliases.json
@@@ -0,0 +1 @@
++{"deletion_id":"del_01JAB2C3D4E5F6G7H8J9K0MNPT","subject":"usr_01JAB2C3D4E5F6G7H8J9K0MNPQ","aliases":["usr_01JAB2C3D4E5F6G7H8J9K0MNPR"],"requested_at":"2026-08-28T21:00:00.000Z","deleted_at":"2026-09-27T21:00:00.000Z"}
ddiff --git a/fixtures/network.account.export_requested/invalid/legacy-id.json b/fixtures/network.account.export_requested/invalid/legacy-id.json
nnew file mode 100644
iindex 00000000..8ad6183e
---- /dev/null
++++ b/fixtures/network.account.export_requested/invalid/legacy-id.json
@@@ -0,0 +1 @@
++{"export_id":"exp_01JAB2C3D4E5F6G7H8J9K0MNPS","subject":"42","requested_at":"2026-09-27T21:00:00.000Z","deadline":"2026-09-27T21:30:00.000Z"}
ddiff --git a/fixtures/network.account.export_requested/invalid/no-deadline.json b/fixtures/network.account.export_requested/invalid/no-deadline.json
nnew file mode 100644
iindex 00000000..9e5c6c76
---- /dev/null
++++ b/fixtures/network.account.export_requested/invalid/no-deadline.json
@@@ -0,0 +1 @@
++{"export_id":"exp_01JAB2C3D4E5F6G7H8J9K0MNPS","subject":"usr_01JAB2C3D4E5F6G7H8J9K0MNPQ","requested_at":"2026-09-27T21:00:00.000Z"}
ddiff --git a/fixtures/network.account.export_requested/valid/requested.json b/fixtures/network.account.export_requested/valid/requested.json
nnew file mode 100644
iindex 00000000..960c7469
---- /dev/null
++++ b/fixtures/network.account.export_requested/valid/requested.json
@@@ -0,0 +1 @@
++{"export_id":"exp_01JAB2C3D4E5F6G7H8J9K0MNPS","subject":"usr_01JAB2C3D4E5F6G7H8J9K0MNPQ","requested_at":"2026-09-27T21:00:00.000Z","deadline":"2026-09-27T21:30:00.000Z"}
ddiff --git a/generated/json-schema/bundle.json b/generated/json-schema/bundle.json
iindex 3bab4f5a..cfb86108 100644
---- a/generated/json-schema/bundle.json
++++ b/generated/json-schema/bundle.json
@@@ -25379,6 +25379,379 @@
}
}
},
++ "network.account.export_requested": {
++ "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.account.export_requested.v1.json",
++ "title": "NetworkAccountExportRequestedPayload",
++ "description": "network.account.export_requested v1 (OpenVibe.Network; roadmap WS-B task 7, ADR-033). A person asked for a copy of their data. Each service that keeps data about people pushes its part before the deadline: POST /internal/account-exports/{export_id}/parts (network.account-export-part@1, capability network.account.export.contribute), with only the subject's own rows. A redelivery carries the same export_id; a second part from the same service replaces the first.",
++ "type": "object",
++ "required": [
++ "export_id",
++ "subject",
++ "requested_at",
++ "deadline"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "export_id": {
++ "type": "string",
++ "pattern": "^exp_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The export job; a redelivery carries the same id."
++ },
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The person whose data is exported."
++ },
++ "requested_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "deadline": {
++ "type": "string",
++ "format": "date-time",
++ "description": "Network builds the archive when every expected service has answered, or at this time (30 minutes after the request); a later part is refused."
++ }
++ }
++ }
++ },
++ "network.account.deleted": {
++ "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.account.deleted.v1.json",
++ "title": "NetworkAccountDeletedPayload",
++ "description": "network.account.deleted v1 (OpenVibe.Network; roadmap WS-B task 7, ADR-033). An account's 30-day grace ended and Network erased what it owns; the subject is a tombstone and is never reused. Each service erases its own rows about the subject once per deletion_id: authored content is removed (an item with other people's replies beneath it stays as an authorless tombstone), likes, votes and reactions go with counts recomputed, money and ledger rows stay pseudonymised, and media under a retention hold stays. It then confirms: POST /internal/account-deletions/{deletion_id}/confirmations (network.account-deletion-confirmation@1, capability network.account.deletion.confirm).",
++ "type": "object",
++ "required": [
++ "deletion_id",
++ "subject",
++ "requested_at",
++ "deleted_at"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "deletion_id": {
++ "type": "string",
++ "pattern": "^del_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The deletion; a redelivery carries the same id."
++ },
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The deleted account."
++ },
++ "aliases": {
++ "type": "array",
++ "maxItems": 50,
++ "items": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "description": "Subjects merged into this account earlier (ADR-029); a service that still keeps rows under one erases them too."
++ },
++ "requested_at": {
++ "type": "string",
++ "format": "date-time",
++ "description": "When the person scheduled the deletion."
++ },
++ "deleted_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ },
++ "network.account-export": {
++ "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-export.v1.json",
++ "title": "NetworkAccountExport",
++ "description": "network.account-export@1 (OpenVibe.Network; roadmap WS-B task 7, ADR-033): a person's data export job. POST /api/v1/account/export starts one (one open at a time, one a day) and GET /api/v1/account/export/{id} answers its state. When ready or partial, GET /api/v1/account/export/{id}/download serves the zip to the same person until expires_at.",
++ "type": "object",
++ "required": [
++ "export_id",
++ "status",
++ "requested_at",
++ "deadline",
++ "services"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "export_id": {
++ "type": "string",
++ "pattern": "^exp_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "status": {
++ "enum": [
++ "pending",
++ "ready",
++ "partial",
++ "expired",
++ "failed"
++ ],
++ "description": "pending: waiting for the services' parts. ready: every expected service answered. partial: the deadline passed without some (services[].status missing). expired: the archive is gone (7 days). failed: the archive could not be built."
++ },
++ "requested_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "deadline": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "ready_at": {
++ "type": "string",
++ "format": "date-time",
++ "description": "When the archive was built."
++ },
++ "expires_at": {
++ "type": "string",
++ "format": "date-time",
++ "description": "The archive is deleted then (7 days after ready_at)."
++ },
++ "size_bytes": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "services": {
++ "type": "array",
++ "maxItems": 64,
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "service",
++ "status"
++ ],
++ "properties": {
++ "service": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]{1,31}$"
++ },
++ "status": {
++ "enum": [
++ "received",
++ "waiting",
++ "missing"
++ ]
++ },
++ "files": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "bytes": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "network.account-export-part": {
++ "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-export-part.v1.json",
++ "title": "NetworkAccountExportPart",
++ "description": "network.account-export-part@1 (OpenVibe.Network; roadmap WS-B task 7, ADR-033): one service's part of a person's data export, POST /internal/account-exports/{export_id}/parts with capability network.account.export.contribute (a token speaks only for its own service). Files are JSON documents of the subject's own rows (never another person's data), at most 20 MB in all; each lands in the archive as <service>/<name>. A second part from the same service replaces the first.",
++ "type": "object",
++ "required": [
++ "subject",
++ "files"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "Must be the export's subject."
++ },
++ "files": {
++ "type": "array",
++ "maxItems": 64,
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "name",
++ "content"
++ ],
++ "properties": {
++ "name": {
++ "type": "string",
++ "pattern": "^[a-z0-9][a-z0-9_.-]{0,62}\\.json$",
++ "description": "A flat file name ending in .json."
++ },
++ "content": {
++ "description": "Any JSON value: the rows, as the service describes them."
++ }
++ }
++ }
++ },
++ "truncated": {
++ "type": "array",
++ "maxItems": 64,
++ "items": {
++ "type": "string",
++ "maxLength": 64
++ },
++ "description": "Files cut at the service's own row limit, named so the README says so."
++ },
++ "note": {
++ "type": "string",
++ "maxLength": 500,
++ "description": "A line for the README, such as where media files are downloaded."
++ }
++ }
++ }
++ },
++ "network.account-deletion": {
++ "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-deletion.v1.json",
++ "title": "NetworkAccountDeletion",
++ "description": "network.account-deletion@1 (OpenVibe.Network; roadmap WS-B task 7, ADR-033): a person's scheduled account deletion. POST /api/v1/account/deletion (a fresh sign-in, auth_time within 10 minutes, and the username typed out) schedules it 30 days ahead; GET answers it; DELETE cancels it before delete_after.",
++ "type": "object",
++ "required": [
++ "deletion_id",
++ "status",
++ "requested_at",
++ "delete_after"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "deletion_id": {
++ "type": "string",
++ "pattern": "^del_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "status": {
++ "enum": [
++ "scheduled",
++ "cancelled",
++ "deleted"
++ ]
++ },
++ "requested_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "delete_after": {
++ "type": "string",
++ "format": "date-time",
++ "description": "Nothing leaves Network before this time; the account works normally until then."
++ },
++ "cancelled_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "deleted_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ },
++ "network.account-deletion-confirmation": {
++ "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-deletion-confirmation.v1.json",
++ "title": "NetworkAccountDeletionConfirmation",
++ "description": "network.account-deletion-confirmation@1 (OpenVibe.Network; roadmap WS-B task 7, ADR-033): one service confirms it erased a deleted account's rows, POST /internal/account-deletions/{deletion_id}/confirmations with capability network.account.deletion.confirm (a token speaks only for its own service). Counts only: what it erased and what it keeps (money and ledger rows pseudonymised, media under a retention hold, tombstones that keep others' replies in place). A repeat confirmation replaces the first.",
++ "type": "object",
++ "required": [
++ "subject",
++ "completed_at",
++ "erased"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "Must be the deletion's subject."
++ },
++ "completed_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "erased": {
++ "type": "object",
++ "maxProperties": 64,
++ "additionalProperties": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "description": "Rows erased, by kind (messages, follows, pastes, …)."
++ },
++ "retained": {
++ "type": "object",
++ "maxProperties": 64,
++ "additionalProperties": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "description": "Rows kept, by kind, each pseudonymised or held (ledger, held_media, tombstones, …)."
++ }
++ }
++ }
++ },
++ "network.account-data-receipt": {
++ "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-data-receipt.v1.json",
++ "title": "NetworkAccountDataReceipt",
++ "description": "network.account-data-receipt@1 (OpenVibe.Network; roadmap WS-B task 7, ADR-033): Network's answer to a service's export part (network.account.export.contribute) or deletion confirmation (network.account.deletion.confirm). replaced: this service had already sent one for the same job, and this one replaced it.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "service",
++ "received_at",
++ "replaced"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^(exp|del)_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The export or deletion."
++ },
++ "service": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]{1,31}$"
++ },
++ "received_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "replaced": {
++ "type": "boolean"
++ }
++ }
++ }
++ },
"network.subject.merged": {
"version": "1.0.0",
"owner": "network",
ddiff --git a/generated/openapi/ai.json b/generated/openapi/ai.json
iindex c340a320..891bb34b 100644
---- a/generated/openapi/ai.json
++++ b/generated/openapi/ai.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.AI API",
-- "version": "0.70.0",
-- "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.70.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.71.0",
++ "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.71.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"
ddiff --git a/generated/openapi/billing.json b/generated/openapi/billing.json
iindex b2c9d9a4..110cd531 100644
---- a/generated/openapi/billing.json
++++ b/generated/openapi/billing.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Billing API",
-- "version": "0.70.0",
-- "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.70.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.71.0",
++ "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.71.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"
ddiff --git a/generated/openapi/blog.json b/generated/openapi/blog.json
iindex b55344ee..74127485 100644
---- a/generated/openapi/blog.json
++++ b/generated/openapi/blog.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Blog API",
-- "version": "0.70.0",
-- "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.70.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.71.0",
++ "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.71.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"
ddiff --git a/generated/openapi/chat.json b/generated/openapi/chat.json
iindex fea40e74..b2d947e9 100644
---- a/generated/openapi/chat.json
++++ b/generated/openapi/chat.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Chat API",
-- "version": "0.70.0",
-- "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.70.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.71.0",
++ "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.71.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"
ddiff --git a/generated/openapi/codes.json b/generated/openapi/codes.json
iindex 8f1873ae..67511132 100644
---- a/generated/openapi/codes.json
++++ b/generated/openapi/codes.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Codes API",
-- "version": "0.70.0",
-- "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.70.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.71.0",
++ "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.71.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"
ddiff --git a/generated/openapi/community.json b/generated/openapi/community.json
iindex 3dc7e31a..6491aa7c 100644
---- a/generated/openapi/community.json
++++ b/generated/openapi/community.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Community API",
-- "version": "0.70.0",
-- "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.70.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.71.0",
++ "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.71.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"
ddiff --git a/generated/openapi/coupons.json b/generated/openapi/coupons.json
iindex 2302e5ba..65468c8e 100644
---- a/generated/openapi/coupons.json
++++ b/generated/openapi/coupons.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Coupons API",
-- "version": "0.70.0",
-- "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.70.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.71.0",
++ "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.71.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"
ddiff --git a/generated/openapi/deals.json b/generated/openapi/deals.json
iindex 50131639..82bca9be 100644
---- a/generated/openapi/deals.json
++++ b/generated/openapi/deals.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Deals API",
-- "version": "0.70.0",
-- "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.70.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.71.0",
++ "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.71.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"
ddiff --git a/generated/openapi/events.json b/generated/openapi/events.json
iindex 5ecad197..c9056985 100644
---- a/generated/openapi/events.json
++++ b/generated/openapi/events.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Events API",
-- "version": "0.70.0",
-- "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.70.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.71.0",
++ "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.71.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"
ddiff --git a/generated/openapi/games.json b/generated/openapi/games.json
iindex b7268ced..df9a629d 100644
---- a/generated/openapi/games.json
++++ b/generated/openapi/games.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Games API",
-- "version": "0.70.0",
-- "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.70.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.71.0",
++ "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.71.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"
ddiff --git a/generated/openapi/host.json b/generated/openapi/host.json
iindex 2a4ab2d1..2d3f9a2c 100644
---- a/generated/openapi/host.json
++++ b/generated/openapi/host.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Host API",
-- "version": "0.70.0",
-- "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.70.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.71.0",
++ "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.71.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"
ddiff --git a/generated/openapi/index.json b/generated/openapi/index.json
iindex 85b97a9b..e7b36e74 100644
---- a/generated/openapi/index.json
++++ b/generated/openapi/index.json
@@@ -96,8 +96,8 @@
"service": "network",
"name": "OpenVibe.Network",
"origin": "https://openvibe.network",
-- "operations": 18,
-- "capabilities": 15
++ "operations": 20,
++ "capabilities": 17
},
{
"service": "news",
ddiff --git a/generated/openapi/live.json b/generated/openapi/live.json
iindex 18507f63..3a2ec924 100644
---- a/generated/openapi/live.json
++++ b/generated/openapi/live.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Live API",
-- "version": "0.70.0",
-- "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.70.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.71.0",
++ "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.71.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"
ddiff --git a/generated/openapi/media.json b/generated/openapi/media.json
iindex 86a9a925..193fb32e 100644
---- a/generated/openapi/media.json
++++ b/generated/openapi/media.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Media API",
-- "version": "0.70.0",
-- "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.70.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.71.0",
++ "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.71.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"
ddiff --git a/generated/openapi/network.json b/generated/openapi/network.json
iindex b84bbbdb..ade6920f 100644
---- a/generated/openapi/network.json
++++ b/generated/openapi/network.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Network API",
-- "version": "0.70.0",
-- "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.70.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.71.0",
++ "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.71.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"
@@@ -304,6 +304,138 @@
}
}
},
++ "/internal/account-deletions/{deletion_id}/confirmations": {
++ "post": {
++ "operationId": "post_internal_account_deletions_deletion_id_confirmations",
++ "summary": "Confirm that a deleted account's rows are erased at a service (ADR-033), with counts of what was erased and what is kept (pseudonymised money rows, held media, tombstones).",
++ "description": "**network.account.deletion.confirm** (internal): Confirm that a deleted account's rows are erased at a service (ADR-033), with counts of what was erased and what is kept (pseudonymised money rows, held media, tombstones). A token may only speak for its own service.",
++ "tags": [
++ "network.account"
++ ],
++ "x-openvibe-capabilities": [
++ "network.account.deletion.confirm"
++ ],
++ "x-openvibe-visibility": [
++ "internal"
++ ],
++ "x-openvibe-quota-class": "account-data",
++ "security": [
++ {
++ "openvibeToken": [
++ "network.account.deletion.confirm"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/network.account-deletion-confirmation.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
++ {
++ "name": "deletion_id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/network.account-data-receipt.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/internal/account-exports/{export_id}/parts": {
++ "post": {
++ "operationId": "post_internal_account_exports_export_id_parts",
++ "summary": "Push a service's part of a person's data export (ADR-033): the subject's own rows as JSON files, before the export's deadline.",
++ "description": "**network.account.export.contribute** (internal): Push a service's part of a person's data export (ADR-033): the subject's own rows as JSON files, before the export's deadline. A token may only speak for its own service; a second part replaces the first.",
++ "tags": [
++ "network.account"
++ ],
++ "x-openvibe-capabilities": [
++ "network.account.export.contribute"
++ ],
++ "x-openvibe-visibility": [
++ "internal"
++ ],
++ "x-openvibe-quota-class": "account-data",
++ "security": [
++ {
++ "openvibeToken": [
++ "network.account.export.contribute"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/network.account-export-part.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
++ {
++ "name": "export_id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/network.account-data-receipt.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
"/internal/blocks?subject=usr_…": {
"get": {
"operationId": "get_internal_blocks_subject_usr",
@@@ -1438,6 +1570,129 @@
}
}
},
++ "network.account-data-receipt.v1": {
++ "title": "NetworkAccountDataReceipt",
++ "description": "network.account-data-receipt@1 (OpenVibe.Network; roadmap WS-B task 7, ADR-033): Network's answer to a service's export part (network.account.export.contribute) or deletion confirmation (network.account.deletion.confirm). replaced: this service had already sent one for the same job, and this one replaced it.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "service",
++ "received_at",
++ "replaced"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^(exp|del)_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The export or deletion."
++ },
++ "service": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]{1,31}$"
++ },
++ "received_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "replaced": {
++ "type": "boolean"
++ }
++ }
++ },
++ "network.account-deletion-confirmation.v1": {
++ "title": "NetworkAccountDeletionConfirmation",
++ "description": "network.account-deletion-confirmation@1 (OpenVibe.Network; roadmap WS-B task 7, ADR-033): one service confirms it erased a deleted account's rows, POST /internal/account-deletions/{deletion_id}/confirmations with capability network.account.deletion.confirm (a token speaks only for its own service). Counts only: what it erased and what it keeps (money and ledger rows pseudonymised, media under a retention hold, tombstones that keep others' replies in place). A repeat confirmation replaces the first.",
++ "type": "object",
++ "required": [
++ "subject",
++ "completed_at",
++ "erased"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "Must be the deletion's subject."
++ },
++ "completed_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "erased": {
++ "type": "object",
++ "maxProperties": 64,
++ "additionalProperties": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "description": "Rows erased, by kind (messages, follows, pastes, …)."
++ },
++ "retained": {
++ "type": "object",
++ "maxProperties": 64,
++ "additionalProperties": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "description": "Rows kept, by kind, each pseudonymised or held (ledger, held_media, tombstones, …)."
++ }
++ }
++ },
++ "network.account-export-part.v1": {
++ "title": "NetworkAccountExportPart",
++ "description": "network.account-export-part@1 (OpenVibe.Network; roadmap WS-B task 7, ADR-033): one service's part of a person's data export, POST /internal/account-exports/{export_id}/parts with capability network.account.export.contribute (a token speaks only for its own service). Files are JSON documents of the subject's own rows (never another person's data), at most 20 MB in all; each lands in the archive as <service>/<name>. A second part from the same service replaces the first.",
++ "type": "object",
++ "required": [
++ "subject",
++ "files"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "Must be the export's subject."
++ },
++ "files": {
++ "type": "array",
++ "maxItems": 64,
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "name",
++ "content"
++ ],
++ "properties": {
++ "name": {
++ "type": "string",
++ "pattern": "^[a-z0-9][a-z0-9_.-]{0,62}\\.json$",
++ "description": "A flat file name ending in .json."
++ },
++ "content": {
++ "description": "Any JSON value: the rows, as the service describes them."
++ }
++ }
++ }
++ },
++ "truncated": {
++ "type": "array",
++ "maxItems": 64,
++ "items": {
++ "type": "string",
++ "maxLength": 64
++ },
++ "description": "Files cut at the service's own row limit, named so the README says so."
++ },
++ "note": {
++ "type": "string",
++ "maxLength": 500,
++ "description": "A line for the README, such as where media files are downloaded."
++ }
++ }
++ },
"network.blocks-result.v1": {
"title": "NetworkBlocksResult",
"description": "network.blocks-result@1: the answer of network.blocks.read on OpenVibe.Network. GET /internal/blocks?subject=usr_… (a service token holding network.blocks.read; the shared internal key is refused) → { subject, blocks, blocked_by }: the people the subject has blocked and the people who have blocked the subject, as usr_ subjects in subject order, active blocks only (a person may hold at most 5000). Answered with Cache-Control: no-store. A subject that is not a usr_ id is refused with 400 blocks.bad_subject (problem+json).",
ddiff --git a/generated/openapi/news.json b/generated/openapi/news.json
iindex a351bd68..137caabf 100644
---- a/generated/openapi/news.json
++++ b/generated/openapi/news.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.News API",
-- "version": "0.70.0",
-- "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.70.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.71.0",
++ "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.71.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"
ddiff --git a/generated/openapi/openre.json b/generated/openapi/openre.json
iindex 9f6187bc..51d18a48 100644
---- a/generated/openapi/openre.json
++++ b/generated/openapi/openre.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenRe.Stream API",
-- "version": "0.70.0",
-- "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.70.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.71.0",
++ "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.71.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"
ddiff --git a/generated/openapi/reviews.json b/generated/openapi/reviews.json
iindex e1bdc581..46b55e66 100644
---- a/generated/openapi/reviews.json
++++ b/generated/openapi/reviews.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Reviews API",
-- "version": "0.70.0",
-- "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.70.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.71.0",
++ "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.71.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"
ddiff --git a/generated/openapi/search.json b/generated/openapi/search.json
iindex d088d6e0..78b694ec 100644
---- a/generated/openapi/search.json
++++ b/generated/openapi/search.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Search API",
-- "version": "0.70.0",
-- "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.70.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.71.0",
++ "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.71.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"
ddiff --git a/generated/openapi/sources.json b/generated/openapi/sources.json
iindex 03821e3d..e1ef80e0 100644
---- a/generated/openapi/sources.json
++++ b/generated/openapi/sources.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Sources API",
-- "version": "0.70.0",
-- "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.70.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.71.0",
++ "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.71.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"
ddiff --git a/generated/openapi/tips.json b/generated/openapi/tips.json
iindex 93a01caa..c2eed1cf 100644
---- a/generated/openapi/tips.json
++++ b/generated/openapi/tips.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Tips API",
-- "version": "0.70.0",
-- "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.70.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.71.0",
++ "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.71.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"
ddiff --git a/generated/openapi/tools.json b/generated/openapi/tools.json
iindex bf4abcef..346e89f8 100644
---- a/generated/openapi/tools.json
++++ b/generated/openapi/tools.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Tools API",
-- "version": "0.70.0",
-- "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.70.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.71.0",
++ "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.71.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"
ddiff --git a/generated/openapi/trade.json b/generated/openapi/trade.json
iindex 949e6bdb..e7ba4875 100644
---- a/generated/openapi/trade.json
++++ b/generated/openapi/trade.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Trade API",
-- "version": "0.70.0",
-- "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.70.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.71.0",
++ "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.71.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"
ddiff --git a/generated/openapi/vip.json b/generated/openapi/vip.json
iindex 9f56d584..9b35e324 100644
---- a/generated/openapi/vip.json
++++ b/generated/openapi/vip.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.VIP API",
-- "version": "0.70.0",
-- "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.70.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.71.0",
++ "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.71.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"
ddiff --git a/generated/openapi/wiki.json b/generated/openapi/wiki.json
iindex f1fb5b8e..c260ca08 100644
---- a/generated/openapi/wiki.json
++++ b/generated/openapi/wiki.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Wiki API",
-- "version": "0.70.0",
-- "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.70.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.71.0",
++ "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.71.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"
ddiff --git a/generated/typescript/index.d.ts b/generated/typescript/index.d.ts
iindex 0ff5c39b..ee07be75 100644
---- a/generated/typescript/index.d.ts
++++ b/generated/typescript/index.d.ts
@@@ -28636,6 +28636,174 @@ export interface NetworkPrincipalGrantChangedPayload {
actor_subject?: string | null;
}
++/** [email protected] (owner: network) */
++/**
++ * network.account.export_requested v1 (OpenVibe.Network; roadmap WS-B task 7, ADR-033). A person asked for a copy of their data. Each service that keeps data about people pushes its part before the deadline: POST /internal/account-exports/{export_id}/parts (network.account-export-part@1, capability network.account.export.contribute), with only the subject's own rows. A redelivery carries the same export_id; a second part from the same service replaces the first.
++ */
++export interface NetworkAccountExportRequestedPayload {
++ /**
++ * The export job; a redelivery carries the same id.
++ */
++ export_id: string;
++ /**
++ * The person whose data is exported.
++ */
++ subject: string;
++ requested_at: string;
++ /**
++ * Network builds the archive when every expected service has answered, or at this time (30 minutes after the request); a later part is refused.
++ */
++ deadline: string;
++}
++
++/** [email protected] (owner: network) */
++/**
++ * network.account.deleted v1 (OpenVibe.Network; roadmap WS-B task 7, ADR-033). An account's 30-day grace ended and Network erased what it owns; the subject is a tombstone and is never reused. Each service erases its own rows about the subject once per deletion_id: authored content is removed (an item with other people's replies beneath it stays as an authorless tombstone), likes, votes and reactions go with counts recomputed, money and ledger rows stay pseudonymised, and media under a retention hold stays. It then confirms: POST /internal/account-deletions/{deletion_id}/confirmations (network.account-deletion-confirmation@1, capability network.account.deletion.confirm).
++ */
++export interface NetworkAccountDeletedPayload {
++ /**
++ * The deletion; a redelivery carries the same id.
++ */
++ deletion_id: string;
++ /**
++ * The deleted account.
++ */
++ subject: string;
++ /**
++ * Subjects merged into this account earlier (ADR-029); a service that still keeps rows under one erases them too.
++ *
++ * @maxItems 50
++ */
++ aliases?: string[];
++ /**
++ * When the person scheduled the deletion.
++ */
++ requested_at: string;
++ deleted_at: string;
++}
++
++/** [email protected] (owner: network) */
++/**
++ * network.account-export@1 (OpenVibe.Network; roadmap WS-B task 7, ADR-033): a person's data export job. POST /api/v1/account/export starts one (one open at a time, one a day) and GET /api/v1/account/export/{id} answers its state. When ready or partial, GET /api/v1/account/export/{id}/download serves the zip to the same person until expires_at.
++ */
++export interface NetworkAccountExport {
++ export_id: string;
++ /**
++ * pending: waiting for the services' parts. ready: every expected service answered. partial: the deadline passed without some (services[].status missing). expired: the archive is gone (7 days). failed: the archive could not be built.
++ */
++ status: "pending" | "ready" | "partial" | "expired" | "failed";
++ requested_at: string;
++ deadline: string;
++ /**
++ * When the archive was built.
++ */
++ ready_at?: string;
++ /**
++ * The archive is deleted then (7 days after ready_at).
++ */
++ expires_at?: string;
++ size_bytes?: number;
++ /**
++ * @maxItems 64
++ */
++ services: {
++ service: string;
++ status: "received" | "waiting" | "missing";
++ files?: number;
++ bytes?: number;
++ }[];
++}
++
++/** [email protected] (owner: network) */
++/**
++ * network.account-export-part@1 (OpenVibe.Network; roadmap WS-B task 7, ADR-033): one service's part of a person's data export, POST /internal/account-exports/{export_id}/parts with capability network.account.export.contribute (a token speaks only for its own service). Files are JSON documents of the subject's own rows (never another person's data), at most 20 MB in all; each lands in the archive as <service>/<name>. A second part from the same service replaces the first.
++ */
++export interface NetworkAccountExportPart {
++ /**
++ * Must be the export's subject.
++ */
++ subject: string;
++ /**
++ * @maxItems 64
++ */
++ files: {
++ /**
++ * A flat file name ending in .json.
++ */
++ name: string;
++ /**
++ * Any JSON value: the rows, as the service describes them.
++ */
++ content: {
++ [k: string]: unknown | undefined;
++ };
++ }[];
++ /**
++ * Files cut at the service's own row limit, named so the README says so.
++ *
++ * @maxItems 64
++ */
++ truncated?: string[];
++ /**
++ * A line for the README, such as where media files are downloaded.
++ */
++ note?: string;
++}
++
++/** [email protected] (owner: network) */
++/**
++ * network.account-deletion@1 (OpenVibe.Network; roadmap WS-B task 7, ADR-033): a person's scheduled account deletion. POST /api/v1/account/deletion (a fresh sign-in, auth_time within 10 minutes, and the username typed out) schedules it 30 days ahead; GET answers it; DELETE cancels it before delete_after.
++ */
++export interface NetworkAccountDeletion {
++ deletion_id: string;
++ status: "scheduled" | "cancelled" | "deleted";
++ requested_at: string;
++ /**
++ * Nothing leaves Network before this time; the account works normally until then.
++ */
++ delete_after: string;
++ cancelled_at?: string;
++ deleted_at?: string;
++}
++
++/** [email protected] (owner: network) */
++/**
++ * network.account-deletion-confirmation@1 (OpenVibe.Network; roadmap WS-B task 7, ADR-033): one service confirms it erased a deleted account's rows, POST /internal/account-deletions/{deletion_id}/confirmations with capability network.account.deletion.confirm (a token speaks only for its own service). Counts only: what it erased and what it keeps (money and ledger rows pseudonymised, media under a retention hold, tombstones that keep others' replies in place). A repeat confirmation replaces the first.
++ */
++export interface NetworkAccountDeletionConfirmation {
++ /**
++ * Must be the deletion's subject.
++ */
++ subject: string;
++ completed_at: string;
++ /**
++ * Rows erased, by kind (messages, follows, pastes, …).
++ */
++ erased: {
++ [k: string]: number | undefined;
++ };
++ /**
++ * Rows kept, by kind, each pseudonymised or held (ledger, held_media, tombstones, …).
++ */
++ retained?: {
++ [k: string]: number | undefined;
++ };
++}
++
++/** [email protected] (owner: network) */
++/**
++ * network.account-data-receipt@1 (OpenVibe.Network; roadmap WS-B task 7, ADR-033): Network's answer to a service's export part (network.account.export.contribute) or deletion confirmation (network.account.deletion.confirm). replaced: this service had already sent one for the same job, and this one replaced it.
++ */
++export interface NetworkAccountDataReceipt {
++ /**
++ * The export or deletion.
++ */
++ id: string;
++ service: string;
++ received_at: string;
++ replaced: boolean;
++}
++
/** [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).
ddiff --git a/manifests/capabilities/network.account.deletion.confirm.json b/manifests/capabilities/network.account.deletion.confirm.json
nnew file mode 100644
iindex 00000000..ae2789b8
---- /dev/null
++++ b/manifests/capabilities/network.account.deletion.confirm.json
@@@ -0,0 +1,21 @@
++{
++ "id": "network.account.deletion.confirm",
++ "version": "1.0",
++ "owner": "network",
++ "status": "active",
++ "visibility": "internal",
++ "description": "Confirm that a deleted account's rows are erased at a service (ADR-033), with counts of what was erased and what is kept (pseudonymised money rows, held media, tombstones). A token may only speak for its own service.",
++ "permissions": [
++ "account:delete"
++ ],
++ "resourceConstraints": [
++ "owner"
++ ],
++ "quotaClass": "account-data",
++ "events": [],
++ "implementedBy": [
++ "POST /internal/account-deletions/:deletion_id/confirmations"
++ ],
++ "inputSchema": "network.account-deletion-confirmation@1",
++ "outputSchema": "network.account-data-receipt@1"
++}
ddiff --git a/manifests/capabilities/network.account.export.contribute.json b/manifests/capabilities/network.account.export.contribute.json
nnew file mode 100644
iindex 00000000..0f12ed53
---- /dev/null
++++ b/manifests/capabilities/network.account.export.contribute.json
@@@ -0,0 +1,21 @@
++{
++ "id": "network.account.export.contribute",
++ "version": "1.0",
++ "owner": "network",
++ "status": "active",
++ "visibility": "internal",
++ "description": "Push a service's part of a person's data export (ADR-033): the subject's own rows as JSON files, before the export's deadline. A token may only speak for its own service; a second part replaces the first.",
++ "permissions": [
++ "account:export"
++ ],
++ "resourceConstraints": [
++ "owner"
++ ],
++ "quotaClass": "account-data",
++ "events": [],
++ "implementedBy": [
++ "POST /internal/account-exports/:export_id/parts"
++ ],
++ "inputSchema": "network.account-export-part@1",
++ "outputSchema": "network.account-data-receipt@1"
++}
ddiff --git a/manifests/services/chat.json b/manifests/services/chat.json
iindex e44c2ed9..f720b113 100644
---- a/manifests/services/chat.json
++++ b/manifests/services/chat.json
@@@ -25,6 +25,8 @@
"chat.room.message.deleted"
],
"eventsConsumed": [
++ "network.account.deleted",
++ "network.account.export_requested",
"network.block.changed",
"network.user.token_valid_after"
],
ddiff --git a/manifests/services/community.json b/manifests/services/community.json
iindex 936def48..5eac40c2 100644
---- a/manifests/services/community.json
++++ b/manifests/services/community.json
@@@ -38,6 +38,8 @@
"community.thread.created"
],
"eventsConsumed": [
++ "network.account.deleted",
++ "network.account.export_requested",
"network.block.changed",
"network.user.token_valid_after"
],
ddiff --git a/manifests/services/games.json b/manifests/services/games.json
iindex a1d045f7..dee27db9 100644
---- a/manifests/services/games.json
++++ b/manifests/services/games.json
@@@ -47,6 +47,8 @@
"games.moderation.action"
],
"eventsConsumed": [
++ "network.account.deleted",
++ "network.account.export_requested",
"network.user.token_valid_after"
],
"namespacesOwned": [
ddiff --git a/manifests/services/live.json b/manifests/services/live.json
iindex f4cfb8c8..8ab2b801 100644
---- a/manifests/services/live.json
++++ b/manifests/services/live.json
@@@ -51,6 +51,8 @@
"media.clip.*",
"media.storage.*",
"media.vod.*",
++ "network.account.deleted",
++ "network.account.export_requested",
"network.user.token_valid_after",
"network.user.updated",
"openre.session.*"
ddiff --git a/manifests/services/media.json b/manifests/services/media.json
iindex b437f01e..66dfc443 100644
---- a/manifests/services/media.json
++++ b/manifests/services/media.json
@@@ -59,6 +59,8 @@
"media.vod.ready"
],
"eventsConsumed": [
++ "network.account.deleted",
++ "network.account.export_requested",
"network.user.token_valid_after"
],
"namespacesOwned": [
ddiff --git a/manifests/services/network.json b/manifests/services/network.json
iindex 1ca2b32d..2c92ff90 100644
---- a/manifests/services/network.json
++++ b/manifests/services/network.json
@@@ -34,6 +34,8 @@
"capabilities": [
"identity.subject.resolve",
"mods.grant.manage",
++ "network.account.deletion.confirm",
++ "network.account.export.contribute",
"network.analytics.creator.read",
"network.blocks.read",
"network.coins.credit",
@@@ -60,6 +62,8 @@
"network.follow.created",
"network.follow.deleted",
"network.subject.merged",
++ "network.account.export_requested",
++ "network.account.deleted",
"network.grant.changed",
"network.module.updated",
"network.notification.created",
ddiff --git a/package.json b/package.json
iindex ff46d342..6ad8b7ff 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-contracts",
-- "version": "0.70.0",
++ "version": "0.71.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",