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

OP OpenVibers committed 26 Sept 2026 · 28c621dcea · 12 files, +235 −13


Changes

ddiff --git a/apps/server/dist-types/mods/moderationEvents.test.d.ts b/apps/server/dist-types/mods/moderationEvents.test.d.ts
nnew file mode 100644
iindex 0000000..670e88a
---- /dev/null
++++ b/apps/server/dist-types/mods/moderationEvents.test.d.ts
@@@ -0,0 +1,2 @@
++export {};
++//# sourceMappingURL=moderationEvents.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/mods/moderationEvents.test.d.ts.map b/apps/server/dist-types/mods/moderationEvents.test.d.ts.map
nnew file mode 100644
iindex 0000000..e4db300
---- /dev/null
++++ b/apps/server/dist-types/mods/moderationEvents.test.d.ts.map
@@@ -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
ddiff --git a/apps/server/dist-types/mods/registry.d.ts b/apps/server/dist-types/mods/registry.d.ts
iindex 8ba4e74..5010345 100644
---- a/apps/server/dist-types/mods/registry.d.ts
++++ b/apps/server/dist-types/mods/registry.d.ts
@@@ -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;
ddiff --git a/apps/server/dist-types/mods/registry.d.ts.map b/apps/server/dist-types/mods/registry.d.ts.map
iindex 1d6be22..5b5852e 100644
---- a/apps/server/dist-types/mods/registry.d.ts.map
++++ b/apps/server/dist-types/mods/registry.d.ts.map
@@@ -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
ddiff --git a/apps/server/dist-types/platform/gameEvents.d.ts b/apps/server/dist-types/platform/gameEvents.d.ts
iindex 065a359..c6ab5ce 100644
---- a/apps/server/dist-types/platform/gameEvents.d.ts
++++ b/apps/server/dist-types/platform/gameEvents.d.ts
@@@ -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;
ddiff --git a/apps/server/dist-types/platform/gameEvents.d.ts.map b/apps/server/dist-types/platform/gameEvents.d.ts.map
iindex 05da452..4ea2fb0 100644
---- a/apps/server/dist-types/platform/gameEvents.d.ts.map
++++ b/apps/server/dist-types/platform/gameEvents.d.ts.map
@@@ -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
ddiff --git a/apps/server/package.json b/apps/server/package.json
iindex 495103e..83b23a5 100644
---- a/apps/server/package.json
++++ b/apps/server/package.json
@@@ -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"
ddiff --git a/apps/server/src/mods/moderationEvents.test.ts b/apps/server/src/mods/moderationEvents.test.ts
nnew file mode 100644
iindex 0000000..7d9aa68
---- /dev/null
++++ b/apps/server/src/mods/moderationEvents.test.ts
@@@ -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()
++ })
++})
ddiff --git a/apps/server/src/mods/registry.ts b/apps/server/src/mods/registry.ts
iindex a909ccc..5da08aa 100644
---- a/apps/server/src/mods/registry.ts
++++ b/apps/server/src/mods/registry.ts
@@@ -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 view
const 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,
ddiff --git a/apps/server/src/platform/gameEvents.ts b/apps/server/src/platform/gameEvents.ts
iindex ddc2788..b83570e 100644
---- a/apps/server/src/platform/gameEvents.ts
++++ b/apps/server/src/platform/gameEvents.ts
@@@ -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
ddiff --git a/docs/service-manifest-proposal.json b/docs/service-manifest-proposal.json
iindex 6f6a5c7..45b7a0a 100644
---- a/docs/service-manifest-proposal.json
++++ b/docs/service-manifest-proposal.json
@@@ -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.*"],
ddiff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
iindex 246b442..4b81b98 100644
---- a/pnpm-lock.yaml
++++ b/pnpm-lock.yaml
@@@ -112,8 +112,8 @@ importers:
specifier: ^11.9.1
version: 11.10.0
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:
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
@@@ -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.0
engines: {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.0
ajv-formats: 3.0.1([email protected])