openvibers/openvibe.games Commits

Prettier on the files the mod-principal and budget work touched (networkGrants, registry, runtime, routes' tests, revocationEvents, modLifecycleProof): formatting only, no change in behaviour.

OP OpenVibers committed 28 Sept 2026 · b6a825d81e · 8 files, +461 −89


Changes

ddiff --git a/apps/server/scripts/modLifecycleProof.ts b/apps/server/scripts/modLifecycleProof.ts
iindex b151f1a..32a8ac9 100644
---- a/apps/server/scripts/modLifecycleProof.ts
++++ b/apps/server/scripts/modLifecycleProof.ts
@@@ -49,7 +49,8 @@ async function token(): Promise<string> {
}),
})
const body = (await res.json()) as { access_token?: string; error?: string }
-- if (!res.ok || !body.access_token) throw new Error(`no games.mod.manage token: ${res.status} ${body.error ?? ''}`)
++ if (!res.ok || !body.access_token)
++ throw new Error(`no games.mod.manage token: ${res.status} ${body.error ?? ''}`)
return body.access_token
}
@@@ -61,7 +62,10 @@ async function main(): Promise<void> {
headers: { authorization: `Bearer ${tok}`, 'content-type': 'application/json' },
...(body ? { body: JSON.stringify(body) } : {}),
})
-- return { status: res.status, body: (await res.json().catch(() => ({}))) as Record<string, unknown> }
++ return {
++ status: res.status,
++ body: (await res.json().catch(() => ({}))) as Record<string, unknown>,
++ }
}
const id = `mod_${ulid()}`
const failures: string[] = []
@@@ -73,7 +77,8 @@ async function main(): Promise<void> {
id,
name: 'Lifecycle proof',
version: '1.0.0',
-- description: 'WS-M task 6: install, grant, use, revoke. One inert prop far from spawn; retracted at the end.',
++ description:
++ 'WS-M task 6: install, grant, use, revoke. One inert prop far from spawn; retracted at the end.',
// A publisher is a user or an app; the proof publishes as a synthetic app of its own.
publisher: { type: 'app', id: `app_${ulid()}` },
target: 'games.browser',
@@@ -85,11 +90,24 @@ async function main(): Promise<void> {
const pack = { props: [{ key: 'proof-bench', item: 'workbench', pos: [3900, 0, 3900] }] }
let r = await api('POST', '', { manifest, pack, approve: [], enable: true })
-- step('install', r.status === 201 && Array.isArray(r.body.granted) && (r.body.granted as unknown[]).length === 0, `${r.status} ${id} granted=${JSON.stringify(r.body.granted)}`)
++ step(
++ 'install',
++ r.status === 201 && Array.isArray(r.body.granted) && (r.body.granted as unknown[]).length === 0,
++ `${r.status} ${id} granted=${JSON.stringify(r.body.granted)}`,
++ )
r = await api('POST', `/${id}/grants`, { capability: CAP })
-- step('grant', r.status === 200 && JSON.stringify(r.body.granted) === JSON.stringify([CAP]), `${r.status} granted=${JSON.stringify(r.body.granted)}`)
++ step(
++ 'grant',
++ r.status === 200 && JSON.stringify(r.body.granted) === JSON.stringify([CAP]),
++ `${r.status} granted=${JSON.stringify(r.body.granted)}`,
++ )
-- const audit = async () => ((await api('GET', `/${id}/audit?limit=100`)).body.audit ?? []) as { action: string; capability: string | null; actor: string }[]
++ const audit = async () =>
++ ((await api('GET', `/${id}/audit?limit=100`)).body.audit ?? []) as {
++ action: string
++ capability: string | null
++ actor: string
++ }[]
let used = false
for (let i = 0; i < 30 && !used; i++) {
await new Promise((res) => setTimeout(res, 1000))
@@@ -98,7 +116,11 @@ async function main(): Promise<void> {
step('use', used, used ? 'the runtime placed the prop' : 'no use within 30 s')
r = await api('POST', `/${id}/revoke`, { reason: 'WS-M task 6 lifecycle proof: done' })
-- step('revoke', r.status === 200 && r.body.status === 'revoked', `${r.status} status=${String(r.body.status)}`)
++ step(
++ 'revoke',
++ r.status === 200 && r.body.status === 'revoked',
++ `${r.status} status=${String(r.body.status)}`,
++ )
let retracted = false
for (let i = 0; i < 30 && !retracted; i++) {
await new Promise((res) => setTimeout(res, 1000))
ddiff --git a/apps/server/src/mods/networkGrants.test.ts b/apps/server/src/mods/networkGrants.test.ts
iindex e4fbe63..bddb5be 100644
---- a/apps/server/src/mods/networkGrants.test.ts
++++ b/apps/server/src/mods/networkGrants.test.ts
@@@ -5,7 +5,11 @@ import { openSqliteStore } from '@openvibe/persistence/sqlite'
import { createConsoleLogger } from '@openvibe/shared'
import { afterEach, describe, expect, it } from 'vitest'
import { CAP_ANNOUNCE, CAP_PLACE_PROP } from './contentPack.js'
--import { createNetworkModGrants, type ModGrantAuthority, type ModPrincipal } from './networkGrants.js'
++import {
++ createNetworkModGrants,
++ type ModGrantAuthority,
++ type ModPrincipal,
++} from './networkGrants.js'
import { ModError, ModRegistry, type ModActor } from './registry.js'
import { handleModsRequest } from './routes.js'
import { MOD_ID, sampleManifest } from './testFixtures.js'
@@@ -17,7 +21,10 @@ import { MOD_ID, sampleManifest } from './testFixtures.js'
* client maps Network's answers to ModErrors.
*/
const log = createConsoleLogger({ app: 'test' }, 'error')
--const STAFF: ModActor = { audit: 'usr_01JABCDEFGHJKMNPQRSTVWXYZ0', subject: { type: 'user', id: 'usr_01JABCDEFGHJKMNPQRSTVWXYZ0' } }
++const STAFF: ModActor = {
++ audit: 'usr_01JABCDEFGHJKMNPQRSTVWXYZ0',
++ subject: { type: 'user', id: 'usr_01JABCDEFGHJKMNPQRSTVWXYZ0' },
++}
let server: Server | null = null
afterEach(() => {
server?.close()
@@@ -30,23 +37,38 @@ function fakeNetwork(allow: string[]) {
let principal: ModPrincipal | null = null
const state = { down: false }
const ensure = () => {
-- if (state.down) throw new ModError('mod.grants_unavailable', 'OpenVibe.Network could not be asked', 503)
++ if (state.down)
++ throw new ModError('mod.grants_unavailable', 'OpenVibe.Network could not be asked', 503)
if (!principal) throw new ModError('mod.not_found', 'no such mod principal', 404)
return principal
}
const grants: ModGrantAuthority = {
async register(manifest, approve) {
calls.push(`register:${[...approve].join(',')}`)
-- if (state.down) throw new ModError('mod.grants_unavailable', 'OpenVibe.Network could not be asked', 503)
-- const requested = (manifest as { permissions: { capabilities: string[] } }).permissions.capabilities
++ if (state.down)
++ throw new ModError('mod.grants_unavailable', 'OpenVibe.Network could not be asked', 503)
++ const requested = (manifest as { permissions: { capabilities: string[] } }).permissions
++ .capabilities
const approved = approve.filter((c) => allow.includes(c))
-- principal = { mod_id: MOD_ID, owner: 'games', status: 'active', requested, approved, pending: requested.filter((c) => !approved.includes(c)), revoked: [], revision: 1 }
++ principal = {
++ mod_id: MOD_ID,
++ owner: 'games',
++ status: 'active',
++ requested,
++ approved,
++ pending: requested.filter((c) => !approved.includes(c)),
++ revoked: [],
++ revision: 1,
++ }
return principal
},
async change(_id, capability, action) {
calls.push(`${action}:${capability}`)
const p = ensure()
-- p.approved = action === 'approve' ? [...new Set([...p.approved, capability])] : p.approved.filter((c) => c !== capability)
++ p.approved =
++ action === 'approve'
++ ? [...new Set([...p.approved, capability])]
++ : p.approved.filter((c) => c !== capability)
p.revision++
return p
},
@@@ -74,37 +96,68 @@ async function start(grants: ModGrantAuthority) {
})
await new Promise<void>((r) => server!.listen(0, '127.0.0.1', r))
const base = `http://127.0.0.1:${(server.address() as AddressInfo).port}/api/v1/mods`
-- const post = (path: string, body: unknown) => fetch(`${base}${path}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) })
++ const post = (path: string, body: unknown) =>
++ fetch(`${base}${path}`, {
++ method: 'POST',
++ headers: { 'content-type': 'application/json' },
++ body: JSON.stringify(body),
++ })
return { registry, base, post }
}
--const manifest = sampleManifest({ permissions: { capabilities: [CAP_PLACE_PROP, CAP_ANNOUNCE] } } as never)
++const manifest = sampleManifest({
++ permissions: { capabilities: [CAP_PLACE_PROP, CAP_ANNOUNCE] },
++} as never)
const pack = { props: [{ key: 'bench', item: 'workbench', pos: [0, 0, 0] }] }
describe('mod principals in Network', () => {
it('installs with what Network approved, and asks it before every grant change', async () => {
const net = fakeNetwork([CAP_PLACE_PROP])
const { registry, post, base } = await start(net.grants)
-- const created = await post('', { manifest, pack, approve: [CAP_PLACE_PROP, CAP_ANNOUNCE], enable: true })
++ const created = await post('', {
++ manifest,
++ pack,
++ approve: [CAP_PLACE_PROP, CAP_ANNOUNCE],
++ enable: true,
++ })
expect(created.status).toBe(201)
expect(await created.json()).toMatchObject({ id: MOD_ID, granted: [CAP_PLACE_PROP] })
expect(net.calls).toEqual([`register:${CAP_PLACE_PROP},${CAP_ANNOUNCE}`])
expect((await post(`/${MOD_ID}/grants`, { capability: CAP_ANNOUNCE })).status).toBe(200)
expect(registry.isGranted(MOD_ID, CAP_ANNOUNCE)).toBe(true)
-- expect((await fetch(`${base}/${MOD_ID}/grants/${CAP_ANNOUNCE}`, { method: 'DELETE' })).status).toBe(200)
++ expect(
++ (await fetch(`${base}/${MOD_ID}/grants/${CAP_ANNOUNCE}`, { method: 'DELETE' })).status,
++ ).toBe(200)
expect(registry.isGranted(MOD_ID, CAP_ANNOUNCE)).toBe(false)
expect(net.calls.slice(1)).toEqual([`approve:${CAP_ANNOUNCE}`, `revoke:${CAP_ANNOUNCE}`])
// Network's events arrive after the API applied its answers: an older revision changes nothing.
expect((await post(`/${MOD_ID}/grants`, { capability: CAP_ANNOUNCE })).status).toBe(200)
-- registry.applyNetwork({ mod_id: MOD_ID, status: 'active', approved: [CAP_PLACE_PROP], revision: 1 })
-- registry.applyNetwork({ mod_id: MOD_ID, status: 'active', approved: [CAP_PLACE_PROP], revision: 3 })
++ registry.applyNetwork({
++ mod_id: MOD_ID,
++ status: 'active',
++ approved: [CAP_PLACE_PROP],
++ revision: 1,
++ })
++ registry.applyNetwork({
++ mod_id: MOD_ID,
++ status: 'active',
++ approved: [CAP_PLACE_PROP],
++ revision: 3,
++ })
expect(registry.isGranted(MOD_ID, CAP_ANNOUNCE)).toBe(true)
expect(registry.auditLog(MOD_ID).some((a) => a.actor === 'network')).toBe(false)
// A newer one (a staff change in Network) applies.
-- registry.applyNetwork({ mod_id: MOD_ID, status: 'active', approved: [CAP_PLACE_PROP], revision: 5 })
++ registry.applyNetwork({
++ mod_id: MOD_ID,
++ status: 'active',
++ approved: [CAP_PLACE_PROP],
++ revision: 5,
++ })
expect(registry.isGranted(MOD_ID, CAP_ANNOUNCE)).toBe(false)
-- expect((await fetch(`${base}/${MOD_ID}/grants/${CAP_ANNOUNCE}`, { method: 'DELETE' })).status).toBe(200)
++ expect(
++ (await fetch(`${base}/${MOD_ID}/grants/${CAP_ANNOUNCE}`, { method: 'DELETE' })).status,
++ ).toBe(200)
// Network unreachable: 503, and nothing changes here.
net.state.down = true
@@@ -130,10 +183,20 @@ describe('mod principals in Network', () => {
it("applyNetwork makes an install's copy follow its principal", () => {
const registry = new ModRegistry(openSqliteStore(':memory:'), createContent())
registry.install({ manifest, pack, approve: [CAP_PLACE_PROP], enable: true }, STAFF)
-- expect(registry.applyNetwork({ mod_id: 'mod_01JZZZZZZZZZZZZZZZZZZZZZZZ', status: 'active', approved: [] })).toBeNull()
++ expect(
++ registry.applyNetwork({
++ mod_id: 'mod_01JZZZZZZZZZZZZZZZZZZZZZZZ',
++ status: 'active',
++ approved: [],
++ }),
++ ).toBeNull()
registry.applyNetwork({ mod_id: MOD_ID, status: 'active', approved: [CAP_ANNOUNCE] })
expect([...(registry.get(MOD_ID)?.granted ?? [])]).toEqual([CAP_ANNOUNCE])
-- registry.applyNetwork({ mod_id: MOD_ID, status: 'active', approved: [CAP_ANNOUNCE, 'games.not.bindable'] })
++ registry.applyNetwork({
++ mod_id: MOD_ID,
++ status: 'active',
++ approved: [CAP_ANNOUNCE, 'games.not.bindable'],
++ })
expect([...(registry.get(MOD_ID)?.granted ?? [])]).toEqual([CAP_ANNOUNCE])
expect(registry.auditLog(MOD_ID).some((a) => a.actor === 'network')).toBe(true)
registry.applyNetwork({ mod_id: MOD_ID, status: 'revoked', approved: [] })
@@@ -144,19 +207,51 @@ describe('mod principals in Network', () => {
it('the client sends the principal token and maps Network answers', async () => {
const seen: { url: string; auth: string | null; body: unknown }[] = []
const answers: Response[] = [
-- Response.json({ mod_id: MOD_ID, owner: 'games', status: 'active', requested: [], approved: [], pending: [], revoked: [], revision: 1 }, { status: 201 }),
++ Response.json(
++ {
++ mod_id: MOD_ID,
++ owner: 'games',
++ status: 'active',
++ requested: [],
++ approved: [],
++ pending: [],
++ revoked: [],
++ revision: 1,
++ },
++ { status: 201 },
++ ),
Response.json({ error: 'mod.not_requested', detail: 'not requested' }, { status: 422 }),
Response.json({ error: 'server' }, { status: 502 }),
]
const fetchImpl = (async (url: string, init?: RequestInit) => {
-- seen.push({ url, auth: new Headers(init?.headers).get('authorization'), body: init?.body ? JSON.parse(String(init.body)) : null })
++ seen.push({
++ url,
++ auth: new Headers(init?.headers).get('authorization'),
++ body: init?.body ? JSON.parse(String(init.body)) : null,
++ })
return answers.shift() as Response
}) as unknown as typeof fetch
const scopes: (string | undefined)[] = []
-- const client = createNetworkModGrants({ networkUrl: 'http://network.test', tokens: { getToken: async (ctx) => { scopes.push(ctx?.scope); return 'tok' } }, fetchImpl })
++ const client = createNetworkModGrants({
++ networkUrl: 'http://network.test',
++ tokens: {
++ getToken: async (ctx) => {
++ scopes.push(ctx?.scope)
++ return 'tok'
++ },
++ },
++ fetchImpl,
++ })
expect((await client.register(manifest, [CAP_PLACE_PROP], 'usr_x')).revision).toBe(1)
-- expect(seen[0]).toMatchObject({ url: 'http://network.test/internal/mods', auth: 'Bearer tok', body: { approve: [CAP_PLACE_PROP], actor: 'usr_x' } })
-- await expect(client.change(MOD_ID, CAP_ANNOUNCE, 'approve', 'usr_x')).rejects.toMatchObject({ code: 'mod.not_requested', status: 422 })
++ expect(seen[0]).toMatchObject({
++ url: 'http://network.test/internal/mods',
++ auth: 'Bearer tok',
++ body: { approve: [CAP_PLACE_PROP], actor: 'usr_x' },
++ })
++ await expect(client.change(MOD_ID, CAP_ANNOUNCE, 'approve', 'usr_x')).rejects.toMatchObject({
++ code: 'mod.not_requested',
++ status: 422,
++ })
await expect(client.revoke(MOD_ID, 'usr_x')).rejects.toMatchObject({ status: 503 })
expect(scopes.every((s) => s === 'mods.grant.manage')).toBe(true)
})
ddiff --git a/apps/server/src/mods/networkGrants.ts b/apps/server/src/mods/networkGrants.ts
iindex 295d719..c48a5cc 100644
---- a/apps/server/src/mods/networkGrants.ts
++++ b/apps/server/src/mods/networkGrants.ts
@@@ -22,7 +22,12 @@ export interface ModPrincipal {
export interface ModGrantAuthority {
register(manifest: unknown, approve: readonly string[], actor: string): Promise<ModPrincipal>
-- change(modId: string, capability: string, action: 'approve' | 'revoke', actor: string): Promise<ModPrincipal>
++ change(
++ modId: string,
++ capability: string,
++ action: 'approve' | 'revoke',
++ actor: string,
++ ): Promise<ModPrincipal>
revoke(modId: string, actor: string, reason?: string): Promise<ModPrincipal>
list(): Promise<ModPrincipal[]>
}
@@@ -36,30 +41,59 @@ export function createNetworkModGrants(opts: {
async function call<T>(method: 'GET' | 'POST', path: string, body?: unknown): Promise<T> {
let res: Response
try {
-- const token = await opts.tokens.getToken({ audience: 'openvibe.network', scope: 'mods.grant.manage' })
++ const token = await opts.tokens.getToken({
++ audience: 'openvibe.network',
++ scope: 'mods.grant.manage',
++ })
res = await f(`${opts.networkUrl}/internal/mods${path}`, {
method,
-- headers: { authorization: `Bearer ${token}`, accept: 'application/json', ...(body ? { 'content-type': 'application/json' } : {}) },
++ headers: {
++ authorization: `Bearer ${token}`,
++ accept: 'application/json',
++ ...(body ? { 'content-type': 'application/json' } : {}),
++ },
...(body ? { body: JSON.stringify(body) } : {}),
signal: AbortSignal.timeout(10_000),
})
} catch (err) {
-- throw new ModError('mod.grants_unavailable', `OpenVibe.Network could not be asked: ${String((err as Error)?.message ?? err)}`, 503)
++ throw new ModError(
++ 'mod.grants_unavailable',
++ `OpenVibe.Network could not be asked: ${String((err as Error)?.message ?? err)}`,
++ 503,
++ )
}
const data = (await res.json().catch(() => ({}))) as Record<string, unknown>
if (!res.ok) {
-- const code = typeof data.error === 'string' ? data.error : typeof data.code === 'string' ? data.code : 'mod.grants_refused'
-- const status = res.status >= 500 ? 503 : res.status === 401 || res.status === 403 ? 503 : res.status
-- throw new ModError(code, `OpenVibe.Network: ${typeof data.detail === 'string' ? data.detail : `HTTP ${res.status}`}`, status)
++ const code =
++ typeof data.error === 'string'
++ ? data.error
++ : typeof data.code === 'string'
++ ? data.code
++ : 'mod.grants_refused'
++ const status =
++ res.status >= 500 ? 503 : res.status === 401 || res.status === 403 ? 503 : res.status
++ throw new ModError(
++ code,
++ `OpenVibe.Network: ${typeof data.detail === 'string' ? data.detail : `HTTP ${res.status}`}`,
++ status,
++ )
}
return data as T
}
return {
-- register: (manifest, approve, actor) => call<ModPrincipal>('POST', '', { manifest, approve: [...approve], actor }),
++ register: (manifest, approve, actor) =>
++ call<ModPrincipal>('POST', '', { manifest, approve: [...approve], actor }),
change: (modId, capability, action, actor) =>
-- call<ModPrincipal>('POST', `/${encodeURIComponent(modId)}/grants`, { capability, action, actor }),
++ call<ModPrincipal>('POST', `/${encodeURIComponent(modId)}/grants`, {
++ capability,
++ action,
++ actor,
++ }),
revoke: (modId, actor, reason) =>
-- call<ModPrincipal>('POST', `/${encodeURIComponent(modId)}/revoke`, { actor, ...(reason ? { reason } : {}) }),
++ call<ModPrincipal>('POST', `/${encodeURIComponent(modId)}/revoke`, {
++ actor,
++ ...(reason ? { reason } : {}),
++ }),
list: async () => (await call<{ mods: ModPrincipal[] }>('GET', '')).mods ?? [],
}
}
ddiff --git a/apps/server/src/mods/registry.ts b/apps/server/src/mods/registry.ts
iindex 4923795..6683a62 100644
---- a/apps/server/src/mods/registry.ts
++++ b/apps/server/src/mods/registry.ts
@@@ -60,7 +60,10 @@ export interface ModActor {
export const SYSTEM_ACTOR: ModActor = { audit: 'games', subject: { type: 'service', id: 'games' } }
/** Changes that come from OpenVibe.Network's mod principal (network.mod.grants_changed). */
--export const NETWORK_ACTOR: ModActor = { audit: 'network', subject: { type: 'service', id: 'network' } }
++export const NETWORK_ACTOR: ModActor = {
++ audit: 'network',
++ subject: { type: 'service', id: 'network' },
++}
export class ModError extends Error {
constructor(
@@@ -311,7 +314,10 @@ export class ModRegistry {
* a revoked principal revokes the install; otherwise capabilities Network does not approve are revoked here and
* approved ones this runtime can bind are granted. Unknown installs are ignored. Returns the view, or null.
*/
-- applyNetwork(principal: { mod_id: string; status: string; approved: readonly string[]; revision?: number }, actor: ModActor = NETWORK_ACTOR): ModView | null {
++ applyNetwork(
++ principal: { mod_id: string; status: string; approved: readonly string[]; revision?: number },
++ actor: ModActor = NETWORK_ACTOR,
++ ): ModView | null {
const view = this.views.get(principal.mod_id)
if (!view) return null
// Events can arrive after the API already applied a newer answer: an older (or equal) revision changes nothing.
@@@ -319,11 +325,13 @@ export class ModRegistry {
if (principal.revision <= (this.networkRevision.get(view.mod.id) ?? 0)) return view
this.networkRevision.set(view.mod.id, principal.revision)
}
-- if (principal.status === 'revoked') return this.revoke(view.mod.id, actor, 'revoked in OpenVibe.Network')
++ if (principal.status === 'revoked')
++ return this.revoke(view.mod.id, actor, 'revoked in OpenVibe.Network')
if (view.mod.status === 'revoked') return view
const approved = new Set(principal.approved)
let current = view
-- for (const cap of [...current.granted]) if (!approved.has(cap)) current = this.revokeGrant(view.mod.id, cap, actor)
++ for (const cap of [...current.granted])
++ if (!approved.has(cap)) current = this.revokeGrant(view.mod.id, cap, actor)
for (const cap of approved) {
if (current.granted.has(cap)) continue
try {
ddiff --git a/apps/server/src/mods/runtime.ts b/apps/server/src/mods/runtime.ts
iindex c67be76..54e3f9e 100644
---- a/apps/server/src/mods/runtime.ts
++++ b/apps/server/src/mods/runtime.ts
@@@ -99,7 +99,10 @@ export class ModRuntime {
/** Reconciles in a row over the CPU budget, per mod. */
private readonly cpuStrikes = new Map<string, number>()
/** Host effects per mod in the current minute, and minutes in a row that were throttled. */
-- private readonly effects = new Map<string, { windowStart: number; count: number; throttled: boolean; floodWindows: number }>()
++ private readonly effects = new Map<
++ string,
++ { windowStart: number; count: number; throttled: boolean; floodWindows: number }
++ >()
private readonly now: () => number
constructor(
@@@ -248,7 +251,8 @@ export class ModRuntime {
const nowMs = this.now()
let e = this.effects.get(id)
if (!e || nowMs - e.windowStart >= 60_000) {
-- const floodWindows = e && e.throttled && nowMs - e.windowStart < 120_000 ? e.floodWindows + 1 : 0
++ const floodWindows =
++ e && e.throttled && nowMs - e.windowStart < 120_000 ? e.floodWindows + 1 : 0
e = { windowStart: nowMs, count: 0, throttled: false, floodWindows }
this.effects.set(id, e)
if (floodWindows >= FLOOD_WINDOWS) {
@@@ -267,7 +271,11 @@ export class ModRuntime {
}
/** Disables the offending mod only; the others keep running. */
-- private enforce(id: string, reason: 'cpu' | 'event_flood', detail: Record<string, unknown>): void {
++ private enforce(
++ id: string,
++ reason: 'cpu' | 'event_flood',
++ detail: Record<string, unknown>,
++ ): void {
if (!this.registry.isActive(id)) return
this.registry.audit(id, 'budget_enforced', null, { reason, ...detail })
this.registry.disable(id, SYSTEM_ACTOR)
@@@ -306,7 +314,11 @@ export class ModRuntime {
const strikes = (this.cpuStrikes.get(view.mod.id) ?? 0) + 1
this.cpuStrikes.set(view.mod.id, strikes)
if (strikes >= CPU_STRIKES) {
-- this.enforce(view.mod.id, 'cpu', { cpu_ms: Math.round(ms * 1000) / 1000, budget_ms: budget, reconciles: strikes })
++ this.enforce(view.mod.id, 'cpu', {
++ cpu_ms: Math.round(ms * 1000) / 1000,
++ budget_ms: budget,
++ reconciles: strikes,
++ })
return
}
const nowMs = this.now()
ddiff --git a/apps/server/src/mods/security.test.ts b/apps/server/src/mods/security.test.ts
iindex 0adfd26..c106bfd 100644
---- a/apps/server/src/mods/security.test.ts
++++ b/apps/server/src/mods/security.test.ts
@@@ -4,7 +4,13 @@ import { createConsoleLogger } from '@openvibe/shared'
import { describe, expect, it } from 'vitest'
import { CAP_ANNOUNCE, CAP_PLACE_PROP, placeableByMod, validateContentPack } from './contentPack.js'
import { ModRegistry, type ModActor } from './registry.js'
--import { CPU_STRIKES, FLOOD_WINDOWS, ModRuntime, ModThrottledError, type ModHost } from './runtime.js'
++import {
++ CPU_STRIKES,
++ FLOOD_WINDOWS,
++ ModRuntime,
++ ModThrottledError,
++ type ModHost,
++} from './runtime.js'
import { sampleManifest } from './testFixtures.js'
/**
@@@ -17,7 +23,10 @@ import { sampleManifest } from './testFixtures.js'
* forged ownership every prop a mod places is owned by that mod, whatever the pack says; an unknown or
* another runtime's principal changes nothing here
*/
--const STAFF: ModActor = { audit: 'usr_01JABCDEFGHJKMNPQRSTVWXYZ0', subject: { type: 'user', id: 'usr_01JABCDEFGHJKMNPQRSTVWXYZ0' } }
++const STAFF: ModActor = {
++ audit: 'usr_01JABCDEFGHJKMNPQRSTVWXYZ0',
++ subject: { type: 'user', id: 'usr_01JABCDEFGHJKMNPQRSTVWXYZ0' },
++}
const content = createContent()
const log = createConsoleLogger({ app: 'test' }, 'error')
const A = 'mod_01JABCDEFGHJKMNPQRSTVWXYZA'
@@@ -51,13 +60,29 @@ function setup(effectsPerMinute = 60) {
const clock = { now: () => now, advance: (ms: number) => (now += ms) }
const store = openSqliteStore(':memory:')
const registry = new ModRegistry(store, content, undefined, clock.now)
-- const runtime = new ModRuntime(registry, store, content, log, { reconcileEveryTicks: 1, now: clock.now, effectsPerMinute })
++ const runtime = new ModRuntime(registry, store, content, log, {
++ reconcileEveryTicks: 1,
++ now: clock.now,
++ effectsPerMinute,
++ })
const install = (id: string, pack: unknown, approve: string[], cpuMs = 50) =>
-- registry.install({ manifest: sampleManifest({ id, resources: { cpuMs, memoryMb: 0, storageMb: 0 } }), pack, approve, enable: true }, STAFF)
++ registry.install(
++ {
++ manifest: sampleManifest({ id, resources: { cpuMs, memoryMb: 0, storageMb: 0 } }),
++ pack,
++ approve,
++ enable: true,
++ },
++ STAFF,
++ )
return { store, registry, runtime, clock, install }
}
--const prop = (key: string, pos: [number, number, number] = [10, 0, 10]) => ({ key, item: 'workbench', pos })
++const prop = (key: string, pos: [number, number, number] = [10, 0, 10]) => ({
++ key,
++ item: 'workbench',
++ pos,
++})
describe('mod security', () => {
it('namespace escape: a mod reaches only its own placements, through frozen bindings', () => {
@@@ -76,12 +101,18 @@ describe('mod security', () => {
expect(apiA.removeProp('bench')).toBe(true)
expect([...w.entities.values()].map((e) => e.owner)).toEqual([B])
expect(apiA.removeProp('bench')).toBe(false)
-- expect(registry.auditLog(B).some((a) => a.action === 'use' && JSON.stringify(a.detail ?? {}).includes('removed'))).toBe(false)
++ expect(
++ registry
++ .auditLog(B)
++ .some((a) => a.action === 'use' && JSON.stringify(a.detail ?? {}).includes('removed')),
++ ).toBe(false)
// Only inert items: anything with health, storage, a shop, a machine or a vehicle part is refused, at install
// and at the binding.
const loot = content.allItems().find((i) => placeableByMod(content, i.id) !== null)
expect(loot).toBeDefined()
-- expect(validateContentPack({ props: [{ key: 'x', item: loot!.id, pos: [0, 0, 0] }] }, content).ok).toBe(false)
++ expect(
++ validateContentPack({ props: [{ key: 'x', item: loot!.id, pos: [0, 0, 0] }] }, content).ok,
++ ).toBe(false)
expect(() => apiA.placeProp('x', loot!.id, [0, 0, 0])).toThrow(/not allowed/)
})
@@@ -117,7 +148,13 @@ describe('mod security', () => {
}
}
expect(registry.get(A)?.mod.status).toBe('disabled')
-- expect(registry.auditLog(A).some((a) => a.action === 'budget_enforced' && JSON.stringify(a.detail).includes('event_flood'))).toBe(true)
++ expect(
++ registry
++ .auditLog(A)
++ .some(
++ (a) => a.action === 'budget_enforced' && JSON.stringify(a.detail).includes('event_flood'),
++ ),
++ ).toBe(true)
expect(registry.get(B)?.mod.status).toBe('enabled')
expect(() => apiA.announce('after')).toThrow()
})
@@@ -141,20 +178,35 @@ describe('mod security', () => {
expect([...slow.entities.values()].filter((e) => e.owner === A)).toEqual([])
})
-- it('forged ownership: props are the mod\'s whatever the pack says; unknown or foreign principals change nothing', () => {
++ it("forged ownership: props are the mod's whatever the pack says; unknown or foreign principals change nothing", () => {
const { registry, runtime, install } = setup()
const w = world()
-- expect(validateContentPack({ props: [{ ...prop('x'), owner: 'usr_01JABCDEFGHJKMNPQRSTVWXYZ0' }] }, content).ok).toBe(false)
++ expect(
++ validateContentPack(
++ { props: [{ ...prop('x'), owner: 'usr_01JABCDEFGHJKMNPQRSTVWXYZ0' }] },
++ content,
++ ).ok,
++ ).toBe(false)
install(A, { props: [prop('bench')] }, [CAP_PLACE_PROP])
runtime.tick(1, w.host)
expect([...w.entities.values()]).toEqual([{ item: 'workbench', owner: A }])
// A principal Games does not have, or that another runtime owns, is not applied here.
-- expect(registry.applyNetwork({ mod_id: B, status: 'active', approved: [CAP_PLACE_PROP, CAP_ANNOUNCE] })).toBeNull()
++ expect(
++ registry.applyNetwork({
++ mod_id: B,
++ status: 'active',
++ approved: [CAP_PLACE_PROP, CAP_ANNOUNCE],
++ }),
++ ).toBeNull()
expect(registry.get(B)).toBeNull()
// A grant the manifest never requested cannot be applied, even from Network.
const manifestA = sampleManifest({ id: A, permissions: { capabilities: [CAP_PLACE_PROP] } })
expect(manifestA.permissions.capabilities).not.toContain('games.world.teleport')
-- registry.applyNetwork({ mod_id: A, status: 'active', approved: [CAP_PLACE_PROP, 'games.world.teleport'] })
++ registry.applyNetwork({
++ mod_id: A,
++ status: 'active',
++ approved: [CAP_PLACE_PROP, 'games.world.teleport'],
++ })
expect([...(registry.get(A)?.granted ?? [])]).toEqual([CAP_PLACE_PROP])
})
})
ddiff --git a/apps/server/src/platform/revocationEvents.test.ts b/apps/server/src/platform/revocationEvents.test.ts
iindex 68bfd87..ac962b5 100644
---- a/apps/server/src/platform/revocationEvents.test.ts
++++ b/apps/server/src/platform/revocationEvents.test.ts
@@@ -3,7 +3,12 @@ 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, MERGE_TOPIC, TOPIC } from './revocationEvents.js'
++import {
++ createRevocationEvents,
++ ensureRevocationSubscription,
++ MERGE_TOPIC,
++ TOPIC,
++} from './revocationEvents.js'
/**
* POST /internal/events for network.user.token_valid_after: signature v2 only, loopback only, the
@@@ -39,7 +44,8 @@ const events = createRevocationEvents({
},
onAccountEvent: async (event) => {
accountCalls.push(String(event.event_type))
-- if (event.payload?.export_id === 'exp_01J8Z3Q4R5S6T7V8W9X0Y1Z2Z9') throw new Error('Network down')
++ if (event.payload?.export_id === 'exp_01J8Z3Q4R5S6T7V8W9X0Y1Z2Z9')
++ throw new Error('Network down')
return 'exported'
},
log,
@@@ -65,18 +71,33 @@ const envelope = (over: Record<string, unknown> = {}) => ({
event_type: TOPIC,
version: 1,
source: 'network',
-- payload: { subject: { type: 'user', id: SUBJECT }, valid_after: new Date(at).toISOString(), reason: 'signed_out_everywhere' },
++ 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 } })
++ 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(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')
@@@ -87,9 +108,23 @@ describe('revocation events', () => {
it('an account merge (ADR-029) moves characters through onMerged; foreign or malformed ones are ignored', async () => {
const FROM = 'usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2B4'
const merged = (over: Record<string, unknown> = {}) =>
-- envelope({ event_id: 'evt_01J8Z3Q4R5S6T7V8W9X0Y1Z2C5', event_type: MERGE_TOPIC, payload: { merge_id: 'mrg_01J8Z3Q4R5S6T7V8W9X0Y1Z2D6', from: FROM, into: SUBJECT, merged_at: new Date(at).toISOString(), initiated_by: 'person' }, ...over })
++ envelope({
++ event_id: 'evt_01J8Z3Q4R5S6T7V8W9X0Y1Z2C5',
++ event_type: MERGE_TOPIC,
++ payload: {
++ merge_id: 'mrg_01J8Z3Q4R5S6T7V8W9X0Y1Z2D6',
++ from: FROM,
++ into: SUBJECT,
++ merged_at: new Date(at).toISOString(),
++ initiated_by: 'person',
++ },
++ ...over,
++ })
expect((await post(merged({ source: 'live' }))).body.outcome).toBe('ignored:source')
-- expect((await post(merged({ payload: { merge_id: 'nope', from: FROM, into: SUBJECT } }))).body.outcome).toBe('ignored:payload')
++ expect(
++ (await post(merged({ payload: { merge_id: 'nope', from: FROM, into: SUBJECT } }))).body
++ .outcome,
++ ).toBe('ignored:payload')
expect(mergedCalls).toEqual([])
expect((await post(merged())).body.outcome).toBe('merged')
expect(mergedCalls).toEqual([[FROM, SUBJECT, 'mrg_01J8Z3Q4R5S6T7V8W9X0Y1Z2D6']])
@@@ -98,18 +133,49 @@ describe('revocation events', () => {
it('account export and deletion (ADR-033) go to onAccountEvent, answered once it resolved (500 when it failed)', async () => {
const exp = (id: string) =>
-- envelope({ event_id: 'evt_01J8Z3Q4R5S6T7V8W9X0Y1Z2E7', event_type: 'network.account.export_requested', payload: { export_id: id, subject: SUBJECT } })
-- expect(await post(exp('exp_01J8Z3Q4R5S6T7V8W9X0Y1Z2Y8'))).toMatchObject({ status: 200, body: { outcome: 'exported' } })
++ envelope({
++ event_id: 'evt_01J8Z3Q4R5S6T7V8W9X0Y1Z2E7',
++ event_type: 'network.account.export_requested',
++ payload: { export_id: id, subject: SUBJECT },
++ })
++ expect(await post(exp('exp_01J8Z3Q4R5S6T7V8W9X0Y1Z2Y8'))).toMatchObject({
++ status: 200,
++ body: { outcome: 'exported' },
++ })
expect((await post(exp('exp_01J8Z3Q4R5S6T7V8W9X0Y1Z2Z9'))).status).toBe(500)
-- expect(accountCalls).toEqual(['network.account.export_requested', 'network.account.export_requested'])
++ expect(accountCalls).toEqual([
++ 'network.account.export_requested',
++ 'network.account.export_requested',
++ ])
})
it("network.mod.grants_changed (ADR-013) reaches onModGrants for Games' own mods only", async () => {
const MOD = 'mod_01J8Z3Q4R5S6T7V8W9X0Y1Z2M1'
const changed = (over: Record<string, unknown> = {}) =>
-- envelope({ event_id: 'evt_01J8Z3Q4R5S6T7V8W9X0Y1Z2M2', event_type: 'network.mod.grants_changed', payload: { mod_id: MOD, owner: 'games', status: 'active', approved: ['games.prop.place'], revision: 2, change: { action: 'revoke', capability: 'games.world.announce' }, by: 'staff' }, ...over })
++ envelope({
++ event_id: 'evt_01J8Z3Q4R5S6T7V8W9X0Y1Z2M2',
++ event_type: 'network.mod.grants_changed',
++ payload: {
++ mod_id: MOD,
++ owner: 'games',
++ status: 'active',
++ approved: ['games.prop.place'],
++ revision: 2,
++ change: { action: 'revoke', capability: 'games.world.announce' },
++ by: 'staff',
++ },
++ ...over,
++ })
expect((await post(changed({ source: 'games' }))).body.outcome).toBe('ignored:source')
-- expect((await post(changed({ payload: { mod_id: MOD, owner: 'tools', status: 'active', approved: [], revision: 1 } }))).body.outcome).toBe('ignored:owner')
++ expect(
++ (
++ await post(
++ changed({
++ payload: { mod_id: MOD, owner: 'tools', status: 'active', approved: [], revision: 1 },
++ }),
++ )
++ ).body.outcome,
++ ).toBe('ignored:owner')
expect((await post(changed())).body.outcome).toBe('mod:enabled')
expect(modGrantCalls).toEqual([[MOD, 'active', 'games.prop.place']])
})
@@@ -129,13 +195,24 @@ describe('revocation events', () => {
}
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 }
++ 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 },
{ topic_pattern: MERGE_TOPIC, endpoint: opts.endpoint, secret: SECRET },
-- { topic_pattern: 'network.account.export_requested', endpoint: opts.endpoint, secret: SECRET },
++ {
++ topic_pattern: 'network.account.export_requested',
++ endpoint: opts.endpoint,
++ secret: SECRET,
++ },
{ topic_pattern: 'network.account.deleted', endpoint: opts.endpoint, secret: SECRET },
{ topic_pattern: 'network.mod.grants_changed', endpoint: opts.endpoint, secret: SECRET },
])
ddiff --git a/apps/server/src/platform/revocationEvents.ts b/apps/server/src/platform/revocationEvents.ts
iindex 6c53420..49f4908 100644
---- a/apps/server/src/platform/revocationEvents.ts
++++ b/apps/server/src/platform/revocationEvents.ts
@@@ -37,7 +37,16 @@ type ParseDelivery = (
event_id?: string
event_type?: string
source?: string
-- payload?: { subject?: { id?: string } | string; valid_after?: string; merge_id?: string; from?: string; into?: string; export_id?: string; deletion_id?: string; aliases?: unknown }
++ payload?: {
++ subject?: { id?: string } | string
++ valid_after?: string
++ merge_id?: string
++ from?: string
++ into?: string
++ export_id?: string
++ deletion_id?: string
++ aliases?: unknown
++ }
}
} | null
@@@ -49,7 +58,13 @@ const { parseDelivery } = req('openvibe-sdk/events') as { parseDelivery: ParseDe
export const TOPIC = 'network.user.token_valid_after'
export const MERGE_TOPIC = 'network.subject.merged'
export const MOD_GRANTS_TOPIC = 'network.mod.grants_changed'
--export const TOPICS = [TOPIC, MERGE_TOPIC, 'network.account.export_requested', 'network.account.deleted', MOD_GRANTS_TOPIC] as const
++export const TOPICS = [
++ TOPIC,
++ MERGE_TOPIC,
++ 'network.account.export_requested',
++ 'network.account.deleted',
++ MOD_GRANTS_TOPIC,
++] as const
const SUBJECT_RE = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/
const MERGE_RE = /^mrg_[0-9A-HJKMNP-TV-Z]{26}$/
const PATH = '/internal/events'
@@@ -67,11 +82,23 @@ export function createRevocationEvents(opts: {
secrets: string[]
onRevoked: (subjectId: string, validAfterMs: number) => number
/** An account merge: move `from`'s characters to `into` (returns what moved; `already` when applied before). */
-- onMerged?: (from: string, into: string, mergeId: string) => { moved: number; kept: number; already: boolean }
++ onMerged?: (
++ from: string,
++ into: string,
++ mergeId: string,
++ ) => { moved: number; kept: number; already: boolean }
/** Account export or deletion (ADR-033): resolves to the outcome; a rejection is answered 500 and redelivered. */
-- onAccountEvent?: (event: NonNullable<NonNullable<ReturnType<ParseDelivery>>['event']>) => Promise<string>
++ onAccountEvent?: (
++ event: NonNullable<NonNullable<ReturnType<ParseDelivery>>['event']>,
++ ) => Promise<string>
/** A mod principal changed in Network: make the install's grants match (returns the outcome). */
-- onModGrants?: (payload: { mod_id: string; owner: string; status: string; approved: string[]; revision: number }) => string
++ onModGrants?: (payload: {
++ mod_id: string
++ owner: string
++ status: string
++ approved: string[]
++ revision: number
++ }) => string
log: Logger
}): RevocationEvents {
const store = createRevocationStore(opts.db, { table: 'token_revocations' })
@@@ -87,7 +114,11 @@ export function createRevocationEvents(opts: {
send(res, 405, { error: 'POST only' })
return true
}
-- if (req.headers['x-forwarded-for'] || req.headers['x-real-ip'] || req.headers['cf-connecting-ip']) {
++ if (
++ req.headers['x-forwarded-for'] ||
++ req.headers['x-real-ip'] ||
++ req.headers['cf-connecting-ip']
++ ) {
send(res, 403, { error: 'internal route' })
return true
}
@@@ -116,7 +147,10 @@ export function createRevocationEvents(opts: {
}
stats.received++
const event = delivery.event
-- if (event.event_type === 'network.account.export_requested' || event.event_type === 'network.account.deleted') {
++ if (
++ event.event_type === 'network.account.export_requested' ||
++ event.event_type === 'network.account.deleted'
++ ) {
if (!opts.onAccountEvent) {
stats.ignored++
send(res, 200, { event_id: event.event_id ?? null, outcome: 'ignored:no_handler' })
@@@ -128,20 +162,41 @@ export function createRevocationEvents(opts: {
send(res, 200, { event_id: event.event_id ?? null, outcome })
},
(err: unknown) => {
-- opts.log.warn('account event failed (Events retries)', { type: event.event_type, error: String((err as Error)?.message ?? err) })
++ opts.log.warn('account event failed (Events retries)', {
++ type: event.event_type,
++ error: String((err as Error)?.message ?? err),
++ })
send(res, 500, { error: 'not applied' })
},
)
return
}
if (event.event_type === MOD_GRANTS_TOPIC) {
-- const p = (event.payload ?? {}) as { mod_id?: unknown; owner?: unknown; status?: unknown; approved?: unknown; revision?: unknown }
++ const p = (event.payload ?? {}) as {
++ mod_id?: unknown
++ owner?: unknown
++ status?: unknown
++ approved?: unknown
++ revision?: unknown
++ }
let outcome: string
if (event.source !== 'network') outcome = 'ignored:source'
else if (!opts.onModGrants) outcome = 'ignored:no_handler'
-- else if (typeof p.mod_id !== 'string' || !Array.isArray(p.approved) || typeof p.status !== 'string') outcome = 'ignored:payload'
++ else if (
++ typeof p.mod_id !== 'string' ||
++ !Array.isArray(p.approved) ||
++ typeof p.status !== 'string'
++ )
++ outcome = 'ignored:payload'
else if (p.owner !== 'games') outcome = 'ignored:owner'
-- else outcome = opts.onModGrants({ mod_id: p.mod_id, owner: p.owner, status: p.status, approved: p.approved.map(String), revision: Number(p.revision) || 0 })
++ else
++ outcome = opts.onModGrants({
++ mod_id: p.mod_id,
++ owner: p.owner,
++ status: p.status,
++ approved: p.approved.map(String),
++ revision: Number(p.revision) || 0,
++ })
if (outcome.startsWith('ignored')) stats.ignored++
send(res, 200, { event_id: event.event_id ?? null, outcome })
return
@@@ -151,11 +206,18 @@ export function createRevocationEvents(opts: {
let outcome = 'merged'
if (event.source !== 'network') outcome = 'ignored:source'
else if (!opts.onMerged) outcome = 'ignored:no_handler'
-- else if (!MERGE_RE.test(String(p.merge_id ?? '')) || !SUBJECT_RE.test(String(p.from ?? '')) || !SUBJECT_RE.test(String(p.into ?? '')) || p.from === p.into) outcome = 'ignored:payload'
++ else if (
++ !MERGE_RE.test(String(p.merge_id ?? '')) ||
++ !SUBJECT_RE.test(String(p.from ?? '')) ||
++ !SUBJECT_RE.test(String(p.into ?? '')) ||
++ p.from === p.into
++ )
++ outcome = 'ignored:payload'
else {
const r = opts.onMerged(String(p.from), String(p.into), String(p.merge_id))
outcome = r.already ? 'unchanged' : 'merged'
-- if (!r.already) opts.log.info('account merged: characters moved', { moved: r.moved, kept: r.kept })
++ if (!r.already)
++ opts.log.info('account merged: characters moved', { moved: r.moved, kept: r.kept })
}
if (outcome.startsWith('ignored')) stats.ignored++
send(res, 200, { event_id: event.event_id ?? null, outcome })
@@@ -192,14 +254,24 @@ export async function ensureRevocationSubscription(opts: {
log: Logger
}): Promise<'exists' | 'created'> {
const f = opts.fetchImpl ?? fetch
-- const token = await opts.tokens.getToken({ audience: 'openvibe.events', scope: 'events.subscription.manage' })
++ 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 body = (await list.json()) as {
++ subscriptions?: { id: string; topic_pattern: string; endpoint: string }[]
++ }
let created = 0
for (const topic of TOPICS) {
-- if ((body.subscriptions ?? []).some((s) => s.topic_pattern === topic && s.endpoint === opts.endpoint)) continue
++ if (
++ (body.subscriptions ?? []).some(
++ (s) => s.topic_pattern === topic && s.endpoint === opts.endpoint,
++ )
++ )
++ continue
const r = await f(`${opts.eventsUrl}/api/v1/subscriptions`, {
method: 'POST',
headers: { ...headers, 'content-type': 'application/json' },