openvibers/openvibe.contracts Commits

v0.36.0: the moderation audit log (ADR-022) — chat.moderation.action gets its payload contract (what Chat already sends for chat and Live moderation), community.moderation.action is new (staff actions on other people's content), Network consumes both plus tips.interaction.moderated and billing.staff.action; fixtures

OP OpenVibers committed 24 Sept 2026 · a350f35247 · 18 files, +338 −3


Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 833aa09b..4e8098cb 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -4,6 +4,15 @@ All notable changes to `openvibe-contracts`. Releases are git tags (`vX.Y.Z`) th
from `https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/<tag>`. Before v0.30.0,
the notes were in the tag and commit messages (`git tag -n1`).
++## 0.36.0 — 2026-09-24
++
++The moderation audit log (ADR-022):
++
++- **`chat.moderation.action`** now has a payload contract, matching what OpenVibe.Chat already sends for chat and Live moderation.
++- **`community.moderation.action`** is new: staff actions on other people's content in Community.
++
++OpenVibe.Network's manifest consumes both, plus `tips.interaction.moderated` and `billing.staff.action`, for the audit log staff can read in admin. Fixtures cover valid and invalid payloads. Additive.
++
## 0.35.0 — 2026-09-24
- OpenVibe.Community publishes events: `community.paste.created|updated|deleted`, `community.thread.created`,
ddiff --git a/contracts/catalog.json b/contracts/catalog.json
iindex 15270105..855df9bc 100644
---- a/contracts/catalog.json
++++ b/contracts/catalog.json
@@@ -1445,6 +1445,26 @@
"visibility": "first-party",
"compatibility": "backward",
"status": "active"
++ },
++ {
++ "id": "chat.moderation.action",
++ "version": "1.0.0",
++ "owner": "chat",
++ "schema": "events/payloads/chat.moderation.action.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-022"
++ },
++ {
++ "id": "community.moderation.action",
++ "version": "1.0.0",
++ "owner": "community",
++ "schema": "events/payloads/community.moderation.action.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-022"
}
]
}
ddiff --git a/contracts/events/payloads/chat.moderation.action.v1.json b/contracts/events/payloads/chat.moderation.action.v1.json
nnew file mode 100644
iindex 00000000..4a52b1c5
---- /dev/null
++++ b/contracts/events/payloads/chat.moderation.action.v1.json
@@@ -0,0 +1,20 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/chat.moderation.action.v1.json",
++ "title": "ChatModerationActionPayload",
++ "description": "chat.moderation.action v1 (OpenVibe.Chat server/db/database.js logModerationAction). A staff or channel moderator action in chat or in Live (Live's moderation log is written through the chat bridge): bans and unbans (site, channel, IP/CIDR), timeouts, message deletes and purges, slow/sub-only modes, and the like. OpenVibe.Network keeps it in the moderation audit log (ADR-022). Envelope: subject { type: moderation_action, id: <action_id> }, visibility internal, actor the acting person when known. details is free-form per action (never secrets or message text beyond what moderators saw).",
++ "type": "object",
++ "required": ["action_id", "action_type", "scope_type"],
++ "additionalProperties": false,
++ "properties": {
++ "action_id": { "type": "integer", "minimum": 1 },
++ "action_type": { "type": "string", "minLength": 1, "maxLength": 64, "description": "ban, unban, timeout, delete_message, purge, slow_mode, ip_ban, … as Chat and Live name them." },
++ "scope_type": { "type": "string", "minLength": 1, "maxLength": 32, "description": "site, channel, stream, room, …" },
++ "scope_id": { "type": ["string", "integer", "null"] },
++ "actor_user_id": { "type": ["integer", "null"], "description": "Legacy Live user id of the actor (kept while subjects are adopted)." },
++ "actor_subject": { "type": ["string", "null"] },
++ "target_user_id": { "type": ["integer", "null"] },
++ "target_subject": { "type": ["string", "null"] },
++ "details": { "type": "object" }
++ }
++}
ddiff --git a/contracts/events/payloads/community.moderation.action.v1.json b/contracts/events/payloads/community.moderation.action.v1.json
nnew file mode 100644
iindex 00000000..9839452f
---- /dev/null
++++ b/contracts/events/payloads/community.moderation.action.v1.json
@@@ -0,0 +1,25 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/community.moderation.action.v1.json",
++ "title": "CommunityModerationActionPayload",
++ "description": "community.moderation.action v1 (OpenVibe.Community server/events.js moderationAction). A staff action on someone else's content in Community: editing or deleting another person's paste, changing its visibility, censoring a screenshot, bulk actions, deleting forks, removing a comment, deleting or locking a thread. OpenVibe.Network keeps it in the moderation audit log (ADR-022). Envelope: subject { type: moderation_action, id: <target type>:<target id> }, visibility internal, actor the staff member. Never carries the content itself.",
++ "type": "object",
++ "required": ["action", "target", "actor_subject"],
++ "additionalProperties": false,
++ "properties": {
++ "action": { "enum": ["paste.edited", "paste.deleted", "paste.visibility_changed", "paste.censored", "pastes.bulk", "forks.deleted", "comment.deleted", "thread.deleted", "thread.locked", "thread.unlocked", "post.deleted"] },
++ "target": {
++ "type": "object",
++ "required": ["type", "id"],
++ "additionalProperties": false,
++ "properties": {
++ "type": { "enum": ["paste", "paste_comment", "comment", "thread", "post", "pastes"] },
++ "id": { "type": "string", "minLength": 1, "maxLength": 200 },
++ "owner_subject": { "type": ["string", "null"], "description": "Whose content it was, when known." }
++ }
++ },
++ "actor_subject": { "type": ["string", "null"], "description": "The staff member (null only for a service acting without a person)." },
++ "reason": { "type": ["string", "null"], "maxLength": 500 },
++ "details": { "type": "object", "description": "Per action: the fields changed, a bulk action's name and count, … never the content." }
++ }
++}
ddiff --git a/fixtures/chat.moderation.action/invalid/extra-field.json b/fixtures/chat.moderation.action/invalid/extra-field.json
nnew file mode 100644
iindex 00000000..bfa2c69d
---- /dev/null
++++ b/fixtures/chat.moderation.action/invalid/extra-field.json
@@@ -0,0 +1 @@
++{ "action_id": 1, "action_type": "ban", "scope_type": "site", "ip": "203.0.113.9" }
ddiff --git a/fixtures/chat.moderation.action/invalid/no-action-type.json b/fixtures/chat.moderation.action/invalid/no-action-type.json
nnew file mode 100644
iindex 00000000..74c184ad
---- /dev/null
++++ b/fixtures/chat.moderation.action/invalid/no-action-type.json
@@@ -0,0 +1 @@
++{ "action_id": 1, "scope_type": "site", "details": {} }
ddiff --git a/fixtures/chat.moderation.action/valid/channel-timeout.json b/fixtures/chat.moderation.action/valid/channel-timeout.json
nnew file mode 100644
iindex 00000000..de903f18
---- /dev/null
++++ b/fixtures/chat.moderation.action/valid/channel-timeout.json
@@@ -0,0 +1,11 @@
++{
++ "action_id": 813,
++ "action_type": "timeout",
++ "scope_type": "channel",
++ "scope_id": "327",
++ "actor_user_id": null,
++ "actor_subject": null,
++ "target_user_id": null,
++ "target_subject": null,
++ "details": {}
++}
ddiff --git a/fixtures/chat.moderation.action/valid/site-ban.json b/fixtures/chat.moderation.action/valid/site-ban.json
nnew file mode 100644
iindex 00000000..3719dcc7
---- /dev/null
++++ b/fixtures/chat.moderation.action/valid/site-ban.json
@@@ -0,0 +1,11 @@
++{
++ "action_id": 812,
++ "action_type": "ban",
++ "scope_type": "site",
++ "scope_id": null,
++ "actor_user_id": 1,
++ "actor_subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "target_user_id": 44,
++ "target_subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "details": { "reason": "spam", "duration": null }
++}
ddiff --git a/fixtures/community.moderation.action/invalid/carries-content.json b/fixtures/community.moderation.action/invalid/carries-content.json
nnew file mode 100644
iindex 00000000..1f5c7144
---- /dev/null
++++ b/fixtures/community.moderation.action/invalid/carries-content.json
@@@ -0,0 +1 @@
++{ "action": "paste.edited", "target": { "type": "paste", "id": "x" }, "actor_subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "content": "the paste text" }
ddiff --git a/fixtures/community.moderation.action/invalid/unknown-action.json b/fixtures/community.moderation.action/invalid/unknown-action.json
nnew file mode 100644
iindex 00000000..f558ba2a
---- /dev/null
++++ b/fixtures/community.moderation.action/invalid/unknown-action.json
@@@ -0,0 +1 @@
++{ "action": "paste.promoted", "target": { "type": "paste", "id": "x" }, "actor_subject": null }
ddiff --git a/fixtures/community.moderation.action/valid/bulk.json b/fixtures/community.moderation.action/valid/bulk.json
nnew file mode 100644
iindex 00000000..b88c02b5
---- /dev/null
++++ b/fixtures/community.moderation.action/valid/bulk.json
@@@ -0,0 +1,6 @@
++{
++ "action": "pastes.bulk",
++ "target": { "type": "pastes", "id": "bulk" },
++ "actor_subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "details": { "bulk_action": "private", "done": 12, "skipped": 1 }
++}
ddiff --git a/fixtures/community.moderation.action/valid/paste-deleted.json b/fixtures/community.moderation.action/valid/paste-deleted.json
nnew file mode 100644
iindex 00000000..98d1cf49
---- /dev/null
++++ b/fixtures/community.moderation.action/valid/paste-deleted.json
@@@ -0,0 +1,7 @@
++{
++ "action": "paste.deleted",
++ "target": { "type": "paste", "id": "k3f9Qa", "owner_subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" },
++ "actor_subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "reason": null,
++ "details": {}
++}
ddiff --git a/generated/json-schema/bundle.json b/generated/json-schema/bundle.json
iindex 6761202f..49e596d1 100644
---- a/generated/json-schema/bundle.json
++++ b/generated/json-schema/bundle.json
@@@ -16101,6 +16101,162 @@
}
}
}
++ },
++ "chat.moderation.action": {
++ "version": "1.0.0",
++ "owner": "chat",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/chat.moderation.action.v1.json",
++ "title": "ChatModerationActionPayload",
++ "description": "chat.moderation.action v1 (OpenVibe.Chat server/db/database.js logModerationAction). A staff or channel moderator action in chat or in Live (Live's moderation log is written through the chat bridge): bans and unbans (site, channel, IP/CIDR), timeouts, message deletes and purges, slow/sub-only modes, and the like. OpenVibe.Network keeps it in the moderation audit log (ADR-022). Envelope: subject { type: moderation_action, id: <action_id> }, visibility internal, actor the acting person when known. details is free-form per action (never secrets or message text beyond what moderators saw).",
++ "type": "object",
++ "required": [
++ "action_id",
++ "action_type",
++ "scope_type"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "action_id": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "action_type": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 64,
++ "description": "ban, unban, timeout, delete_message, purge, slow_mode, ip_ban, … as Chat and Live name them."
++ },
++ "scope_type": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 32,
++ "description": "site, channel, stream, room, …"
++ },
++ "scope_id": {
++ "type": [
++ "string",
++ "integer",
++ "null"
++ ]
++ },
++ "actor_user_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "description": "Legacy Live user id of the actor (kept while subjects are adopted)."
++ },
++ "actor_subject": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "target_user_id": {
++ "type": [
++ "integer",
++ "null"
++ ]
++ },
++ "target_subject": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "details": {
++ "type": "object"
++ }
++ }
++ }
++ },
++ "community.moderation.action": {
++ "version": "1.0.0",
++ "owner": "community",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/community.moderation.action.v1.json",
++ "title": "CommunityModerationActionPayload",
++ "description": "community.moderation.action v1 (OpenVibe.Community server/events.js moderationAction). A staff action on someone else's content in Community: editing or deleting another person's paste, changing its visibility, censoring a screenshot, bulk actions, deleting forks, removing a comment, deleting or locking a thread. OpenVibe.Network keeps it in the moderation audit log (ADR-022). Envelope: subject { type: moderation_action, id: <target type>:<target id> }, visibility internal, actor the staff member. Never carries the content itself.",
++ "type": "object",
++ "required": [
++ "action",
++ "target",
++ "actor_subject"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "action": {
++ "enum": [
++ "paste.edited",
++ "paste.deleted",
++ "paste.visibility_changed",
++ "paste.censored",
++ "pastes.bulk",
++ "forks.deleted",
++ "comment.deleted",
++ "thread.deleted",
++ "thread.locked",
++ "thread.unlocked",
++ "post.deleted"
++ ]
++ },
++ "target": {
++ "type": "object",
++ "required": [
++ "type",
++ "id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {
++ "enum": [
++ "paste",
++ "paste_comment",
++ "comment",
++ "thread",
++ "post",
++ "pastes"
++ ]
++ },
++ "id": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 200
++ },
++ "owner_subject": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "Whose content it was, when known."
++ }
++ }
++ },
++ "actor_subject": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "The staff member (null only for a service acting without a person)."
++ },
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 500
++ },
++ "details": {
++ "type": "object",
++ "description": "Per action: the fields changed, a bulk action's name and count, … never the content."
++ }
++ }
++ }
}
}
}
ddiff --git a/generated/typescript/index.d.ts b/generated/typescript/index.d.ts
iindex 39652cb4..ecd226b2 100644
---- a/generated/typescript/index.d.ts
++++ b/generated/typescript/index.d.ts
@@@ -13131,3 +13131,64 @@ export interface CommunityCommentCreatedPayload {
*/
url: string | null;
}
++
++/** [email protected] (owner: chat) */
++/**
++ * chat.moderation.action v1 (OpenVibe.Chat server/db/database.js logModerationAction). A staff or channel moderator action in chat or in Live (Live's moderation log is written through the chat bridge): bans and unbans (site, channel, IP/CIDR), timeouts, message deletes and purges, slow/sub-only modes, and the like. OpenVibe.Network keeps it in the moderation audit log (ADR-022). Envelope: subject { type: moderation_action, id: <action_id> }, visibility internal, actor the acting person when known. details is free-form per action (never secrets or message text beyond what moderators saw).
++ */
++export interface ChatModerationActionPayload {
++ action_id: number;
++ /**
++ * ban, unban, timeout, delete_message, purge, slow_mode, ip_ban, … as Chat and Live name them.
++ */
++ action_type: string;
++ /**
++ * site, channel, stream, room, …
++ */
++ scope_type: string;
++ scope_id?: string | number | null;
++ /**
++ * Legacy Live user id of the actor (kept while subjects are adopted).
++ */
++ actor_user_id?: number | null;
++ actor_subject?: string | null;
++ target_user_id?: number | null;
++ target_subject?: string | null;
++ details?: {};
++}
++
++/** [email protected] (owner: community) */
++/**
++ * community.moderation.action v1 (OpenVibe.Community server/events.js moderationAction). A staff action on someone else's content in Community: editing or deleting another person's paste, changing its visibility, censoring a screenshot, bulk actions, deleting forks, removing a comment, deleting or locking a thread. OpenVibe.Network keeps it in the moderation audit log (ADR-022). Envelope: subject { type: moderation_action, id: <target type>:<target id> }, visibility internal, actor the staff member. Never carries the content itself.
++ */
++export interface CommunityModerationActionPayload {
++ action:
++ | "paste.edited"
++ | "paste.deleted"
++ | "paste.visibility_changed"
++ | "paste.censored"
++ | "pastes.bulk"
++ | "forks.deleted"
++ | "comment.deleted"
++ | "thread.deleted"
++ | "thread.locked"
++ | "thread.unlocked"
++ | "post.deleted";
++ target: {
++ type: "paste" | "paste_comment" | "comment" | "thread" | "post" | "pastes";
++ id: string;
++ /**
++ * Whose content it was, when known.
++ */
++ owner_subject?: string | null;
++ };
++ /**
++ * The staff member (null only for a service acting without a person).
++ */
++ actor_subject: string | null;
++ reason?: string | null;
++ /**
++ * Per action: the fields changed, a bulk action's name and count, … never the content.
++ */
++ details?: {};
++}
ddiff --git a/manifests/services/community.json b/manifests/services/community.json
iindex fba7b510..82faab2c 100644
---- a/manifests/services/community.json
++++ b/manifests/services/community.json
@@@ -28,6 +28,7 @@
],
"eventsProduced": [
"community.comment.created",
++ "community.moderation.action",
"community.paste.created",
"community.paste.deleted",
"community.paste.updated",
ddiff --git a/manifests/services/network.json b/manifests/services/network.json
iindex 5a32c8c8..dd6f6882 100644
---- a/manifests/services/network.json
++++ b/manifests/services/network.json
@@@ -38,8 +38,12 @@
"network.module.updated"
],
"eventsConsumed": [
++ "billing.staff.action",
++ "chat.moderation.action",
++ "community.moderation.action",
"deals.watch.matched",
"live.stream.started",
++ "tips.interaction.moderated",
"trade.alert.triggered"
],
"namespacesOwned": [
ddiff --git a/package-lock.json b/package-lock.json
iindex 752fb017..f0e5ba45 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-contracts",
-- "version": "0.35.0",
++ "version": "0.36.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-contracts",
-- "version": "0.35.0",
++ "version": "0.36.0",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex 296af56a..390cecd1 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-contracts",
-- "version": "0.35.0",
++ "version": "0.36.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",