openvibers/openvibe.games Commits
Moderation audit (ADR-022, WS-D task 1): games.moderation.action from the outbox in the same transaction when staff revoke (mod.revoked, with the reason and the grants it ended), disable (mod.disabled) or put back a disabled (mod.enabled) mod someone else published, the publisher as owner_subject; installs, first enables, grant changes and the publisher acting on their own mod report nothing. Declared in the service manifest proposal. Pins: openvibe-contracts 0.49.0 → 0.53.0
Changes
nnew file mode 100644@@@ -0,0 +1,2 @@++export {};++//# sourceMappingURL=moderationEvents.test.d.ts.map\\ No newline at end of filennew file mode 100644@@@ -0,0 +1 @@++{"version":3,"file":"moderationEvents.test.d.ts","sourceRoot":"","sources":["../../src/mods/moderationEvents.test.ts"],"names":[],"mappings":""}\\ No newline at end of file@@@ -11,7 +11,10 @@** Every change is one transaction holding the registry rows, the audit rows* and the `games.mod.*` outbox event. The in-memory view the runtime asks on-- * its hot path (`isGranted`) is refreshed only after that commit.++ * its hot path (`isGranted`) is refreshed only after that commit. Taking down++ * (revoke, disable) or putting back (enable after a disable) a mod someone++ * else published also writes `games.moderation.action` for Network's++ * moderation audit log (ADR-022) in that transaction.** Trust tiers are metadata: `isGranted` never looks at them.*@@@ -89,6 +92,8 @@ export declare class ModRegistry {private assertGrantable;private require;private writeAudit;++ /** games.moderation.action, unless the publisher is acting on their own mod. */++ private moderated;private emit;/** Re-reads one install after a committed change. */private refresh;@@@ -1 +1 @@--{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/mods/registry.ts"],"names":[],"mappings":"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"}\\ No newline at end of file++{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/mods/registry.ts"],"names":[],"mappings":"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"}\\ No newline at end of file@@@ -8,6 +8,9 @@* games.blueprint.unlocked a persisted blueprint recipe unlock* games.world.saved the authoritative world was written (checkpoint / shutdown)* games.mod.* mod install, enable, disable, grant changes and revoke++ * games.moderation.action staff took down or put back a mod someone else published++ * (common.moderation-action@1, ADR-022), for Network's moderation++ * audit log** Scraplandia has no achievement system, so there is no games.achievement.** event: skill levels and blueprint unlocks are the real progression@@@ -63,6 +66,18 @@ export declare function modEvent(kind: ModLifecycle, mod: {target: string;trustTier: string;}, actor: SubjectRef, detail: Record<string, unknown>): EventInput;++/**++ * games.moderation.action: one staff action on someone else's content. `actor` is the staff++ * member (a service when no person is known); the payload never carries the content itself.++ */++export declare function moderationEvent(action: string, target: {++ type: string;++ id: string;++ ownerSubject: string | null;++}, actor: SubjectRef, opts?: {++ reason?: string | null;++ details?: Record<string, unknown>;++}): EventInput;/** Where events go. `enqueue` must run inside the transaction making the change. */export interface EventSink {readonly enabled: boolean;@@@ -1 +1 @@--{"version":3,"file":"gameEvents.d.ts","sourceRoot":"","sources":["../../src/platform/gameEvents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAC7D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAEnD,eAAO,MAAM,YAAY,UAAU,CAAA;AAEnC,yCAAyC;AACzC,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,uEAAuE;IACvE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,yDAAyD;AACzD,MAAM,WAAW,gBAAgB;IAC/B,0BAA0B;IAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,qCAAqC;IACrC,OAAO,EAAE,MAAM,EAAE,CAAA;CAClB;AAID,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,UAAU,CAI7D;AAkBD,wBAAgB,iBAAiB,CAC/B,CAAC,EAAE,WAAW,EACd,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,GACzC,UAAU,CASZ;AAED,wBAAgB,eAAe,CAC7B,CAAC,EAAE,WAAW,EACd,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,GAC9C,UAAU,CAaZ;AAED,iFAAiF;AACjF,wBAAgB,cAAc,CAC5B,CAAC,EAAE,WAAW,EACd,MAAM,EAAE,gBAAgB,EACxB,KAAK,EAAE,gBAAgB,EACvB,KAAK,EAAE,MAAM,GACZ,UAAU,EAAE,CA4Bd;AAED,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,EACb,IAAI,EAAE;IAAE,MAAM,EAAE,YAAY,GAAG,UAAU,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC9F,UAAU,CAeZ;AAED,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,gBAAgB,CAAA;AAE9F,wBAAgB,QAAQ,CACtB,IAAI,EAAE,YAAY,EAClB,GAAG,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,EACrF,KAAK,EAAE,UAAU,EACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,UAAU,CAkBZ;AAED,oFAAoF;AACpF,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,OAAO,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAA;CACjC;AAED,eAAO,MAAM,SAAS,EAAE,SAAiD,CAAA;AAEzE,+EAA+E;AAC/E,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAQpD;AAQD;;;;;GAKG;AACH,qBAAa,iBAAiB;IAQ1B,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAClC,OAAO,CAAC,QAAQ,CAAC,GAAG;IAVtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6B;IACrD,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,eAAe,CAAI;IAC3B,OAAO,CAAC,cAAc,CAAI;IAC1B,OAAO,CAAC,gBAAgB,CAAQ;gBAGb,IAAI,EAAE,SAAS,EACf,KAAK,EAAE,MAAM,EACb,iBAAiB,EAAE,MAAM,EACzB,GAAG,GAAE,MAAM,MAAiB;IAM/C,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,wEAAwE;IACxE,UAAU,CAAC,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,gBAAgB,EAAE,QAAQ,EAAE,OAAO,GAAG,IAAI;IASpF;;;OAGG;IACH,kBAAkB,CAAC,KAAK,EAAE;QAAE,MAAM,EAAE,WAAW,CAAC;QAAC,QAAQ,EAAE,gBAAgB,CAAA;KAAE,EAAE,GAAG,MAAM,IAAI;IAmB5F,0EAA0E;IAC1E,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,IAAI;IAS5C;;;;OAIG;IACH,gBAAgB,CACd,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,YAAY,GAAG,UAAU,GAChC,MAAM,IAAI;CAwBd"}\\ No newline at end of file++{"version":3,"file":"gameEvents.d.ts","sourceRoot":"","sources":["../../src/platform/gameEvents.ts"],"names":[],"mappings":"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"}\\ No newline at end of file@@@ -19,7 +19,7 @@"@openvibe/shared": "workspace:*","ajv": "^8.17.1","better-sqlite3": "^11.9.1",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.53.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0","tsx": "^4.19.3","ws": "^8.18.1"nnew file mode 100644@@@ -0,0 +1,125 @@++/**++ * games.moderation.action (ADR-022): staff taking down or putting back a mod++ * someone else published goes to Network's moderation audit log, from the++ * outbox in the same transaction; the publisher acting on their own mod does++ * not.++ */++import { createRequire } from 'node:module'++import { createContent } from '@openvibe/content'++import { openSqliteStore } from '@openvibe/persistence/sqlite'++import { createClient } from 'openvibe-sdk/core'++import { createEventsClient, createOutbox, type SqliteDatabase } from 'openvibe-sdk/events'++import { describe, expect, it } from 'vitest'++import { EVENT_SOURCE, outboxSink } from '../platform/gameEvents.js'++import { CAP_PLACE_PROP } from './contentPack.js'++import { ModRegistry, type ModActor } from './registry.js'++import { MOD_ID, sampleManifest } from './testFixtures.js'++++interface Contracts {++ validate(ref: string, value: unknown): { valid: boolean; errors: unknown[] }++}++const contracts = createRequire(import.meta.url)('openvibe-contracts') as Contracts++++const PUBLISHER = sampleManifest().publisher.id++const STAFF_SUBJECT = 'usr_01JABCDEFGHJKMNPQRSTVWXYZ9'++const STAFF: ModActor = { audit: STAFF_SUBJECT, subject: { type: 'user', id: STAFF_SUBJECT } }++const OWNER: ModActor = { audit: PUBLISHER, subject: { type: 'user', id: PUBLISHER } }++const PACK = {++ props: [{ key: 'bench', item: 'workbench', pos: [10, 0, 10] as [number, number, number] }],++}++++interface Envelope {++ event_type: string++ source: string++ visibility: string++ actor: unknown++ subject: unknown++ payload: {++ action: string++ target: unknown++ actor_subject: string | null++ reason: string | null++ details: Record<string, unknown>++ }++}++++function setup() {++ const store = openSqliteStore(':memory:')++ const client = createClient({++ baseUrls: { events: 'http://127.0.0.1:9' },++ autoDiscover: false,++ retries: 0,++ getToken: async () => {++ throw new Error('no relay in this test')++ },++ })++ const outbox = createOutbox(store.db as unknown as SqliteDatabase, {++ events: createEventsClient(client, { source: EVENT_SOURCE }),++ })++ outbox.ensureSchema()++ const registry = new ModRegistry(store, createContent(), outboxSink(outbox))++ registry.install(++ { manifest: sampleManifest(), pack: PACK, approve: [CAP_PLACE_PROP], enable: true },++ STAFF,++ )++ const moderation = () =>++ (++ store.db.prepare('SELECT envelope FROM event_outbox ORDER BY id').all() as {++ envelope: string++ }[]++ )++ .map((r) => JSON.parse(r.envelope) as Envelope)++ .filter((e) => e.event_type === 'games.moderation.action')++ return { registry, outbox, moderation }++}++++function expectValid(e: Envelope) {++ expect(contracts.validate('events.event-envelope@1', e).errors).toEqual([])++ expect(contracts.validate('games.moderation.action@1', e.payload).errors).toEqual([])++ expect(e.source).toBe('games')++ expect(e.visibility).toBe('internal')++ expect(e.actor).toEqual({ type: 'user', id: STAFF_SUBJECT })++ expect(e.subject).toEqual({ type: 'moderation_action', id: `mod:${MOD_ID}` })++ expect(e.payload.actor_subject).toBe(STAFF_SUBJECT)++ expect(e.payload.target).toEqual({ type: 'mod', id: MOD_ID, owner_subject: PUBLISHER })++}++++describe('games.moderation.action', () => {++ it("staff revoking someone else's mod enqueues exactly one valid event", () => {++ const { registry, outbox, moderation } = setup()++ registry.revoke(MOD_ID, STAFF, 'griefing props')++ const ev = moderation()++ expect(ev).toHaveLength(1)++ expectValid(ev[0]!)++ expect(ev[0]!.payload.action).toBe('mod.revoked')++ expect(ev[0]!.payload.reason).toBe('griefing props')++ expect(ev[0]!.payload.details).toEqual({++ previous: 'enabled',++ revoked_grants: [CAP_PLACE_PROP],++ })++ registry.revoke(MOD_ID, STAFF, 'again')++ expect(moderation()).toHaveLength(1)++ void outbox.stop()++ })++++ it('a disable, and the enable that puts it back, are one event each; installing is none', () => {++ const { registry, outbox, moderation } = setup()++ expect(moderation()).toHaveLength(0)++ registry.disable(MOD_ID, STAFF)++ expect(moderation().map((e) => e.payload.action)).toEqual(['mod.disabled'])++ registry.enable(MOD_ID, STAFF)++ const ev = moderation()++ expect(ev.map((e) => e.payload.action)).toEqual(['mod.disabled', 'mod.enabled'])++ for (const e of ev) expectValid(e)++ void outbox.stop()++ })++++ it('the publisher acting on their own mod enqueues none', () => {++ const { registry, outbox, moderation } = setup()++ registry.disable(MOD_ID, OWNER)++ registry.enable(MOD_ID, OWNER)++ registry.revoke(MOD_ID, OWNER, 'withdrawn by its author')++ expect(moderation()).toHaveLength(0)++ void outbox.stop()++ })++})@@@ -11,7 +11,10 @@** Every change is one transaction holding the registry rows, the audit rows* and the `games.mod.*` outbox event. The in-memory view the runtime asks on-- * its hot path (`isGranted`) is refreshed only after that commit.++ * its hot path (`isGranted`) is refreshed only after that commit. Taking down++ * (revoke, disable) or putting back (enable after a disable) a mod someone++ * else published also writes `games.moderation.action` for Network's++ * moderation audit log (ADR-022) in that transaction.** Trust tiers are metadata: `isGranted` never looks at them.*@@@ -30,7 +33,13 @@ import type {PersistenceStore,} from '@openvibe/persistence'import type { SubjectRef } from 'openvibe-sdk/core'--import { modEvent, NO_EVENTS, type EventSink, type ModLifecycle } from '../platform/gameEvents.js'++import {++ moderationEvent,++ modEvent,++ NO_EVENTS,++ type EventSink,++ type ModLifecycle,++} from '../platform/gameEvents.js'import {capabilitiesUsedBy,CONTENT_RUNTIME_CAPABILITIES,@@@ -229,6 +238,10 @@ export class ModRegistry {revoked_grants: [...view.granted].sort(),...(reason ? { reason } : {}),})++ this.moderated('mod.revoked', view, actor, reason, {++ previous: view.mod.status,++ revoked_grants: [...view.granted].sort(),++ })})return this.refresh(id)}@@@ -270,10 +283,17 @@ export class ModRegistry {if (view.mod.status === status) return viewconst at = this.now()this.store.transaction(() => {++ // Enabling puts a mod back only when someone disabled it before (a first enable is not moderation).++ const restoring =++ status === 'enabled' &&++ this.store.mods.auditLog(id, 10_000).some((a: ModAuditDto) => a.action === 'disable')this.store.mods.setStatus(id, status, at)const action = status === 'enabled' ? 'enable' : 'disable'this.writeAudit(id, action, null, actor, null)this.emit(status === 'enabled' ? 'enabled' : 'disabled', view.mod, actor, {})++ if (status === 'disabled' || restoring) {++ this.moderated(`mod.${status}`, view, actor, undefined, { previous: view.mod.status })++ }})return this.refresh(id)}@@@ -314,6 +334,30 @@ export class ModRegistry {})}++ /** games.moderation.action, unless the publisher is acting on their own mod. */++ private moderated(++ action: string,++ view: ModView,++ actor: ModActor,++ reason: string | undefined,++ details: Record<string, unknown>,++ ): void {++ const publisher = view.manifest.publisher++ const owner = publisher && publisher.type === 'user' ? publisher.id : null++ if (owner && actor.subject.type === 'user' && actor.subject.id === owner) return++ this.events.enqueue(++ moderationEvent(++ action,++ { type: 'mod', id: view.mod.id, ownerSubject: owner },++ actor.subject,++ {++ reason: reason ?? null,++ details,++ },++ ),++ )++ }++private emit(kind: ModLifecycle,mod: ModInstallDto,@@@ -8,6 +8,9 @@* games.blueprint.unlocked a persisted blueprint recipe unlock* games.world.saved the authoritative world was written (checkpoint / shutdown)* games.mod.* mod install, enable, disable, grant changes and revoke++ * games.moderation.action staff took down or put back a mod someone else published++ * (common.moderation-action@1, ADR-022), for Network's moderation++ * audit log** Scraplandia has no achievement system, so there is no games.achievement.** event: skill levels and blueprint unlocks are the real progression@@@ -181,6 +184,32 @@ export function modEvent(}}++/**++ * games.moderation.action: one staff action on someone else's content. `actor` is the staff++ * member (a service when no person is known); the payload never carries the content itself.++ */++export function moderationEvent(++ action: string,++ target: { type: string; id: string; ownerSubject: string | null },++ actor: SubjectRef,++ opts: { reason?: string | null; details?: Record<string, unknown> } = {},++): EventInput {++ const t = { type: target.type, id: target.id.slice(0, 200), owner_subject: target.ownerSubject }++ return {++ event_type: 'games.moderation.action',++ actor,++ subject: { type: 'moderation_action', id: `${t.type}:${t.id}`.slice(0, 200) },++ visibility: 'internal',++ payload: {++ action,++ target: t,++ actor_subject: actor.type === 'user' ? actor.id : null,++ reason: opts.reason ? opts.reason.slice(0, 500) : null,++ details: opts.details ?? {},++ },++ }++}++/** Where events go. `enqueue` must run inside the transaction making the change. */export interface EventSink {readonly enabled: boolean@@@ -26,7 +26,8 @@"games.mod.enabled","games.mod.disabled","games.mod.grants_changed",-- "games.mod.revoked"++ "games.mod.revoked",++ "games.moderation.action"],"eventsConsumed": [],"namespacesOwned": ["games.*"],@@@ -112,8 +112,8 @@ importers:specifier: ^11.9.1version: 11.10.0openvibe-contracts:-- specifier: https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.0-- version: https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.0++ specifier: https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.53.0++ version: https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.53.0openvibe-sdk:specifier: https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0version: https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0@@@ -1326,9 +1326,9 @@ packages:resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}-- openvibe-contracts@https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.0:-- resolution: {tarball: https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.0}-- version: 0.49.0++ openvibe-contracts@https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.53.0:++ resolution: {tarball: https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.53.0}++ version: 0.53.0engines: {node: '>=20'}hasBin: true@@@ -2742,7 +2742,7 @@ snapshots:dependencies:wrappy: 1.0.2-- openvibe-contracts@https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.0:++ openvibe-contracts@https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.53.0:dependencies:ajv: 8.20.0ajv-formats: 3.0.1([email protected])
new file mode 100644
new file mode 100644
@@ -0,0 +1,2 @@
@@ -0,0 +1,2 @@
+export {};
+//# sourceMappingURL=moderationEvents.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{"version":3,"file":"moderationEvents.test.d.ts","sourceRoot":"","sources":["../../src/mods/moderationEvents.test.ts"],"names":[],"mappings":""}
\ No newline at end of file
\ No newline at end of file
@@ -11,7 +11,10 @@
@@ -11,7 +11,10 @@
*
*
* Every change is one transaction holding the registry rows, the audit rows
* Every change is one transaction holding the registry rows, the audit rows
* and the `games.mod.*` outbox event. The in-memory view the runtime asks on
* and the `games.mod.*` outbox event. The in-memory view the runtime asks on
- * its hot path (`isGranted`) is refreshed only after that commit.
+ * its hot path (`isGranted`) is refreshed only after that commit. Taking down
+ * (revoke, disable) or putting back (enable after a disable) a mod someone
+ * else published also writes `games.moderation.action` for Network's
+ * moderation audit log (ADR-022) in that transaction.
*
*
* Trust tiers are metadata: `isGranted` never looks at them.
* Trust tiers are metadata: `isGranted` never looks at them.
*
*
@@ -89,6 +92,8 @@ export declare class ModRegistry {
@@ -89,6 +92,8 @@ export declare class ModRegistry {
private assertGrantable;
private assertGrantable;
private require;
private require;
private writeAudit;
private writeAudit;
+ /** games.moderation.action, unless the publisher is acting on their own mod. */
+ private moderated;
private emit;
private emit;
/** Re-reads one install after a committed change. */
/** Re-reads one install after a committed change. */
private refresh;
private refresh;
@@ -1 +1 @@
@@ -1 +1 @@
-{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/mods/registry.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
\ No newline at end of file
+{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/mods/registry.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
\ No newline at end of file
@@ -8,6 +8,9 @@
@@ -8,6 +8,9 @@
* games.blueprint.unlocked a persisted blueprint recipe unlock
* games.blueprint.unlocked a persisted blueprint recipe unlock
* games.world.saved the authoritative world was written (checkpoint / shutdown)
* games.world.saved the authoritative world was written (checkpoint / shutdown)
* games.mod.* mod install, enable, disable, grant changes and revoke
* games.mod.* mod install, enable, disable, grant changes and revoke
+ * games.moderation.action staff took down or put back a mod someone else published
+ * (common.moderation-action@1, ADR-022), for Network's moderation
+ * audit log
*
*
* Scraplandia has no achievement system, so there is no games.achievement.*
* Scraplandia has no achievement system, so there is no games.achievement.*
* event: skill levels and blueprint unlocks are the real progression
* event: skill levels and blueprint unlocks are the real progression
@@ -63,6 +66,18 @@ export declare function modEvent(kind: ModLifecycle, mod: {
@@ -63,6 +66,18 @@ export declare function modEvent(kind: ModLifecycle, mod: {
target: string;
target: string;
trustTier: string;
trustTier: string;
}, actor: SubjectRef, detail: Record<string, unknown>): EventInput;
}, actor: SubjectRef, detail: Record<string, unknown>): EventInput;
+/**
+ * games.moderation.action: one staff action on someone else's content. `actor` is the staff
+ * member (a service when no person is known); the payload never carries the content itself.
+ */
+export declare function moderationEvent(action: string, target: {
+ type: string;
+ id: string;
+ ownerSubject: string | null;
+}, actor: SubjectRef, opts?: {
+ reason?: string | null;
+ details?: Record<string, unknown>;
+}): EventInput;
/** Where events go. `enqueue` must run inside the transaction making the change. */
/** Where events go. `enqueue` must run inside the transaction making the change. */
export interface EventSink {
export interface EventSink {
readonly enabled: boolean;
readonly enabled: boolean;
@@ -1 +1 @@
@@ -1 +1 @@
-{"version":3,"file":"gameEvents.d.ts","sourceRoot":"","sources":["../../src/platform/gameEvents.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
\ No newline at end of file
+{"version":3,"file":"gameEvents.d.ts","sourceRoot":"","sources":["../../src/platform/gameEvents.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
\ No newline at end of file
@@ -19,7 +19,7 @@
@@ -19,7 +19,7 @@
"@openvibe/shared": "workspace:*",
"@openvibe/shared": "workspace:*",
"ajv": "^8.17.1",
"ajv": "^8.17.1",
"better-sqlite3": "^11.9.1",
"better-sqlite3": "^11.9.1",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.53.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0",
"tsx": "^4.19.3",
"tsx": "^4.19.3",
"ws": "^8.18.1"
"ws": "^8.18.1"
new file mode 100644
new file mode 100644
@@ -0,0 +1,125 @@
@@ -0,0 +1,125 @@
+/**
+ * games.moderation.action (ADR-022): staff taking down or putting back a mod
+ * someone else published goes to Network's moderation audit log, from the
+ * outbox in the same transaction; the publisher acting on their own mod does
+ * not.
+ */
+import { createRequire } from 'node:module'
+import { createContent } from '@openvibe/content'
+import { openSqliteStore } from '@openvibe/persistence/sqlite'
+import { createClient } from 'openvibe-sdk/core'
+import { createEventsClient, createOutbox, type SqliteDatabase } from 'openvibe-sdk/events'
+import { describe, expect, it } from 'vitest'
+import { EVENT_SOURCE, outboxSink } from '../platform/gameEvents.js'
+import { CAP_PLACE_PROP } from './contentPack.js'
+import { ModRegistry, type ModActor } from './registry.js'
+import { MOD_ID, sampleManifest } from './testFixtures.js'
+
+interface Contracts {
+ validate(ref: string, value: unknown): { valid: boolean; errors: unknown[] }
+}
+const contracts = createRequire(import.meta.url)('openvibe-contracts') as Contracts
+
+const PUBLISHER = sampleManifest().publisher.id
+const STAFF_SUBJECT = 'usr_01JABCDEFGHJKMNPQRSTVWXYZ9'
+const STAFF: ModActor = { audit: STAFF_SUBJECT, subject: { type: 'user', id: STAFF_SUBJECT } }
+const OWNER: ModActor = { audit: PUBLISHER, subject: { type: 'user', id: PUBLISHER } }
+const PACK = {
+ props: [{ key: 'bench', item: 'workbench', pos: [10, 0, 10] as [number, number, number] }],
+}
+
+interface Envelope {
+ event_type: string
+ source: string
+ visibility: string
+ actor: unknown
+ subject: unknown
+ payload: {
+ action: string
+ target: unknown
+ actor_subject: string | null
+ reason: string | null
+ details: Record<string, unknown>
+ }
+}
+
+function setup() {
+ const store = openSqliteStore(':memory:')
+ const client = createClient({
+ baseUrls: { events: 'http://127.0.0.1:9' },
+ autoDiscover: false,
+ retries: 0,
+ getToken: async () => {
+ throw new Error('no relay in this test')
+ },
+ })
+ const outbox = createOutbox(store.db as unknown as SqliteDatabase, {
+ events: createEventsClient(client, { source: EVENT_SOURCE }),
+ })
+ outbox.ensureSchema()
+ const registry = new ModRegistry(store, createContent(), outboxSink(outbox))
+ registry.install(
+ { manifest: sampleManifest(), pack: PACK, approve: [CAP_PLACE_PROP], enable: true },
+ STAFF,
+ )
+ const moderation = () =>
+ (
+ store.db.prepare('SELECT envelope FROM event_outbox ORDER BY id').all() as {
+ envelope: string
+ }[]
+ )
+ .map((r) => JSON.parse(r.envelope) as Envelope)
+ .filter((e) => e.event_type === 'games.moderation.action')
+ return { registry, outbox, moderation }
+}
+
+function expectValid(e: Envelope) {
+ expect(contracts.validate('events.event-envelope@1', e).errors).toEqual([])
+ expect(contracts.validate('games.moderation.action@1', e.payload).errors).toEqual([])
+ expect(e.source).toBe('games')
+ expect(e.visibility).toBe('internal')
+ expect(e.actor).toEqual({ type: 'user', id: STAFF_SUBJECT })
+ expect(e.subject).toEqual({ type: 'moderation_action', id: `mod:${MOD_ID}` })
+ expect(e.payload.actor_subject).toBe(STAFF_SUBJECT)
+ expect(e.payload.target).toEqual({ type: 'mod', id: MOD_ID, owner_subject: PUBLISHER })
+}
+
+describe('games.moderation.action', () => {
+ it("staff revoking someone else's mod enqueues exactly one valid event", () => {
+ const { registry, outbox, moderation } = setup()
+ registry.revoke(MOD_ID, STAFF, 'griefing props')
+ const ev = moderation()
+ expect(ev).toHaveLength(1)
+ expectValid(ev[0]!)
+ expect(ev[0]!.payload.action).toBe('mod.revoked')
+ expect(ev[0]!.payload.reason).toBe('griefing props')
+ expect(ev[0]!.payload.details).toEqual({
+ previous: 'enabled',
+ revoked_grants: [CAP_PLACE_PROP],
+ })
+ registry.revoke(MOD_ID, STAFF, 'again')
+ expect(moderation()).toHaveLength(1)
+ void outbox.stop()
+ })
+
+ it('a disable, and the enable that puts it back, are one event each; installing is none', () => {
+ const { registry, outbox, moderation } = setup()
+ expect(moderation()).toHaveLength(0)
+ registry.disable(MOD_ID, STAFF)
+ expect(moderation().map((e) => e.payload.action)).toEqual(['mod.disabled'])
+ registry.enable(MOD_ID, STAFF)
+ const ev = moderation()
+ expect(ev.map((e) => e.payload.action)).toEqual(['mod.disabled', 'mod.enabled'])
+ for (const e of ev) expectValid(e)
+ void outbox.stop()
+ })
+
+ it('the publisher acting on their own mod enqueues none', () => {
+ const { registry, outbox, moderation } = setup()
+ registry.disable(MOD_ID, OWNER)
+ registry.enable(MOD_ID, OWNER)
+ registry.revoke(MOD_ID, OWNER, 'withdrawn by its author')
+ expect(moderation()).toHaveLength(0)
+ void outbox.stop()
+ })
+})
@@ -11,7 +11,10 @@
@@ -11,7 +11,10 @@
*
*
* Every change is one transaction holding the registry rows, the audit rows
* Every change is one transaction holding the registry rows, the audit rows
* and the `games.mod.*` outbox event. The in-memory view the runtime asks on
* and the `games.mod.*` outbox event. The in-memory view the runtime asks on
- * its hot path (`isGranted`) is refreshed only after that commit.
+ * its hot path (`isGranted`) is refreshed only after that commit. Taking down
+ * (revoke, disable) or putting back (enable after a disable) a mod someone
+ * else published also writes `games.moderation.action` for Network's
+ * moderation audit log (ADR-022) in that transaction.
*
*
* Trust tiers are metadata: `isGranted` never looks at them.
* Trust tiers are metadata: `isGranted` never looks at them.
*
*
@@ -30,7 +33,13 @@ import type {
@@ -30,7 +33,13 @@ import type {
PersistenceStore,
PersistenceStore,
} from '@openvibe/persistence'
} from '@openvibe/persistence'
import type { SubjectRef } from 'openvibe-sdk/core'
import type { SubjectRef } from 'openvibe-sdk/core'
-import { modEvent, NO_EVENTS, type EventSink, type ModLifecycle } from '../platform/gameEvents.js'
+import {
+ moderationEvent,
+ modEvent,
+ NO_EVENTS,
+ type EventSink,
+ type ModLifecycle,
+} from '../platform/gameEvents.js'
import {
import {
capabilitiesUsedBy,
capabilitiesUsedBy,
CONTENT_RUNTIME_CAPABILITIES,
CONTENT_RUNTIME_CAPABILITIES,
@@ -229,6 +238,10 @@ export class ModRegistry {
@@ -229,6 +238,10 @@ export class ModRegistry {
revoked_grants: [...view.granted].sort(),
revoked_grants: [...view.granted].sort(),
...(reason ? { reason } : {}),
...(reason ? { reason } : {}),
})
})
+ this.moderated('mod.revoked', view, actor, reason, {
+ previous: view.mod.status,
+ revoked_grants: [...view.granted].sort(),
+ })
})
})
return this.refresh(id)
return this.refresh(id)
}
}
@@ -270,10 +283,17 @@ export class ModRegistry {
@@ -270,10 +283,17 @@ export class ModRegistry {
if (view.mod.status === status) return view
if (view.mod.status === status) return view
const at = this.now()
const at = this.now()
this.store.transaction(() => {
this.store.transaction(() => {
+ // Enabling puts a mod back only when someone disabled it before (a first enable is not moderation).
+ const restoring =
+ status === 'enabled' &&
+ this.store.mods.auditLog(id, 10_000).some((a: ModAuditDto) => a.action === 'disable')
this.store.mods.setStatus(id, status, at)
this.store.mods.setStatus(id, status, at)
const action = status === 'enabled' ? 'enable' : 'disable'
const action = status === 'enabled' ? 'enable' : 'disable'
this.writeAudit(id, action, null, actor, null)
this.writeAudit(id, action, null, actor, null)
this.emit(status === 'enabled' ? 'enabled' : 'disabled', view.mod, actor, {})
this.emit(status === 'enabled' ? 'enabled' : 'disabled', view.mod, actor, {})
+ if (status === 'disabled' || restoring) {
+ this.moderated(`mod.${status}`, view, actor, undefined, { previous: view.mod.status })
+ }
})
})
return this.refresh(id)
return this.refresh(id)
}
}
@@ -314,6 +334,30 @@ export class ModRegistry {
@@ -314,6 +334,30 @@ export class ModRegistry {
})
})
}
}
+ /** games.moderation.action, unless the publisher is acting on their own mod. */
+ private moderated(
+ action: string,
+ view: ModView,
+ actor: ModActor,
+ reason: string | undefined,
+ details: Record<string, unknown>,
+ ): void {
+ const publisher = view.manifest.publisher
+ const owner = publisher && publisher.type === 'user' ? publisher.id : null
+ if (owner && actor.subject.type === 'user' && actor.subject.id === owner) return
+ this.events.enqueue(
+ moderationEvent(
+ action,
+ { type: 'mod', id: view.mod.id, ownerSubject: owner },
+ actor.subject,
+ {
+ reason: reason ?? null,
+ details,
+ },
+ ),
+ )
+ }
+
private emit(
private emit(
kind: ModLifecycle,
kind: ModLifecycle,
mod: ModInstallDto,
mod: ModInstallDto,
@@ -8,6 +8,9 @@
@@ -8,6 +8,9 @@
* games.blueprint.unlocked a persisted blueprint recipe unlock
* games.blueprint.unlocked a persisted blueprint recipe unlock
* games.world.saved the authoritative world was written (checkpoint / shutdown)
* games.world.saved the authoritative world was written (checkpoint / shutdown)
* games.mod.* mod install, enable, disable, grant changes and revoke
* games.mod.* mod install, enable, disable, grant changes and revoke
+ * games.moderation.action staff took down or put back a mod someone else published
+ * (common.moderation-action@1, ADR-022), for Network's moderation
+ * audit log
*
*
* Scraplandia has no achievement system, so there is no games.achievement.*
* Scraplandia has no achievement system, so there is no games.achievement.*
* event: skill levels and blueprint unlocks are the real progression
* event: skill levels and blueprint unlocks are the real progression
@@ -181,6 +184,32 @@ export function modEvent(
@@ -181,6 +184,32 @@ export function modEvent(
}
}
}
}
+/**
+ * games.moderation.action: one staff action on someone else's content. `actor` is the staff
+ * member (a service when no person is known); the payload never carries the content itself.
+ */
+export function moderationEvent(
+ action: string,
+ target: { type: string; id: string; ownerSubject: string | null },
+ actor: SubjectRef,
+ opts: { reason?: string | null; details?: Record<string, unknown> } = {},
+): EventInput {
+ const t = { type: target.type, id: target.id.slice(0, 200), owner_subject: target.ownerSubject }
+ return {
+ event_type: 'games.moderation.action',
+ actor,
+ subject: { type: 'moderation_action', id: `${t.type}:${t.id}`.slice(0, 200) },
+ visibility: 'internal',
+ payload: {
+ action,
+ target: t,
+ actor_subject: actor.type === 'user' ? actor.id : null,
+ reason: opts.reason ? opts.reason.slice(0, 500) : null,
+ details: opts.details ?? {},
+ },
+ }
+}
+
/** Where events go. `enqueue` must run inside the transaction making the change. */
/** Where events go. `enqueue` must run inside the transaction making the change. */
export interface EventSink {
export interface EventSink {
readonly enabled: boolean
readonly enabled: boolean
@@ -26,7 +26,8 @@
@@ -26,7 +26,8 @@
"games.mod.enabled",
"games.mod.enabled",
"games.mod.disabled",
"games.mod.disabled",
"games.mod.grants_changed",
"games.mod.grants_changed",
- "games.mod.revoked"
+ "games.mod.revoked",
+ "games.moderation.action"
],
],
"eventsConsumed": [],
"eventsConsumed": [],
"namespacesOwned": ["games.*"],
"namespacesOwned": ["games.*"],
@@ -112,8 +112,8 @@ importers:
@@ -112,8 +112,8 @@ importers:
specifier: ^11.9.1
specifier: ^11.9.1
version: 11.10.0
version: 11.10.0
openvibe-contracts:
openvibe-contracts:
- specifier: https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.0
- version: https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.0
+ specifier: https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.53.0
+ version: https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.53.0
openvibe-sdk:
openvibe-sdk:
specifier: https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0
specifier: https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0
version: https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0
version: https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0
@@ -1326,9 +1326,9 @@ packages:
@@ -1326,9 +1326,9 @@ packages:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
- openvibe-contracts@https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.0:
- resolution: {tarball: https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.0}
- version: 0.49.0
+ openvibe-contracts@https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.53.0:
+ resolution: {tarball: https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.53.0}
+ version: 0.53.0
engines: {node: '>=20'}
engines: {node: '>=20'}
hasBin: true
hasBin: true
@@ -2742,7 +2742,7 @@ snapshots:
@@ -2742,7 +2742,7 @@ snapshots:
dependencies:
dependencies:
wrappy: 1.0.2
wrappy: 1.0.2
- openvibe-contracts@https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.0:
+ openvibe-contracts@https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.53.0:
dependencies:
dependencies:
ajv: 8.20.0
ajv: 8.20.0
ajv-formats: 3.0.1([email protected])
ajv-formats: 3.0.1([email protected])