openvibers/openvibe.games Commits
Sign-out everywhere reaches Games (WS-B task 4, Contracts 0.39.0, SDK 0.9.1): POST /internal/events (GAMES_EVENTS_SECRET, signature v2, loopback only) applies network.user.token_valid_after into the SDK revocation store and closes the person's sessions opened with an older sign-in (reject signed_out, 4011); guests are never touched; the subscription is created at boot when the secret is set. Sessions keep their sign-in's iat
Changes
@@@ -357,9 +357,13 @@ async function start(): Promise<void> {}} else if (msg.reason === 'guest_one_character') {hud.setStatus('guests get one character — sign in with OpenVibe for 3 slots')-- } else if (msg.reason === 'auth_failed') {++ } else if (msg.reason === 'auth_failed' || msg.reason === 'signed_out') {localStorage.removeItem('ovg_sso')-- hud.setStatus('OpenVibe sign-in expired — refresh to continue as guest or sign in again')++ hud.setStatus(++ msg.reason === 'signed_out'++ ? 'You signed out of OpenVibe everywhere — refresh to continue as guest or sign in again'++ : 'OpenVibe sign-in expired — refresh to continue as guest or sign in again',++ )} else {hud.setStatus(`rejected: ${msg.reason} — refresh the page`)}@@@ -50,6 +50,14 @@ export interface PlatformConfig {mediaNamespace: string;/** At most one `games.world.saved` checkpoint event per this many minutes. */worldSavedEventMinutes: number;++ /**++ * GAMES_EVENTS_SECRET (comma-separated for rotation, 32+ characters each): signs Games' own Events++ * subscriptions (network.user.token_valid_after → POST /internal/events). Empty = no subscription,++ * and the route answers 503.++ */++ eventsSecrets: string[];++ /** Where Events delivers (GAMES_EVENTS_ENDPOINT, else loopback on PORT). */++ eventsEndpoint: string | null;}export declare function loadPlatformConfig(env: NodeJS.ProcessEnv): PlatformConfig;export declare function loadConfig(env: NodeJS.ProcessEnv): ServerConfig;@@@ -1 +1 @@--{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,0EAA0E;IAC1E,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,+EAA+E;IAC/E,cAAc,EAAE,OAAO,CAAA;IACvB,gFAAgF;IAChF,KAAK,EAAE;QACL,QAAQ,EAAE,MAAM,CAAA;QAChB,YAAY,EAAE,MAAM,CAAA;QACpB,OAAO,EAAE,MAAM,CAAA;QACf,OAAO,EAAE,MAAM,CAAA;QACf,6DAA6D;QAC7D,OAAO,EAAE,MAAM,CAAA;KAChB,GAAG,IAAI,CAAA;IACR;;;;OAIG;IACH,QAAQ,EAAE,cAAc,CAAA;IACxB,QAAQ,EAAE,MAAM,CAAA;IAChB,qCAAqC;IACrC,aAAa,EAAE,MAAM,CAAA;IACrB,8EAA8E;IAC9E,cAAc,EAAE,MAAM,CAAA;IACtB,UAAU,EAAE,MAAM,CAAA;IAClB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,iBAAiB,EAAE,MAAM,CAAA;IACzB,4DAA4D;IAC5D,kBAAkB,EAAE,MAAM,CAAA;CAC3B;AAED,MAAM,WAAW,cAAc;IAC7B,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAA;IAChB,yDAAyD;IACzD,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,0FAA0F;IAC1F,UAAU,EAAE,MAAM,CAAA;IAClB,uFAAuF;IACvF,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,8DAA8D;IAC9D,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,iEAAiE;IACjE,cAAc,EAAE,MAAM,CAAA;IACtB,8EAA8E;IAC9E,sBAAsB,EAAE,MAAM,CAAA;CAC/B;AAMD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,cAAc,CAazE;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,YAAY,CAkC/D"}\\ No newline at end of file++{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,0EAA0E;IAC1E,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,+EAA+E;IAC/E,cAAc,EAAE,OAAO,CAAA;IACvB,gFAAgF;IAChF,KAAK,EAAE;QACL,QAAQ,EAAE,MAAM,CAAA;QAChB,YAAY,EAAE,MAAM,CAAA;QACpB,OAAO,EAAE,MAAM,CAAA;QACf,OAAO,EAAE,MAAM,CAAA;QACf,6DAA6D;QAC7D,OAAO,EAAE,MAAM,CAAA;KAChB,GAAG,IAAI,CAAA;IACR;;;;OAIG;IACH,QAAQ,EAAE,cAAc,CAAA;IACxB,QAAQ,EAAE,MAAM,CAAA;IAChB,qCAAqC;IACrC,aAAa,EAAE,MAAM,CAAA;IACrB,8EAA8E;IAC9E,cAAc,EAAE,MAAM,CAAA;IACtB,UAAU,EAAE,MAAM,CAAA;IAClB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,iBAAiB,EAAE,MAAM,CAAA;IACzB,4DAA4D;IAC5D,kBAAkB,EAAE,MAAM,CAAA;CAC3B;AAED,MAAM,WAAW,cAAc;IAC7B,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAA;IAChB,yDAAyD;IACzD,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,0FAA0F;IAC1F,UAAU,EAAE,MAAM,CAAA;IAClB,uFAAuF;IACvF,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,8DAA8D;IAC9D,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,iEAAiE;IACjE,cAAc,EAAE,MAAM,CAAA;IACtB,8EAA8E;IAC9E,sBAAsB,EAAE,MAAM,CAAA;IAC9B;;;;OAIG;IACH,aAAa,EAAE,MAAM,EAAE,CAAA;IACvB,4EAA4E;IAC5E,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AAMD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,cAAc,CAmBzE;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,YAAY,CAkC/D"}\\ No newline at end of file@@@ -178,6 +178,12 @@ export declare class GameServer {private broadcastDespawn;private broadcastToKnowing;/** Live map edit: every client refetches and rebuilds its terrain. */++ /**++ * Network moved this person's token cutoff (sign out everywhere, password changed, banned): close++ * every session they opened with an older Network sign-in (4011 signed_out). The client's reconnect++ * then fails its /api/auth/me check. Returns how many closed.++ */++ revokeSubject(subjectId: string, validAfterMs: number): number;broadcastMapReload(): void;/** Exposes crafting context for the client-facing recipe availability (welcome-time). */workstationsNear(session: PlayerSession): ReadonlySet<string>;@@@ -1 +1 @@--{"version":3,"file":"gameServer.d.ts","sourceRoot":"","sources":["../../src/game/gameServer.ts"],"names":[],"mappings":"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"}\\ No newline at end of file++{"version":3,"file":"gameServer.d.ts","sourceRoot":"","sources":["../../src/game/gameServer.ts"],"names":[],"mappings":"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"}\\ No newline at end of file@@@ -37,6 +37,11 @@ export interface PlayerSession {token: string;/** Canonical openvibe.network subject of the account; null for local guests. */subjectId: string | null;++ /**++ * When the Network sign-in behind this session was issued (JWT iat, seconds); null for guests. A++ * sign-out everywhere after it closes the session (network.user.token_valid_after).++ */++ authIat: number | null;/** Character slot under the account token (0..2). */charSlot: number;/** openvibe.network rank; gates edit mode + moderation. */@@@ -105,6 +110,7 @@ export interface SessionInit {entityId: EntityId;token: string;subjectId?: string | null;++ authIat?: number | null;charSlot?: number;rank?: 'owner' | 'admin' | 'moderator' | null;name: string;@@@ -1 +1 @@--{"version":3,"file":"playerSession.d.ts","sourceRoot":"","sources":["../../src/game/playerSession.ts"],"names":[],"mappings":"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"}\\ No newline at end of file++{"version":3,"file":"playerSession.d.ts","sourceRoot":"","sources":["../../src/game/playerSession.ts"],"names":[],"mappings":"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"}\\ No newline at end of filennew file mode 100644@@@ -0,0 +1,59 @@++/**++ * Sign-out everywhere reaches Games (roadmap WS-B task 4; Contracts 0.39.0++ * network.user.token_valid_after).++ *++ * POST /internal/events is the endpoint of Games' Events subscription to that topic, signed with++ * GAMES_EVENTS_SECRET (signature v2 only) and loopback only (a request carrying a forwarding header++ * came through nginx and is refused). A delivery moves the person's cutoff in the SDK's revocation++ * store (only ever forward; anything not from Network is ignored), then `onRevoked` closes the game++ * sessions they opened with an older sign-in. New sign-ins already fail: Games checks every token with++ * the Network's /api/auth/me, which applies the same cutoff.++ */++import type { IncomingMessage, ServerResponse } from 'node:http';++import type { Logger } from '@openvibe/shared';++interface RevocationStore {++ apply(event: unknown): string;++ isRevoked(claims: {++ iat?: number;++ subject_id?: string;++ } | null | undefined): boolean;++ cutoffFor(subject: string): number;++}++export declare const TOPIC = "network.user.token_valid_after";++export interface RevocationEvents {++ /** Answers POST /internal/events (returns true when the request was ours). */++ handle(req: IncomingMessage, res: ServerResponse): boolean;++ store: RevocationStore;++ stats: {++ received: number;++ revoked: number;++ closed: number;++ refused: number;++ ignored: number;++ };++}++export declare function createRevocationEvents(opts: {++ db: unknown;++ secrets: string[];++ onRevoked: (subjectId: string, validAfterMs: number) => number;++ log: Logger;++}): RevocationEvents;++/**++ * Create Games' subscription to TOPIC at Events if it is missing (idempotent: an existing one, even++ * disabled by an operator, is left as it is). Needs the grant games events.subscription.manage.++ */++export declare function ensureRevocationSubscription(opts: {++ eventsUrl: string;++ endpoint: string;++ secret: string;++ tokens: {++ getToken(ctx?: {++ audience?: string;++ scope?: string;++ }): Promise<string>;++ };++ fetchImpl?: typeof fetch;++ log: Logger;++}): Promise<'exists' | 'created'>;++export {};++//# sourceMappingURL=revocationEvents.d.ts.map\\ No newline at end of filennew file mode 100644@@@ -0,0 +1 @@++{"version":3,"file":"revocationEvents.d.ts","sourceRoot":"","sources":["../../src/platform/revocationEvents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAEhE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAI9C,UAAU,eAAe;IACvB,KAAK,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAA;IAC7B,SAAS,CAAC,MAAM,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAA;IACpF,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAA;CACnC;AAaD,eAAO,MAAM,KAAK,mCAAmC,CAAA;AAIrD,MAAM,WAAW,gBAAgB;IAC/B,8EAA8E;IAC9E,MAAM,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAA;IAC1D,KAAK,EAAE,eAAe,CAAA;IACtB,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;CAC/F;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE;IAC3C,EAAE,EAAE,OAAO,CAAA;IACX,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,MAAM,CAAA;IAC9D,GAAG,EAAE,MAAM,CAAA;CACZ,GAAG,gBAAgB,CA0DnB;AAED;;;GAGG;AACH,wBAAsB,4BAA4B,CAAC,IAAI,EAAE;IACvD,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE;QAAE,QAAQ,CAAC,GAAG,CAAC,EAAE;YAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;IAClF,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACxB,GAAG,EAAE,MAAM,CAAA;CACZ,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAkBhC"}\\ No newline at end of filennew file mode 100644@@@ -0,0 +1,2 @@++export {};++//# sourceMappingURL=revocationEvents.test.d.ts.map\\ No newline at end of filennew file mode 100644@@@ -0,0 +1 @@++{"version":3,"file":"revocationEvents.test.d.ts","sourceRoot":"","sources":["../../src/platform/revocationEvents.test.ts"],"names":[],"mappings":""}\\ No newline at end of file@@@ -3,6 +3,13 @@ import type { PlatformConfig } from '../config.js';export interface PlatformClient {client: OpenVibeClient;clientId: string;++ /** The `games` principal's tokens, for calls the SDK client has no method for (Events subscriptions). */++ tokens: {++ getToken(ctx?: {++ audience?: string;++ scope?: string;++ }): Promise<string>;++ };}/** Null when no client secret is configured: Games then makes no service calls. */export declare function createPlatformClient(cfg: PlatformConfig, fetchImpl?: FetchLike): PlatformClient | null;@@@ -1 +1 @@--{"version":3,"file":"serviceClient.d.ts","sourceRoot":"","sources":["../../src/platform/serviceClient.ts"],"names":[],"mappings":"AAYA,OAAO,EAAgB,KAAK,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,mBAAmB,CAAA;AACrF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAElD,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,cAAc,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,mFAAmF;AACnF,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,cAAc,EACnB,SAAS,CAAC,EAAE,SAAS,GACpB,cAAc,GAAG,IAAI,CAqBvB"}\\ No newline at end of file++{"version":3,"file":"serviceClient.d.ts","sourceRoot":"","sources":["../../src/platform/serviceClient.ts"],"names":[],"mappings":"AAYA,OAAO,EAAgB,KAAK,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,mBAAmB,CAAA;AACrF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAElD,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,cAAc,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,yGAAyG;IACzG,MAAM,EAAE;QAAE,QAAQ,CAAC,GAAG,CAAC,EAAE;YAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;CACnF;AAED,mFAAmF;AACnF,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,cAAc,EACnB,SAAS,CAAC,EAAE,SAAS,GACpB,cAAc,GAAG,IAAI,CAqBvB"}\\ No newline at end of file@@@ -19,8 +19,8 @@"@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.38.0",-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.6.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.39.0",++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.9.1","tsx": "^4.19.3","ws": "^8.18.1"},@@@ -51,6 +51,14 @@ export interface PlatformConfig {mediaNamespace: string/** At most one `games.world.saved` checkpoint event per this many minutes. */worldSavedEventMinutes: number++ /**++ * GAMES_EVENTS_SECRET (comma-separated for rotation, 32+ characters each): signs Games' own Events++ * subscriptions (network.user.token_valid_after → POST /internal/events). Empty = no subscription,++ * and the route answers 503.++ */++ eventsSecrets: string[]++ /** Where Events delivers (GAMES_EVENTS_ENDPOINT, else loopback on PORT). */++ eventsEndpoint: string | null}function trimSlash(v: string): string {@@@ -69,6 +77,12 @@ export function loadPlatformConfig(env: NodeJS.ProcessEnv): PlatformConfig {mediaUrl: env.MEDIA_URL && env.MEDIA_MIRROR !== 'off' ? trimSlash(env.MEDIA_URL) : null,mediaNamespace: env.MEDIA_NAMESPACE ?? 'games',worldSavedEventMinutes: intEnv(env, 'WORLD_SAVED_EVENT_MINUTES', 15),++ eventsSecrets: (env.GAMES_EVENTS_SECRET ?? '')++ .split(',')++ .map((s) => s.trim())++ .filter((s) => s.length >= 32),++ eventsEndpoint:++ env.GAMES_EVENTS_ENDPOINT ?? `http://127.0.0.1:${intEnv(env, 'PORT', 8000)}/internal/events`,}}@@@ -120,6 +120,18 @@ export interface GameIntegrations {mods?: ModRuntime}++/** A JWT's iat (seconds), read from a token the Network has already accepted; null otherwise. */++function tokenIat(jwt: string): number | null {++ const part = jwt.split('.')[1]++ if (!part) return null++ try {++ const claims = JSON.parse(Buffer.from(part, 'base64url').toString('utf8')) as { iat?: unknown }++ return typeof claims.iat === 'number' ? claims.iat : null++ } catch {++ return null++ }++}++export class GameServer {private readonly sessions = new Map<PlayerId, PlayerSession>()private readonly sessionsByConn = new Map<GameConnection, PlayerSession>()@@@ -1213,6 +1225,7 @@ export class GameServer {// recovery path when the token is gone.let token = msg.tokenlet subjectId: string | null = null++ let authIat: number | null = nulllet rank: 'owner' | 'admin' | 'moderator' | null = nullif (msg.auth) {const user = await resolveNetworkUser(this.config.networkAuthUrl, msg.auth)@@@ -1234,6 +1247,7 @@ export class GameServer {}token = account.keysubjectId = account.subjectId++ authIat = tokenIat(msg.auth)rank = user.rank} else {// A guest token may never look like an account key (`usr_…`,@@@ -1298,6 +1312,7 @@ export class GameServer {entityId: newEntityId(),token,subjectId,++ authIat,rank,name: msg.name,spawn,@@@ -2672,6 +2687,28 @@ export class GameServer {}/** Live map edit: every client refetches and rebuilds its terrain. */++ /**++ * Network moved this person's token cutoff (sign out everywhere, password changed, banned): close++ * every session they opened with an older Network sign-in (4011 signed_out). The client's reconnect++ * then fails its /api/auth/me check. Returns how many closed.++ */++ revokeSubject(subjectId: string, validAfterMs: number): number {++ if (!subjectId || !Number.isFinite(validAfterMs)) return 0++ let closed = 0++ for (const session of [...this.sessions.values()]) {++ if (session.subjectId !== subjectId) continue++ if (session.authIat !== null && session.authIat * 1000 >= validAfterMs) continue++ try {++ session.send(encodeServerMessage({ t: 'reject', reason: 'signed_out' }))++ } catch {++ // closing anyway++ }++ session.closeConnection(4011, 'signed_out')++ closed++++ }++ return closed++ }++broadcastMapReload(): void {this.broadcastAll({ t: 'map_reload' })this.broadcastAll({ t: 'announce', text: '🗺 The world was reshaped by the map editors…' })@@@ -300,4 +300,19 @@ describe('Games on platform identity, events and mods (real server, restart)', (await b.outbox.stop()b.dispose()}, 60_000)++++ it('a sign-out everywhere closes the signed-in session, never a guest (WS-B task 4)', async () => {++ const c = boot(platform)++ const ana = connect(c.game)++ expect((await ana.hello('abcdefgh12', 'tok-ana')).welcome).toBeDefined()++ const guest = connect(c.game)++ expect((await guest.hello('guest00042')).welcome).toBeDefined()++ expect(c.game.revokeSubject('usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2ZZ', Date.now())).toBe(0)++ expect(c.game.revokeSubject(SUBJECT, Date.now())).toBe(1)++ expect(ana.inbox.some((m) => m.t === 'reject' && m.reason === 'signed_out')).toBe(true)++ expect(guest.inbox.some((m) => m.t === 'reject')).toBe(false)++ c.game.shutdown()++ await c.outbox.stop()++ c.dispose()++ }, 60_000)})@@@ -47,6 +47,11 @@ export interface PlayerSession {token: string/** Canonical openvibe.network subject of the account; null for local guests. */subjectId: string | null++ /**++ * When the Network sign-in behind this session was issued (JWT iat, seconds); null for guests. A++ * sign-out everywhere after it closes the session (network.user.token_valid_after).++ */++ authIat: number | null/** Character slot under the account token (0..2). */charSlot: number/** openvibe.network rank; gates edit mode + moderation. */@@@ -113,6 +118,7 @@ export interface SessionInit {entityId: EntityIdtoken: stringsubjectId?: string | null++ authIat?: number | nullcharSlot?: numberrank?: 'owner' | 'admin' | 'moderator' | nullname: string@@@ -138,6 +144,7 @@ export function createSession(init: SessionInit): PlayerSession {entityId: init.entityId,token: init.token,subjectId: init.subjectId ?? null,++ authIat: init.authIat ?? null,charSlot: init.charSlot ?? 0,rank: init.rank ?? null,name: init.name,@@@ -34,6 +34,7 @@ import {} from './platform/gameEvents.js'import { MediaMirror } from './platform/mediaMirror.js'import { createPlatformClient } from './platform/serviceClient.js'++import { createRevocationEvents, ensureRevocationSubscription } from './platform/revocationEvents.js'import { createStaffAuthorizer } from './platform/staffAuth.js'/**@@@ -154,6 +155,30 @@ async function main(): Promise<void> {mods: modRuntime,})++ // Sign-out everywhere (network.user.token_valid_after): POST /internal/events closes the person's++ // older game sessions; the subscription is created at boot when a secret is set (WS-B task 4).++ const revocations = createRevocationEvents({++ db: store.db,++ secrets: config.platform.eventsSecrets,++ onRevoked: (subject, validAfterMs) => game.revokeSubject(subject, validAfterMs),++ log: platformLog,++ })++ if (platform && config.platform.eventsUrl && config.platform.eventsSecrets[0] && config.platform.eventsEndpoint) {++ const subscribe = (attempt: number): void => {++ ensureRevocationSubscription({++ eventsUrl: config.platform.eventsUrl as string,++ endpoint: config.platform.eventsEndpoint as string,++ secret: config.platform.eventsSecrets[0] as string,++ tokens: platform.tokens,++ log: platformLog,++ }).catch((err: unknown) => {++ platformLog.warn('events subscription not ready (will retry)', { error: String((err as Error)?.message ?? err), attempt })++ if (attempt < 5) setTimeout(() => subscribe(attempt + 1), 60_000 * attempt).unref()++ })++ }++ subscribe(1)++ }++// GET /release.json (D43): the deployed commit and package versions, as every OpenVibe service serves it.const serveRelease = releaseHandler(buildRelease(process.cwd()))@@@ -211,6 +236,7 @@ async function main(): Promise<void> {config.oauth,{handle: (req, res) => {++ if (revocations.handle(req, res)) return trueif (readiness.handle(req, res)) return trueif (serveRelease(req, res)) return trueif ((req.url ?? '').split('?')[0] === '/api/v1/platform' && req.method === 'GET') {nnew file mode 100644@@@ -0,0 +1,91 @@++import { createServer, type Server } from 'node:http'++import type { AddressInfo } from 'node:net'++import { createRequire } from 'node:module'++import { createConsoleLogger } from '@openvibe/shared'++import { afterAll, beforeAll, describe, expect, it } from 'vitest'++import { createRevocationEvents, ensureRevocationSubscription, TOPIC } from './revocationEvents.js'++++/**++ * POST /internal/events for network.user.token_valid_after: signature v2 only, loopback only, the++ * cutoff moves once (a redelivery changes nothing, anything not from Network is ignored) and the++ * person's sessions are closed through onRevoked; the boot subscription is created once.++ */++const req = createRequire(import.meta.url)++const Database = req('better-sqlite3') as new (path: string) => unknown++const { signDeliveryHeaders } = req('openvibe-sdk/events') as {++ signDeliveryHeaders(body: string, secret: string): Record<string, string>++}++const SECRET = 'x'.repeat(40)++const SUBJECT = 'usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3'++const log = createConsoleLogger({ app: 'test' }, 'error')++const closedFor: [string, number][] = []++const events = createRevocationEvents({++ db: new Database(':memory:'),++ secrets: [SECRET],++ onRevoked: (s, ms) => {++ closedFor.push([s, ms])++ return 2++ },++ log,++})++let server: Server++let url: string++++beforeAll(async () => {++ server = createServer((req, res) => {++ if (!events.handle(req, res)) {++ res.writeHead(404)++ res.end()++ }++ })++ await new Promise<void>((r) => server.listen(0, '127.0.0.1', r))++ url = `http://127.0.0.1:${(server.address() as AddressInfo).port}/internal/events`++})++afterAll(() => server.close())++++const at = Date.parse('2026-09-24T20:00:00Z')++const envelope = (over: Record<string, unknown> = {}) => ({++ event_id: 'evt_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3',++ event_type: TOPIC,++ version: 1,++ source: 'network',++ payload: { subject: { type: 'user', id: SUBJECT }, valid_after: new Date(at).toISOString(), reason: 'signed_out_everywhere' },++ ...over,++})++const post = async (event: unknown, headers: Record<string, string> = {}, secret = SECRET) => {++ const body = JSON.stringify({ event, seq: 1 })++ const r = await fetch(url, { method: 'POST', body, headers: { 'content-type': 'application/json', ...signDeliveryHeaders(body, secret), ...headers } })++ return { status: r.status, body: (await r.json()) as { outcome?: string; closed?: number } }++}++++describe('revocation events', () => {++ it('applies a signed delivery once and closes the sessions', async () => {++ expect(await post(envelope())).toMatchObject({ status: 200, body: { outcome: 'revoked', closed: 2 } })++ expect(closedFor).toEqual([[SUBJECT, at]])++ expect(events.store.isRevoked({ subject_id: SUBJECT, iat: at / 1000 - 1 })).toBe(true)++ expect((await post(envelope())).body.outcome).toBe('unchanged')++ expect((await post(envelope({ source: 'live' }))).body.outcome).toBe('ignored:source')++ expect(closedFor.length).toBe(1)++ })++++ it('refuses forged, proxied and non-POST requests', async () => {++ expect((await post(envelope(), {}, 'y'.repeat(40))).status).toBe(401)++ expect((await post(envelope(), { 'x-forwarded-for': '1.2.3.4' })).status).toBe(403)++ expect((await fetch(url)).status).toBe(405)++ })++++ it('creates the subscription once', async () => {++ const subs: { topic_pattern: string; endpoint: string }[] = []++ const fake = (async (u: string, init?: RequestInit) => {++ if (init?.method === 'POST') {++ subs.push(JSON.parse(String(init.body)) as { topic_pattern: string; endpoint: string })++ return Response.json({ id: 'sub_1' }, { status: 201 })++ }++ return Response.json({ subscriptions: subs.map((s, i) => ({ id: `sub_${i + 1}`, ...s })) })++ }) as unknown as typeof fetch++ const opts = { eventsUrl: 'http://events.test', endpoint: 'http://127.0.0.1:8000/internal/events', secret: SECRET, tokens: { getToken: async () => 'tok' }, fetchImpl: fake, log }++ expect(await ensureRevocationSubscription(opts)).toBe('created')++ expect(await ensureRevocationSubscription(opts)).toBe('exists')++ expect(subs).toEqual([{ topic_pattern: TOPIC, endpoint: opts.endpoint, secret: SECRET }])++ })++})nnew file mode 100644@@@ -0,0 +1,140 @@++/**++ * Sign-out everywhere reaches Games (roadmap WS-B task 4; Contracts 0.39.0++ * network.user.token_valid_after).++ *++ * POST /internal/events is the endpoint of Games' Events subscription to that topic, signed with++ * GAMES_EVENTS_SECRET (signature v2 only) and loopback only (a request carrying a forwarding header++ * came through nginx and is refused). A delivery moves the person's cutoff in the SDK's revocation++ * store (only ever forward; anything not from Network is ignored), then `onRevoked` closes the game++ * sessions they opened with an older sign-in. New sign-ins already fail: Games checks every token with++ * the Network's /api/auth/me, which applies the same cutoff.++ */++import type { IncomingMessage, ServerResponse } from 'node:http'++import { createRequire } from 'node:module'++import type { Logger } from '@openvibe/shared'++++const req = createRequire(import.meta.url)++++interface RevocationStore {++ apply(event: unknown): string++ isRevoked(claims: { iat?: number; subject_id?: string } | null | undefined): boolean++ cutoffFor(subject: string): number++}++type ParseDelivery = (++ raw: Buffer,++ headers: IncomingMessage['headers'],++ secret: string,++ opts: { requireV2: boolean },++) => { event?: { event_id?: string; event_type?: string; payload?: { subject?: { id?: string }; valid_after?: string } } } | null++++const { createRevocationStore } = req('openvibe-sdk/auth') as {++ createRevocationStore(db: unknown, opts?: { table?: string }): RevocationStore++}++const { parseDelivery } = req('openvibe-sdk/events') as { parseDelivery: ParseDelivery }++++export const TOPIC = 'network.user.token_valid_after'++const PATH = '/internal/events'++const MAX_BODY = 256 * 1024++++export interface RevocationEvents {++ /** Answers POST /internal/events (returns true when the request was ours). */++ handle(req: IncomingMessage, res: ServerResponse): boolean++ store: RevocationStore++ stats: { received: number; revoked: number; closed: number; refused: number; ignored: number }++}++++export function createRevocationEvents(opts: {++ db: unknown++ secrets: string[]++ onRevoked: (subjectId: string, validAfterMs: number) => number++ log: Logger++}): RevocationEvents {++ const store = createRevocationStore(opts.db, { table: 'token_revocations' })++ const stats = { received: 0, revoked: 0, closed: 0, refused: 0, ignored: 0 }++ const send = (res: ServerResponse, status: number, body: unknown): void => {++ res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' })++ res.end(JSON.stringify(body))++ }++++ function handle(req: IncomingMessage, res: ServerResponse): boolean {++ if ((req.url ?? '').split('?')[0] !== PATH) return false++ if (req.method !== 'POST') {++ send(res, 405, { error: 'POST only' })++ return true++ }++ if (req.headers['x-forwarded-for'] || req.headers['x-real-ip'] || req.headers['cf-connecting-ip']) {++ send(res, 403, { error: 'internal route' })++ return true++ }++ if (opts.secrets.length === 0) {++ send(res, 503, { error: 'GAMES_EVENTS_SECRET is not set' })++ return true++ }++ const chunks: Buffer[] = []++ let size = 0++ req.on('data', (c: Buffer) => {++ size += c.length++ if (size > MAX_BODY) req.destroy()++ else chunks.push(c)++ })++ req.on('end', () => {++ const raw = Buffer.concat(chunks)++ let delivery: ReturnType<ParseDelivery> = null++ for (const s of opts.secrets) {++ delivery = parseDelivery(raw, req.headers, s, { requireV2: true })++ if (delivery) break++ }++ if (!delivery || !delivery.event) {++ stats.refused++++ send(res, 401, { error: 'bad signature' })++ return++ }++ stats.received++++ const event = delivery.event++ const outcome = store.apply(event)++ let closed = 0++ if (outcome === 'revoked') {++ stats.revoked++++ const subject = event.payload?.subject?.id ?? ''++ closed = opts.onRevoked(subject, Date.parse(event.payload?.valid_after ?? ''))++ stats.closed += closed++ if (closed) opts.log.info('signed out everywhere: sessions closed', { closed })++ } else if (outcome.startsWith('ignored')) stats.ignored++++ send(res, 200, { event_id: event.event_id ?? null, outcome, closed })++ })++ return true++ }++++ return { handle, store, stats }++}++++/**++ * Create Games' subscription to TOPIC at Events if it is missing (idempotent: an existing one, even++ * disabled by an operator, is left as it is). Needs the grant games events.subscription.manage.++ */++export async function ensureRevocationSubscription(opts: {++ eventsUrl: string++ endpoint: string++ secret: string++ tokens: { getToken(ctx?: { audience?: string; scope?: string }): Promise<string> }++ fetchImpl?: typeof fetch++ log: Logger++}): Promise<'exists' | 'created'> {++ const f = opts.fetchImpl ?? fetch++ const token = await opts.tokens.getToken({ audience: 'openvibe.events', scope: 'events.subscription.manage' })++ const headers = { authorization: `Bearer ${token}`, accept: 'application/json' }++ const list = await f(`${opts.eventsUrl}/api/v1/subscriptions`, { headers })++ if (!list.ok) throw new Error(`Events answered ${list.status} listing subscriptions`)++ const body = (await list.json()) as { subscriptions?: { id: string; topic_pattern: string; endpoint: string }[] }++ const found = (body.subscriptions ?? []).find((s) => s.topic_pattern === TOPIC && s.endpoint === opts.endpoint)++ if (found) return 'exists'++ const r = await f(`${opts.eventsUrl}/api/v1/subscriptions`, {++ method: 'POST',++ headers: { ...headers, 'content-type': 'application/json' },++ body: JSON.stringify({ topic_pattern: TOPIC, endpoint: opts.endpoint, secret: opts.secret }),++ })++ if (r.status === 409) return 'exists'++ if (!r.ok) throw new Error(`Events answered ${r.status} creating the ${TOPIC} subscription`)++ opts.log.info('events subscription created', { topic: TOPIC, endpoint: opts.endpoint })++ return 'created'++}@@@ -16,6 +16,8 @@ import type { PlatformConfig } from '../config.js'export interface PlatformClient {client: OpenVibeClientclientId: string++ /** The `games` principal's tokens, for calls the SDK client has no method for (Events subscriptions). */++ tokens: { getToken(ctx?: { audience?: string; scope?: string }): Promise<string> }}/** Null when no client secret is configured: Games then makes no service calls. */@@@ -42,5 +44,5 @@ export function createPlatformClient(retries: 0,...(fetchImpl ? { fetch: fetchImpl } : {}),})-- return { client, clientId: cfg.clientId }++ return { client, clientId: cfg.clientId, tokens }}@@@ -29,7 +29,13 @@ export interface ServerWelcome {export interface ServerReject {t: 'reject'reason:-- 'protocol_mismatch' | 'server_full' | 'invalid_hello' | 'guest_one_character' | 'auth_failed'++ | 'protocol_mismatch'++ | 'server_full'++ | 'invalid_hello'++ | 'guest_one_character'++ | 'auth_failed'++ /** The person signed out everywhere (or changed their password, or was banned) on the Network. */++ | 'signed_out'}/** Entities that became relevant to this client (full state). */@@@ -112,11 +112,11 @@ importers:specifier: ^11.9.1version: 11.10.0openvibe-contracts:-- specifier: https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.38.0-- version: https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.38.0++ specifier: https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.39.0++ version: https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.39.0openvibe-sdk:-- specifier: https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.6.0-- version: https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.6.0++ specifier: https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.9.1++ version: https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.9.1tsx:specifier: ^4.19.3version: 4.23.11@@@ -1326,15 +1326,15 @@ packages:resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}-- openvibe-contracts@https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.38.0:-- resolution: {tarball: https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.38.0}-- version: 0.38.0++ openvibe-contracts@https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.39.0:++ resolution: {tarball: https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.39.0}++ version: 0.39.0engines: {node: '>=20'}hasBin: true-- openvibe-sdk@https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.6.0:-- resolution: {tarball: https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.6.0}-- version: 0.6.0++ openvibe-sdk@https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.9.1:++ resolution: {tarball: https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.9.1}++ version: 0.9.1engines: {node: '>=20'}@@@ -2742,12 +2742,12 @@ snapshots:dependencies:wrappy: 1.0.2-- openvibe-contracts@https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.38.0:++ openvibe-contracts@https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.39.0:dependencies:ajv: 8.20.0ajv-formats: 3.0.1([email protected])-- openvibe-sdk@https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.6.0: {}++ openvibe-sdk@https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.9.1: {}dependencies:
@@ -357,9 +357,13 @@ async function start(): Promise<void> {
@@ -357,9 +357,13 @@ async function start(): Promise<void> {
}
}
} else if (msg.reason === 'guest_one_character') {
} else if (msg.reason === 'guest_one_character') {
hud.setStatus('guests get one character — sign in with OpenVibe for 3 slots')
hud.setStatus('guests get one character — sign in with OpenVibe for 3 slots')
- } else if (msg.reason === 'auth_failed') {
+ } else if (msg.reason === 'auth_failed' || msg.reason === 'signed_out') {
localStorage.removeItem('ovg_sso')
localStorage.removeItem('ovg_sso')
- hud.setStatus('OpenVibe sign-in expired — refresh to continue as guest or sign in again')
+ hud.setStatus(
+ msg.reason === 'signed_out'
+ ? 'You signed out of OpenVibe everywhere — refresh to continue as guest or sign in again'
+ : 'OpenVibe sign-in expired — refresh to continue as guest or sign in again',
+ )
} else {
} else {
hud.setStatus(`rejected: ${msg.reason} — refresh the page`)
hud.setStatus(`rejected: ${msg.reason} — refresh the page`)
}
}
@@ -50,6 +50,14 @@ export interface PlatformConfig {
@@ -50,6 +50,14 @@ export interface PlatformConfig {
mediaNamespace: string;
mediaNamespace: string;
/** At most one `games.world.saved` checkpoint event per this many minutes. */
/** At most one `games.world.saved` checkpoint event per this many minutes. */
worldSavedEventMinutes: number;
worldSavedEventMinutes: number;
+ /**
+ * GAMES_EVENTS_SECRET (comma-separated for rotation, 32+ characters each): signs Games' own Events
+ * subscriptions (network.user.token_valid_after → POST /internal/events). Empty = no subscription,
+ * and the route answers 503.
+ */
+ eventsSecrets: string[];
+ /** Where Events delivers (GAMES_EVENTS_ENDPOINT, else loopback on PORT). */
+ eventsEndpoint: string | null;
}
}
export declare function loadPlatformConfig(env: NodeJS.ProcessEnv): PlatformConfig;
export declare function loadPlatformConfig(env: NodeJS.ProcessEnv): PlatformConfig;
export declare function loadConfig(env: NodeJS.ProcessEnv): ServerConfig;
export declare function loadConfig(env: NodeJS.ProcessEnv): ServerConfig;
@@ -1 +1 @@
@@ -1 +1 @@
-{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,0EAA0E;IAC1E,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,+EAA+E;IAC/E,cAAc,EAAE,OAAO,CAAA;IACvB,gFAAgF;IAChF,KAAK,EAAE;QACL,QAAQ,EAAE,MAAM,CAAA;QAChB,YAAY,EAAE,MAAM,CAAA;QACpB,OAAO,EAAE,MAAM,CAAA;QACf,OAAO,EAAE,MAAM,CAAA;QACf,6DAA6D;QAC7D,OAAO,EAAE,MAAM,CAAA;KAChB,GAAG,IAAI,CAAA;IACR;;;;OAIG;IACH,QAAQ,EAAE,cAAc,CAAA;IACxB,QAAQ,EAAE,MAAM,CAAA;IAChB,qCAAqC;IACrC,aAAa,EAAE,MAAM,CAAA;IACrB,8EAA8E;IAC9E,cAAc,EAAE,MAAM,CAAA;IACtB,UAAU,EAAE,MAAM,CAAA;IAClB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,iBAAiB,EAAE,MAAM,CAAA;IACzB,4DAA4D;IAC5D,kBAAkB,EAAE,MAAM,CAAA;CAC3B;AAED,MAAM,WAAW,cAAc;IAC7B,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAA;IAChB,yDAAyD;IACzD,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,0FAA0F;IAC1F,UAAU,EAAE,MAAM,CAAA;IAClB,uFAAuF;IACvF,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,8DAA8D;IAC9D,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,iEAAiE;IACjE,cAAc,EAAE,MAAM,CAAA;IACtB,8EAA8E;IAC9E,sBAAsB,EAAE,MAAM,CAAA;CAC/B;AAMD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,cAAc,CAazE;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,YAAY,CAkC/D"}
\ No newline at end of file
\ No newline at end of file
+{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,0EAA0E;IAC1E,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,+EAA+E;IAC/E,cAAc,EAAE,OAAO,CAAA;IACvB,gFAAgF;IAChF,KAAK,EAAE;QACL,QAAQ,EAAE,MAAM,CAAA;QAChB,YAAY,EAAE,MAAM,CAAA;QACpB,OAAO,EAAE,MAAM,CAAA;QACf,OAAO,EAAE,MAAM,CAAA;QACf,6DAA6D;QAC7D,OAAO,EAAE,MAAM,CAAA;KAChB,GAAG,IAAI,CAAA;IACR;;;;OAIG;IACH,QAAQ,EAAE,cAAc,CAAA;IACxB,QAAQ,EAAE,MAAM,CAAA;IAChB,qCAAqC;IACrC,aAAa,EAAE,MAAM,CAAA;IACrB,8EAA8E;IAC9E,cAAc,EAAE,MAAM,CAAA;IACtB,UAAU,EAAE,MAAM,CAAA;IAClB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,iBAAiB,EAAE,MAAM,CAAA;IACzB,4DAA4D;IAC5D,kBAAkB,EAAE,MAAM,CAAA;CAC3B;AAED,MAAM,WAAW,cAAc;IAC7B,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAA;IAChB,yDAAyD;IACzD,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,0FAA0F;IAC1F,UAAU,EAAE,MAAM,CAAA;IAClB,uFAAuF;IACvF,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,8DAA8D;IAC9D,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,iEAAiE;IACjE,cAAc,EAAE,MAAM,CAAA;IACtB,8EAA8E;IAC9E,sBAAsB,EAAE,MAAM,CAAA;IAC9B;;;;OAIG;IACH,aAAa,EAAE,MAAM,EAAE,CAAA;IACvB,4EAA4E;IAC5E,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AAMD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,cAAc,CAmBzE;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,YAAY,CAkC/D"}
\ No newline at end of file
\ No newline at end of file
@@ -178,6 +178,12 @@ export declare class GameServer {
@@ -178,6 +178,12 @@ export declare class GameServer {
private broadcastDespawn;
private broadcastDespawn;
private broadcastToKnowing;
private broadcastToKnowing;
/** Live map edit: every client refetches and rebuilds its terrain. */
/** Live map edit: every client refetches and rebuilds its terrain. */
+ /**
+ * Network moved this person's token cutoff (sign out everywhere, password changed, banned): close
+ * every session they opened with an older Network sign-in (4011 signed_out). The client's reconnect
+ * then fails its /api/auth/me check. Returns how many closed.
+ */
+ revokeSubject(subjectId: string, validAfterMs: number): number;
broadcastMapReload(): void;
broadcastMapReload(): void;
/** Exposes crafting context for the client-facing recipe availability (welcome-time). */
/** Exposes crafting context for the client-facing recipe availability (welcome-time). */
workstationsNear(session: PlayerSession): ReadonlySet<string>;
workstationsNear(session: PlayerSession): ReadonlySet<string>;
@@ -1 +1 @@
@@ -1 +1 @@
-{"version":3,"file":"gameServer.d.ts","sourceRoot":"","sources":["../../src/game/gameServer.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
\ No newline at end of file
+{"version":3,"file":"gameServer.d.ts","sourceRoot":"","sources":["../../src/game/gameServer.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
\ No newline at end of file
@@ -37,6 +37,11 @@ export interface PlayerSession {
@@ -37,6 +37,11 @@ export interface PlayerSession {
token: string;
token: string;
/** Canonical openvibe.network subject of the account; null for local guests. */
/** Canonical openvibe.network subject of the account; null for local guests. */
subjectId: string | null;
subjectId: string | null;
+ /**
+ * When the Network sign-in behind this session was issued (JWT iat, seconds); null for guests. A
+ * sign-out everywhere after it closes the session (network.user.token_valid_after).
+ */
+ authIat: number | null;
/** Character slot under the account token (0..2). */
/** Character slot under the account token (0..2). */
charSlot: number;
charSlot: number;
/** openvibe.network rank; gates edit mode + moderation. */
/** openvibe.network rank; gates edit mode + moderation. */
@@ -105,6 +110,7 @@ export interface SessionInit {
@@ -105,6 +110,7 @@ export interface SessionInit {
entityId: EntityId;
entityId: EntityId;
token: string;
token: string;
subjectId?: string | null;
subjectId?: string | null;
+ authIat?: number | null;
charSlot?: number;
charSlot?: number;
rank?: 'owner' | 'admin' | 'moderator' | null;
rank?: 'owner' | 'admin' | 'moderator' | null;
name: string;
name: string;
@@ -1 +1 @@
@@ -1 +1 @@
-{"version":3,"file":"playerSession.d.ts","sourceRoot":"","sources":["../../src/game/playerSession.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACjE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACxD,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAC7D,OAAO,EACL,UAAU,EAIV,KAAK,eAAe,EACpB,KAAK,aAAa,EACnB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClE,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AAEtE,eAAO,MAAM,cAAc,KAAK,CAAA;AAChC,eAAO,MAAM,WAAW,IAAI,CAAA;AAE5B,sCAAsC;AACtC,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,QAAQ,CAAA;IAClB,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAA;IACZ;+DAC2D;IAC3D,WAAW,EAAE,IAAI,CAAA;IACjB;;kEAE8D;IAC9D,IAAI,EAAE,OAAO,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB;kFAC8E;IAC9E,OAAO,EAAE,IAAI,CAAA;IACb,uDAAuD;IACvD,IAAI,EAAE,OAAO,CAAA;IACb,0BAA0B;IAC1B,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,QAAQ,CAAA;IAClB,QAAQ,EAAE,QAAQ,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,gFAAgF;IAChF,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,qDAAqD;IACrD,QAAQ,EAAE,MAAM,CAAA;IAChB,2DAA2D;IAC3D,IAAI,EAAE,OAAO,GAAG,OAAO,GAAG,WAAW,GAAG,IAAI,CAAA;IAC5C,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,eAAe,CAAA;IACrB,oEAAoE;IACpE,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,SAAS,CAAA;IACpB,MAAM,EAAE,QAAQ,CAAA;IAChB,wEAAwE;IACxE,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACpB,UAAU,EAAE,UAAU,CAAA;IACtB,UAAU,EAAE,UAAU,CAAA;IACtB,YAAY,EAAE,MAAM,CAAA;IACpB,+DAA+D;IAC/D,SAAS,EAAE,OAAO,CAAA;IAClB,IAAI,EAAE,QAAQ,GAAG,IAAI,CAAA;IACrB;8EAC0E;IAC1E,QAAQ,EAAE,OAAO,CAAA;IACjB,yEAAyE;IACzE,KAAK,EAAE,SAAS,GAAG,IAAI,CAAA;IACvB,8CAA8C;IAC9C,UAAU,EAAE,aAAa,CAAA;IACzB,kDAAkD;IAClD,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACpB,+CAA+C;IAC/C,SAAS,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IACnD,yDAAyD;IACzD,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAA;IACxB,4EAA4E;IAC5E,KAAK,EAAE,aAAa,CAAA;IACpB,+CAA+C;IAC/C,UAAU,EAAE,OAAO,CAAA;IACnB,wEAAwE;IACxE,aAAa,EAAE,QAAQ,GAAG,IAAI,CAAA;IAC9B,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAA;IACd,wEAAwE;IACxE,WAAW,EAAE,MAAM,CAAA;IACnB,4DAA4D;IAC5D,UAAU,EAAE,MAAM,CAAA;IAClB,8EAA8E;IAC9E,OAAO,EAAE,eAAe,CAAA;IACxB,4DAA4D;IAC5D,UAAU,EAAE,WAAW,EAAE,CAAA;IACzB,SAAS,EAAE,WAAW,GAAG,IAAI,CAAA;IAC7B,2EAA2E;IAC3E,YAAY,EAAE,MAAM,CAAA;IACpB,gBAAgB,EAAE,MAAM,CAAA;IACxB,0EAA0E;IAC1E,KAAK,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAA;IACpB,yDAAyD;IACzD,KAAK,EAAE,OAAO,CAAA;IACd,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CACpD;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,QAAQ,CAAA;IAClB,QAAQ,EAAE,QAAQ,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,GAAG,WAAW,GAAG,IAAI,CAAA;IAC7C,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,IAAI,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,SAAS,CAAA;IACpB,MAAM,EAAE,QAAQ,CAAA;IAChB,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACpB,UAAU,EAAE,UAAU,CAAA;IACtB,KAAK,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,SAAS,CAAA;IAC1C,KAAK,CAAC,EAAE,SAAS,GAAG,IAAI,CAAA;IACxB,UAAU,CAAC,EAAE,aAAa,CAAA;IAC1B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,SAAS,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IACpD,OAAO,EAAE,eAAe,CAAA;IACxB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CACpD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,aAAa,CA2C9D;AAED,4EAA4E;AAC5E,wBAAgB,WAAW,CAAC,OAAO,EAAE,aAAa,EAAE,GAAG,EAAE,IAAI,GAAG,IAAI,CAKnE;AAED,mDAAmD;AACnD,wBAAgB,aAAa,CAAC,OAAO,EAAE,aAAa,EAAE,GAAG,EAAE,IAAI,GAAG,IAAI,CAMrE"}
\ No newline at end of file
\ No newline at end of file
+{"version":3,"file":"playerSession.d.ts","sourceRoot":"","sources":["../../src/game/playerSession.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
\ No newline at end of file
new file mode 100644
new file mode 100644
@@ -0,0 +1,59 @@
@@ -0,0 +1,59 @@
+/**
+ * Sign-out everywhere reaches Games (roadmap WS-B task 4; Contracts 0.39.0
+ * network.user.token_valid_after).
+ *
+ * POST /internal/events is the endpoint of Games' Events subscription to that topic, signed with
+ * GAMES_EVENTS_SECRET (signature v2 only) and loopback only (a request carrying a forwarding header
+ * came through nginx and is refused). A delivery moves the person's cutoff in the SDK's revocation
+ * store (only ever forward; anything not from Network is ignored), then `onRevoked` closes the game
+ * sessions they opened with an older sign-in. New sign-ins already fail: Games checks every token with
+ * the Network's /api/auth/me, which applies the same cutoff.
+ */
+import type { IncomingMessage, ServerResponse } from 'node:http';
+import type { Logger } from '@openvibe/shared';
+interface RevocationStore {
+ apply(event: unknown): string;
+ isRevoked(claims: {
+ iat?: number;
+ subject_id?: string;
+ } | null | undefined): boolean;
+ cutoffFor(subject: string): number;
+}
+export declare const TOPIC = "network.user.token_valid_after";
+export interface RevocationEvents {
+ /** Answers POST /internal/events (returns true when the request was ours). */
+ handle(req: IncomingMessage, res: ServerResponse): boolean;
+ store: RevocationStore;
+ stats: {
+ received: number;
+ revoked: number;
+ closed: number;
+ refused: number;
+ ignored: number;
+ };
+}
+export declare function createRevocationEvents(opts: {
+ db: unknown;
+ secrets: string[];
+ onRevoked: (subjectId: string, validAfterMs: number) => number;
+ log: Logger;
+}): RevocationEvents;
+/**
+ * Create Games' subscription to TOPIC at Events if it is missing (idempotent: an existing one, even
+ * disabled by an operator, is left as it is). Needs the grant games events.subscription.manage.
+ */
+export declare function ensureRevocationSubscription(opts: {
+ eventsUrl: string;
+ endpoint: string;
+ secret: string;
+ tokens: {
+ getToken(ctx?: {
+ audience?: string;
+ scope?: string;
+ }): Promise<string>;
+ };
+ fetchImpl?: typeof fetch;
+ log: Logger;
+}): Promise<'exists' | 'created'>;
+export {};
+//# sourceMappingURL=revocationEvents.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":"revocationEvents.d.ts","sourceRoot":"","sources":["../../src/platform/revocationEvents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAEhE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAI9C,UAAU,eAAe;IACvB,KAAK,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAA;IAC7B,SAAS,CAAC,MAAM,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAA;IACpF,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAA;CACnC;AAaD,eAAO,MAAM,KAAK,mCAAmC,CAAA;AAIrD,MAAM,WAAW,gBAAgB;IAC/B,8EAA8E;IAC9E,MAAM,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAA;IAC1D,KAAK,EAAE,eAAe,CAAA;IACtB,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;CAC/F;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE;IAC3C,EAAE,EAAE,OAAO,CAAA;IACX,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,MAAM,CAAA;IAC9D,GAAG,EAAE,MAAM,CAAA;CACZ,GAAG,gBAAgB,CA0DnB;AAED;;;GAGG;AACH,wBAAsB,4BAA4B,CAAC,IAAI,EAAE;IACvD,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE;QAAE,QAAQ,CAAC,GAAG,CAAC,EAAE;YAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;IAClF,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACxB,GAAG,EAAE,MAAM,CAAA;CACZ,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAkBhC"}
\ No newline at end of file
\ No newline at end of file
new file mode 100644
new file mode 100644
@@ -0,0 +1,2 @@
@@ -0,0 +1,2 @@
+export {};
+//# sourceMappingURL=revocationEvents.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":"revocationEvents.test.d.ts","sourceRoot":"","sources":["../../src/platform/revocationEvents.test.ts"],"names":[],"mappings":""}
\ No newline at end of file
\ No newline at end of file
@@ -3,6 +3,13 @@ import type { PlatformConfig } from '../config.js';
@@ -3,6 +3,13 @@ import type { PlatformConfig } from '../config.js';
export interface PlatformClient {
export interface PlatformClient {
client: OpenVibeClient;
client: OpenVibeClient;
clientId: string;
clientId: string;
+ /** The `games` principal's tokens, for calls the SDK client has no method for (Events subscriptions). */
+ tokens: {
+ getToken(ctx?: {
+ audience?: string;
+ scope?: string;
+ }): Promise<string>;
+ };
}
}
/** Null when no client secret is configured: Games then makes no service calls. */
/** Null when no client secret is configured: Games then makes no service calls. */
export declare function createPlatformClient(cfg: PlatformConfig, fetchImpl?: FetchLike): PlatformClient | null;
export declare function createPlatformClient(cfg: PlatformConfig, fetchImpl?: FetchLike): PlatformClient | null;
@@ -1 +1 @@
@@ -1 +1 @@
-{"version":3,"file":"serviceClient.d.ts","sourceRoot":"","sources":["../../src/platform/serviceClient.ts"],"names":[],"mappings":"AAYA,OAAO,EAAgB,KAAK,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,mBAAmB,CAAA;AACrF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAElD,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,cAAc,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,mFAAmF;AACnF,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,cAAc,EACnB,SAAS,CAAC,EAAE,SAAS,GACpB,cAAc,GAAG,IAAI,CAqBvB"}
\ No newline at end of file
\ No newline at end of file
+{"version":3,"file":"serviceClient.d.ts","sourceRoot":"","sources":["../../src/platform/serviceClient.ts"],"names":[],"mappings":"AAYA,OAAO,EAAgB,KAAK,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,mBAAmB,CAAA;AACrF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAElD,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,cAAc,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,yGAAyG;IACzG,MAAM,EAAE;QAAE,QAAQ,CAAC,GAAG,CAAC,EAAE;YAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;CACnF;AAED,mFAAmF;AACnF,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,cAAc,EACnB,SAAS,CAAC,EAAE,SAAS,GACpB,cAAc,GAAG,IAAI,CAqBvB"}
\ No newline at end of file
\ No newline at end of file
@@ -19,8 +19,8 @@
@@ -19,8 +19,8 @@
"@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.38.0",
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.6.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.39.0",
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.9.1",
"tsx": "^4.19.3",
"tsx": "^4.19.3",
"ws": "^8.18.1"
"ws": "^8.18.1"
},
},
@@ -51,6 +51,14 @@ export interface PlatformConfig {
@@ -51,6 +51,14 @@ export interface PlatformConfig {
mediaNamespace: string
mediaNamespace: string
/** At most one `games.world.saved` checkpoint event per this many minutes. */
/** At most one `games.world.saved` checkpoint event per this many minutes. */
worldSavedEventMinutes: number
worldSavedEventMinutes: number
+ /**
+ * GAMES_EVENTS_SECRET (comma-separated for rotation, 32+ characters each): signs Games' own Events
+ * subscriptions (network.user.token_valid_after → POST /internal/events). Empty = no subscription,
+ * and the route answers 503.
+ */
+ eventsSecrets: string[]
+ /** Where Events delivers (GAMES_EVENTS_ENDPOINT, else loopback on PORT). */
+ eventsEndpoint: string | null
}
}
function trimSlash(v: string): string {
function trimSlash(v: string): string {
@@ -69,6 +77,12 @@ export function loadPlatformConfig(env: NodeJS.ProcessEnv): PlatformConfig {
@@ -69,6 +77,12 @@ export function loadPlatformConfig(env: NodeJS.ProcessEnv): PlatformConfig {
mediaUrl: env.MEDIA_URL && env.MEDIA_MIRROR !== 'off' ? trimSlash(env.MEDIA_URL) : null,
mediaUrl: env.MEDIA_URL && env.MEDIA_MIRROR !== 'off' ? trimSlash(env.MEDIA_URL) : null,
mediaNamespace: env.MEDIA_NAMESPACE ?? 'games',
mediaNamespace: env.MEDIA_NAMESPACE ?? 'games',
worldSavedEventMinutes: intEnv(env, 'WORLD_SAVED_EVENT_MINUTES', 15),
worldSavedEventMinutes: intEnv(env, 'WORLD_SAVED_EVENT_MINUTES', 15),
+ eventsSecrets: (env.GAMES_EVENTS_SECRET ?? '')
+ .split(',')
+ .map((s) => s.trim())
+ .filter((s) => s.length >= 32),
+ eventsEndpoint:
+ env.GAMES_EVENTS_ENDPOINT ?? `http://127.0.0.1:${intEnv(env, 'PORT', 8000)}/internal/events`,
}
}
}
}
@@ -120,6 +120,18 @@ export interface GameIntegrations {
@@ -120,6 +120,18 @@ export interface GameIntegrations {
mods?: ModRuntime
mods?: ModRuntime
}
}
+/** A JWT's iat (seconds), read from a token the Network has already accepted; null otherwise. */
+function tokenIat(jwt: string): number | null {
+ const part = jwt.split('.')[1]
+ if (!part) return null
+ try {
+ const claims = JSON.parse(Buffer.from(part, 'base64url').toString('utf8')) as { iat?: unknown }
+ return typeof claims.iat === 'number' ? claims.iat : null
+ } catch {
+ return null
+ }
+}
+
export class GameServer {
export class GameServer {
private readonly sessions = new Map<PlayerId, PlayerSession>()
private readonly sessions = new Map<PlayerId, PlayerSession>()
private readonly sessionsByConn = new Map<GameConnection, PlayerSession>()
private readonly sessionsByConn = new Map<GameConnection, PlayerSession>()
@@ -1213,6 +1225,7 @@ export class GameServer {
@@ -1213,6 +1225,7 @@ export class GameServer {
// recovery path when the token is gone.
// recovery path when the token is gone.
let token = msg.token
let token = msg.token
let subjectId: string | null = null
let subjectId: string | null = null
+ let authIat: number | null = null
let rank: 'owner' | 'admin' | 'moderator' | null = null
let rank: 'owner' | 'admin' | 'moderator' | null = null
if (msg.auth) {
if (msg.auth) {
const user = await resolveNetworkUser(this.config.networkAuthUrl, msg.auth)
const user = await resolveNetworkUser(this.config.networkAuthUrl, msg.auth)
@@ -1234,6 +1247,7 @@ export class GameServer {
@@ -1234,6 +1247,7 @@ export class GameServer {
}
}
token = account.key
token = account.key
subjectId = account.subjectId
subjectId = account.subjectId
+ authIat = tokenIat(msg.auth)
rank = user.rank
rank = user.rank
} else {
} else {
// A guest token may never look like an account key (`usr_…`,
// A guest token may never look like an account key (`usr_…`,
@@ -1298,6 +1312,7 @@ export class GameServer {
@@ -1298,6 +1312,7 @@ export class GameServer {
entityId: newEntityId(),
entityId: newEntityId(),
token,
token,
subjectId,
subjectId,
+ authIat,
rank,
rank,
name: msg.name,
name: msg.name,
spawn,
spawn,
@@ -2672,6 +2687,28 @@ export class GameServer {
@@ -2672,6 +2687,28 @@ export class GameServer {
}
}
/** Live map edit: every client refetches and rebuilds its terrain. */
/** Live map edit: every client refetches and rebuilds its terrain. */
+ /**
+ * Network moved this person's token cutoff (sign out everywhere, password changed, banned): close
+ * every session they opened with an older Network sign-in (4011 signed_out). The client's reconnect
+ * then fails its /api/auth/me check. Returns how many closed.
+ */
+ revokeSubject(subjectId: string, validAfterMs: number): number {
+ if (!subjectId || !Number.isFinite(validAfterMs)) return 0
+ let closed = 0
+ for (const session of [...this.sessions.values()]) {
+ if (session.subjectId !== subjectId) continue
+ if (session.authIat !== null && session.authIat * 1000 >= validAfterMs) continue
+ try {
+ session.send(encodeServerMessage({ t: 'reject', reason: 'signed_out' }))
+ } catch {
+ // closing anyway
+ }
+ session.closeConnection(4011, 'signed_out')
+ closed++
+ }
+ return closed
+ }
+
broadcastMapReload(): void {
broadcastMapReload(): void {
this.broadcastAll({ t: 'map_reload' })
this.broadcastAll({ t: 'map_reload' })
this.broadcastAll({ t: 'announce', text: '🗺 The world was reshaped by the map editors…' })
this.broadcastAll({ t: 'announce', text: '🗺 The world was reshaped by the map editors…' })
@@ -300,4 +300,19 @@ describe('Games on platform identity, events and mods (real server, restart)', (
@@ -300,4 +300,19 @@ describe('Games on platform identity, events and mods (real server, restart)', (
await b.outbox.stop()
await b.outbox.stop()
b.dispose()
b.dispose()
}, 60_000)
}, 60_000)
+
+ it('a sign-out everywhere closes the signed-in session, never a guest (WS-B task 4)', async () => {
+ const c = boot(platform)
+ const ana = connect(c.game)
+ expect((await ana.hello('abcdefgh12', 'tok-ana')).welcome).toBeDefined()
+ const guest = connect(c.game)
+ expect((await guest.hello('guest00042')).welcome).toBeDefined()
+ expect(c.game.revokeSubject('usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2ZZ', Date.now())).toBe(0)
+ expect(c.game.revokeSubject(SUBJECT, Date.now())).toBe(1)
+ expect(ana.inbox.some((m) => m.t === 'reject' && m.reason === 'signed_out')).toBe(true)
+ expect(guest.inbox.some((m) => m.t === 'reject')).toBe(false)
+ c.game.shutdown()
+ await c.outbox.stop()
+ c.dispose()
+ }, 60_000)
})
})
@@ -47,6 +47,11 @@ export interface PlayerSession {
@@ -47,6 +47,11 @@ export interface PlayerSession {
token: string
token: string
/** Canonical openvibe.network subject of the account; null for local guests. */
/** Canonical openvibe.network subject of the account; null for local guests. */
subjectId: string | null
subjectId: string | null
+ /**
+ * When the Network sign-in behind this session was issued (JWT iat, seconds); null for guests. A
+ * sign-out everywhere after it closes the session (network.user.token_valid_after).
+ */
+ authIat: number | null
/** Character slot under the account token (0..2). */
/** Character slot under the account token (0..2). */
charSlot: number
charSlot: number
/** openvibe.network rank; gates edit mode + moderation. */
/** openvibe.network rank; gates edit mode + moderation. */
@@ -113,6 +118,7 @@ export interface SessionInit {
@@ -113,6 +118,7 @@ export interface SessionInit {
entityId: EntityId
entityId: EntityId
token: string
token: string
subjectId?: string | null
subjectId?: string | null
+ authIat?: number | null
charSlot?: number
charSlot?: number
rank?: 'owner' | 'admin' | 'moderator' | null
rank?: 'owner' | 'admin' | 'moderator' | null
name: string
name: string
@@ -138,6 +144,7 @@ export function createSession(init: SessionInit): PlayerSession {
@@ -138,6 +144,7 @@ export function createSession(init: SessionInit): PlayerSession {
entityId: init.entityId,
entityId: init.entityId,
token: init.token,
token: init.token,
subjectId: init.subjectId ?? null,
subjectId: init.subjectId ?? null,
+ authIat: init.authIat ?? null,
charSlot: init.charSlot ?? 0,
charSlot: init.charSlot ?? 0,
rank: init.rank ?? null,
rank: init.rank ?? null,
name: init.name,
name: init.name,
@@ -34,6 +34,7 @@ import {
@@ -34,6 +34,7 @@ import {
} from './platform/gameEvents.js'
} from './platform/gameEvents.js'
import { MediaMirror } from './platform/mediaMirror.js'
import { MediaMirror } from './platform/mediaMirror.js'
import { createPlatformClient } from './platform/serviceClient.js'
import { createPlatformClient } from './platform/serviceClient.js'
+import { createRevocationEvents, ensureRevocationSubscription } from './platform/revocationEvents.js'
import { createStaffAuthorizer } from './platform/staffAuth.js'
import { createStaffAuthorizer } from './platform/staffAuth.js'
/**
/**
@@ -154,6 +155,30 @@ async function main(): Promise<void> {
@@ -154,6 +155,30 @@ async function main(): Promise<void> {
mods: modRuntime,
mods: modRuntime,
})
})
+ // Sign-out everywhere (network.user.token_valid_after): POST /internal/events closes the person's
+ // older game sessions; the subscription is created at boot when a secret is set (WS-B task 4).
+ const revocations = createRevocationEvents({
+ db: store.db,
+ secrets: config.platform.eventsSecrets,
+ onRevoked: (subject, validAfterMs) => game.revokeSubject(subject, validAfterMs),
+ log: platformLog,
+ })
+ if (platform && config.platform.eventsUrl && config.platform.eventsSecrets[0] && config.platform.eventsEndpoint) {
+ const subscribe = (attempt: number): void => {
+ ensureRevocationSubscription({
+ eventsUrl: config.platform.eventsUrl as string,
+ endpoint: config.platform.eventsEndpoint as string,
+ secret: config.platform.eventsSecrets[0] as string,
+ tokens: platform.tokens,
+ log: platformLog,
+ }).catch((err: unknown) => {
+ platformLog.warn('events subscription not ready (will retry)', { error: String((err as Error)?.message ?? err), attempt })
+ if (attempt < 5) setTimeout(() => subscribe(attempt + 1), 60_000 * attempt).unref()
+ })
+ }
+ subscribe(1)
+ }
+
// GET /release.json (D43): the deployed commit and package versions, as every OpenVibe service serves it.
// GET /release.json (D43): the deployed commit and package versions, as every OpenVibe service serves it.
const serveRelease = releaseHandler(buildRelease(process.cwd()))
const serveRelease = releaseHandler(buildRelease(process.cwd()))
@@ -211,6 +236,7 @@ async function main(): Promise<void> {
@@ -211,6 +236,7 @@ async function main(): Promise<void> {
config.oauth,
config.oauth,
{
{
handle: (req, res) => {
handle: (req, res) => {
+ if (revocations.handle(req, res)) return true
if (readiness.handle(req, res)) return true
if (readiness.handle(req, res)) return true
if (serveRelease(req, res)) return true
if (serveRelease(req, res)) return true
if ((req.url ?? '').split('?')[0] === '/api/v1/platform' && req.method === 'GET') {
if ((req.url ?? '').split('?')[0] === '/api/v1/platform' && req.method === 'GET') {
new file mode 100644
new file mode 100644
@@ -0,0 +1,91 @@
@@ -0,0 +1,91 @@
+import { createServer, type Server } from 'node:http'
+import type { AddressInfo } from 'node:net'
+import { createRequire } from 'node:module'
+import { createConsoleLogger } from '@openvibe/shared'
+import { afterAll, beforeAll, describe, expect, it } from 'vitest'
+import { createRevocationEvents, ensureRevocationSubscription, TOPIC } from './revocationEvents.js'
+
+/**
+ * POST /internal/events for network.user.token_valid_after: signature v2 only, loopback only, the
+ * cutoff moves once (a redelivery changes nothing, anything not from Network is ignored) and the
+ * person's sessions are closed through onRevoked; the boot subscription is created once.
+ */
+const req = createRequire(import.meta.url)
+const Database = req('better-sqlite3') as new (path: string) => unknown
+const { signDeliveryHeaders } = req('openvibe-sdk/events') as {
+ signDeliveryHeaders(body: string, secret: string): Record<string, string>
+}
+const SECRET = 'x'.repeat(40)
+const SUBJECT = 'usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3'
+const log = createConsoleLogger({ app: 'test' }, 'error')
+const closedFor: [string, number][] = []
+const events = createRevocationEvents({
+ db: new Database(':memory:'),
+ secrets: [SECRET],
+ onRevoked: (s, ms) => {
+ closedFor.push([s, ms])
+ return 2
+ },
+ log,
+})
+let server: Server
+let url: string
+
+beforeAll(async () => {
+ server = createServer((req, res) => {
+ if (!events.handle(req, res)) {
+ res.writeHead(404)
+ res.end()
+ }
+ })
+ await new Promise<void>((r) => server.listen(0, '127.0.0.1', r))
+ url = `http://127.0.0.1:${(server.address() as AddressInfo).port}/internal/events`
+})
+afterAll(() => server.close())
+
+const at = Date.parse('2026-09-24T20:00:00Z')
+const envelope = (over: Record<string, unknown> = {}) => ({
+ event_id: 'evt_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3',
+ event_type: TOPIC,
+ version: 1,
+ source: 'network',
+ payload: { subject: { type: 'user', id: SUBJECT }, valid_after: new Date(at).toISOString(), reason: 'signed_out_everywhere' },
+ ...over,
+})
+const post = async (event: unknown, headers: Record<string, string> = {}, secret = SECRET) => {
+ const body = JSON.stringify({ event, seq: 1 })
+ const r = await fetch(url, { method: 'POST', body, headers: { 'content-type': 'application/json', ...signDeliveryHeaders(body, secret), ...headers } })
+ return { status: r.status, body: (await r.json()) as { outcome?: string; closed?: number } }
+}
+
+describe('revocation events', () => {
+ it('applies a signed delivery once and closes the sessions', async () => {
+ expect(await post(envelope())).toMatchObject({ status: 200, body: { outcome: 'revoked', closed: 2 } })
+ expect(closedFor).toEqual([[SUBJECT, at]])
+ expect(events.store.isRevoked({ subject_id: SUBJECT, iat: at / 1000 - 1 })).toBe(true)
+ expect((await post(envelope())).body.outcome).toBe('unchanged')
+ expect((await post(envelope({ source: 'live' }))).body.outcome).toBe('ignored:source')
+ expect(closedFor.length).toBe(1)
+ })
+
+ it('refuses forged, proxied and non-POST requests', async () => {
+ expect((await post(envelope(), {}, 'y'.repeat(40))).status).toBe(401)
+ expect((await post(envelope(), { 'x-forwarded-for': '1.2.3.4' })).status).toBe(403)
+ expect((await fetch(url)).status).toBe(405)
+ })
+
+ it('creates the subscription once', async () => {
+ const subs: { topic_pattern: string; endpoint: string }[] = []
+ const fake = (async (u: string, init?: RequestInit) => {
+ if (init?.method === 'POST') {
+ subs.push(JSON.parse(String(init.body)) as { topic_pattern: string; endpoint: string })
+ return Response.json({ id: 'sub_1' }, { status: 201 })
+ }
+ return Response.json({ subscriptions: subs.map((s, i) => ({ id: `sub_${i + 1}`, ...s })) })
+ }) as unknown as typeof fetch
+ const opts = { eventsUrl: 'http://events.test', endpoint: 'http://127.0.0.1:8000/internal/events', secret: SECRET, tokens: { getToken: async () => 'tok' }, fetchImpl: fake, log }
+ expect(await ensureRevocationSubscription(opts)).toBe('created')
+ expect(await ensureRevocationSubscription(opts)).toBe('exists')
+ expect(subs).toEqual([{ topic_pattern: TOPIC, endpoint: opts.endpoint, secret: SECRET }])
+ })
+})
new file mode 100644
new file mode 100644
@@ -0,0 +1,140 @@
@@ -0,0 +1,140 @@
+/**
+ * Sign-out everywhere reaches Games (roadmap WS-B task 4; Contracts 0.39.0
+ * network.user.token_valid_after).
+ *
+ * POST /internal/events is the endpoint of Games' Events subscription to that topic, signed with
+ * GAMES_EVENTS_SECRET (signature v2 only) and loopback only (a request carrying a forwarding header
+ * came through nginx and is refused). A delivery moves the person's cutoff in the SDK's revocation
+ * store (only ever forward; anything not from Network is ignored), then `onRevoked` closes the game
+ * sessions they opened with an older sign-in. New sign-ins already fail: Games checks every token with
+ * the Network's /api/auth/me, which applies the same cutoff.
+ */
+import type { IncomingMessage, ServerResponse } from 'node:http'
+import { createRequire } from 'node:module'
+import type { Logger } from '@openvibe/shared'
+
+const req = createRequire(import.meta.url)
+
+interface RevocationStore {
+ apply(event: unknown): string
+ isRevoked(claims: { iat?: number; subject_id?: string } | null | undefined): boolean
+ cutoffFor(subject: string): number
+}
+type ParseDelivery = (
+ raw: Buffer,
+ headers: IncomingMessage['headers'],
+ secret: string,
+ opts: { requireV2: boolean },
+) => { event?: { event_id?: string; event_type?: string; payload?: { subject?: { id?: string }; valid_after?: string } } } | null
+
+const { createRevocationStore } = req('openvibe-sdk/auth') as {
+ createRevocationStore(db: unknown, opts?: { table?: string }): RevocationStore
+}
+const { parseDelivery } = req('openvibe-sdk/events') as { parseDelivery: ParseDelivery }
+
+export const TOPIC = 'network.user.token_valid_after'
+const PATH = '/internal/events'
+const MAX_BODY = 256 * 1024
+
+export interface RevocationEvents {
+ /** Answers POST /internal/events (returns true when the request was ours). */
+ handle(req: IncomingMessage, res: ServerResponse): boolean
+ store: RevocationStore
+ stats: { received: number; revoked: number; closed: number; refused: number; ignored: number }
+}
+
+export function createRevocationEvents(opts: {
+ db: unknown
+ secrets: string[]
+ onRevoked: (subjectId: string, validAfterMs: number) => number
+ log: Logger
+}): RevocationEvents {
+ const store = createRevocationStore(opts.db, { table: 'token_revocations' })
+ const stats = { received: 0, revoked: 0, closed: 0, refused: 0, ignored: 0 }
+ const send = (res: ServerResponse, status: number, body: unknown): void => {
+ res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' })
+ res.end(JSON.stringify(body))
+ }
+
+ function handle(req: IncomingMessage, res: ServerResponse): boolean {
+ if ((req.url ?? '').split('?')[0] !== PATH) return false
+ if (req.method !== 'POST') {
+ send(res, 405, { error: 'POST only' })
+ return true
+ }
+ if (req.headers['x-forwarded-for'] || req.headers['x-real-ip'] || req.headers['cf-connecting-ip']) {
+ send(res, 403, { error: 'internal route' })
+ return true
+ }
+ if (opts.secrets.length === 0) {
+ send(res, 503, { error: 'GAMES_EVENTS_SECRET is not set' })
+ return true
+ }
+ const chunks: Buffer[] = []
+ let size = 0
+ req.on('data', (c: Buffer) => {
+ size += c.length
+ if (size > MAX_BODY) req.destroy()
+ else chunks.push(c)
+ })
+ req.on('end', () => {
+ const raw = Buffer.concat(chunks)
+ let delivery: ReturnType<ParseDelivery> = null
+ for (const s of opts.secrets) {
+ delivery = parseDelivery(raw, req.headers, s, { requireV2: true })
+ if (delivery) break
+ }
+ if (!delivery || !delivery.event) {
+ stats.refused++
+ send(res, 401, { error: 'bad signature' })
+ return
+ }
+ stats.received++
+ const event = delivery.event
+ const outcome = store.apply(event)
+ let closed = 0
+ if (outcome === 'revoked') {
+ stats.revoked++
+ const subject = event.payload?.subject?.id ?? ''
+ closed = opts.onRevoked(subject, Date.parse(event.payload?.valid_after ?? ''))
+ stats.closed += closed
+ if (closed) opts.log.info('signed out everywhere: sessions closed', { closed })
+ } else if (outcome.startsWith('ignored')) stats.ignored++
+ send(res, 200, { event_id: event.event_id ?? null, outcome, closed })
+ })
+ return true
+ }
+
+ return { handle, store, stats }
+}
+
+/**
+ * Create Games' subscription to TOPIC at Events if it is missing (idempotent: an existing one, even
+ * disabled by an operator, is left as it is). Needs the grant games events.subscription.manage.
+ */
+export async function ensureRevocationSubscription(opts: {
+ eventsUrl: string
+ endpoint: string
+ secret: string
+ tokens: { getToken(ctx?: { audience?: string; scope?: string }): Promise<string> }
+ fetchImpl?: typeof fetch
+ log: Logger
+}): Promise<'exists' | 'created'> {
+ const f = opts.fetchImpl ?? fetch
+ const token = await opts.tokens.getToken({ audience: 'openvibe.events', scope: 'events.subscription.manage' })
+ const headers = { authorization: `Bearer ${token}`, accept: 'application/json' }
+ const list = await f(`${opts.eventsUrl}/api/v1/subscriptions`, { headers })
+ if (!list.ok) throw new Error(`Events answered ${list.status} listing subscriptions`)
+ const body = (await list.json()) as { subscriptions?: { id: string; topic_pattern: string; endpoint: string }[] }
+ const found = (body.subscriptions ?? []).find((s) => s.topic_pattern === TOPIC && s.endpoint === opts.endpoint)
+ if (found) return 'exists'
+ const r = await f(`${opts.eventsUrl}/api/v1/subscriptions`, {
+ method: 'POST',
+ headers: { ...headers, 'content-type': 'application/json' },
+ body: JSON.stringify({ topic_pattern: TOPIC, endpoint: opts.endpoint, secret: opts.secret }),
+ })
+ if (r.status === 409) return 'exists'
+ if (!r.ok) throw new Error(`Events answered ${r.status} creating the ${TOPIC} subscription`)
+ opts.log.info('events subscription created', { topic: TOPIC, endpoint: opts.endpoint })
+ return 'created'
+}
@@ -16,6 +16,8 @@ import type { PlatformConfig } from '../config.js'
@@ -16,6 +16,8 @@ import type { PlatformConfig } from '../config.js'
export interface PlatformClient {
export interface PlatformClient {
client: OpenVibeClient
client: OpenVibeClient
clientId: string
clientId: string
+ /** The `games` principal's tokens, for calls the SDK client has no method for (Events subscriptions). */
+ tokens: { getToken(ctx?: { audience?: string; scope?: string }): Promise<string> }
}
}
/** Null when no client secret is configured: Games then makes no service calls. */
/** Null when no client secret is configured: Games then makes no service calls. */
@@ -42,5 +44,5 @@ export function createPlatformClient(
@@ -42,5 +44,5 @@ export function createPlatformClient(
retries: 0,
retries: 0,
...(fetchImpl ? { fetch: fetchImpl } : {}),
...(fetchImpl ? { fetch: fetchImpl } : {}),
})
})
- return { client, clientId: cfg.clientId }
+ return { client, clientId: cfg.clientId, tokens }
}
}
@@ -29,7 +29,13 @@ export interface ServerWelcome {
@@ -29,7 +29,13 @@ export interface ServerWelcome {
export interface ServerReject {
export interface ServerReject {
t: 'reject'
t: 'reject'
reason:
reason:
- 'protocol_mismatch' | 'server_full' | 'invalid_hello' | 'guest_one_character' | 'auth_failed'
+ | 'protocol_mismatch'
+ | 'server_full'
+ | 'invalid_hello'
+ | 'guest_one_character'
+ | 'auth_failed'
+ /** The person signed out everywhere (or changed their password, or was banned) on the Network. */
+ | 'signed_out'
}
}
/** Entities that became relevant to this client (full state). */
/** Entities that became relevant to this client (full state). */
@@ -112,11 +112,11 @@ importers:
@@ -112,11 +112,11 @@ 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.38.0
- version: https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.38.0
+ specifier: https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.39.0
+ version: https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.39.0
openvibe-sdk:
openvibe-sdk:
- specifier: https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.6.0
- version: https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.6.0
+ specifier: https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.9.1
+ version: https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.9.1
tsx:
tsx:
specifier: ^4.19.3
specifier: ^4.19.3
version: 4.23.11
version: 4.23.11
@@ -1326,15 +1326,15 @@ packages:
@@ -1326,15 +1326,15 @@ 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.38.0:
- resolution: {tarball: https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.38.0}
- version: 0.38.0
+ openvibe-contracts@https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.39.0:
+ resolution: {tarball: https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.39.0}
+ version: 0.39.0
engines: {node: '>=20'}
engines: {node: '>=20'}
hasBin: true
hasBin: true
- openvibe-sdk@https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.6.0:
- resolution: {tarball: https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.6.0}
- version: 0.6.0
+ openvibe-sdk@https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.9.1:
+ resolution: {tarball: https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.9.1}
+ version: 0.9.1
engines: {node: '>=20'}
engines: {node: '>=20'}
@@ -2742,12 +2742,12 @@ snapshots:
@@ -2742,12 +2742,12 @@ 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.38.0:
+ openvibe-contracts@https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.39.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])
- openvibe-sdk@https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.6.0: {}
+ openvibe-sdk@https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.9.1: {}
dependencies:
dependencies: