@fieldnotes/sync-server 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/sync-hub.ts","../src/memory-hub-backend.ts","../src/hub-fanout.ts","../src/sync-plugin.ts","../src/resource-limits.ts","../src/create-sync-server.ts","../src/heartbeat.ts","../src/shutdown.ts"],"sourcesContent":["export { SyncHub } from './sync-hub';\nexport type { SyncHubOptions, Connection } from './sync-hub';\nexport { MemoryHubBackend } from './memory-hub-backend';\nexport type { HubBackend } from './hub-backend';\nexport { createSyncServer } from './create-sync-server';\nexport type { CreateSyncServerOptions } from './create-sync-server';\nexport { InMemoryHubFanout } from './hub-fanout';\nexport type { HubFanout } from './hub-fanout';\nexport type { AuthInfo, AuthResult, Authenticate } from './authenticate';\nexport type {\n Authorize,\n AuthorizeContext,\n AuthorizeLayer,\n AuthorizeLayerContext,\n OwnedElement,\n ReadContext,\n CanRead,\n} from './authorize';\nexport { startHeartbeat } from './heartbeat';\nexport type { Heartbeat, HeartbeatSocket, HeartbeatServer } from './heartbeat';\nexport type {\n ServerSyncPlugin,\n ApplyResult,\n ServerOpContext,\n ServerNext,\n PluginSnapshot,\n ServerExtensionRegistry,\n} from './sync-plugin';\n","import {\n createCurrentCapabilities,\n createLegacyCapabilities,\n parseEnvelope,\n isValidElement,\n isValidWireElement,\n isNewerLayerRecord,\n translateOpForPeer,\n type LayerRecord,\n type WireSyncElement,\n type WireSyncOp,\n type WireSyncEnvelope,\n type SyncCapabilities,\n type PluginSnapshot,\n} from '@fieldnotes/sync';\nimport { getDefaultElementRegistry } from '@fieldnotes/core';\nimport type { ElementRegistry } from '@fieldnotes/core';\nimport { MemoryHubBackend } from './memory-hub-backend';\nimport { InMemoryHubFanout, type HubFanout } from './hub-fanout';\nimport type { HubBackend } from './hub-backend';\nimport type { Authorize, AuthorizeLayer, CanRead, OwnedElement } from './authorize';\nimport { ServerPluginRegistry } from './sync-plugin';\nimport type { ApplyResult, ServerOpContext, ServerSyncPlugin } from './sync-plugin';\nimport {\n DEFAULT_MAX_JSON_DEPTH,\n DEFAULT_MAX_PRESENCE_LANES,\n DEFAULT_PRESENCE_THROTTLE_MS,\n hasJsonDepthAtMost,\n} from './resource-limits';\n\nexport interface Connection {\n id: string;\n room: string;\n userId?: string;\n role?: string;\n send(message: string): void;\n}\n\nexport interface SyncHubOptions {\n backend?: HubBackend;\n fanout?: HubFanout;\n instanceId?: string;\n authorize?: Authorize;\n authorizeLayer?: AuthorizeLayer;\n plugins?: readonly ServerSyncPlugin[];\n canRead?: CanRead;\n maxJsonDepth?: number;\n presenceThrottleMs?: number;\n maxPresenceLanes?: number;\n /** Registry used to translate extension elements for legacy peers. */\n elementRegistry?: ElementRegistry;\n}\n\nconst HUB_FROM = 'hub';\nfunction generateInstanceId(): string {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function')\n return crypto.randomUUID();\n return `i-${Math.random().toString(36).slice(2)}`;\n}\n\nfunction isFanoutOp(\n op: unknown,\n): op is Extract<WireSyncOp, { kind: 'upsert' | 'remove' | 'clear' }> {\n if (typeof op !== 'object' || op === null) return false;\n const o = op as { kind?: unknown; element?: unknown; id?: unknown };\n if (o.kind === 'upsert') return isValidWireElement(o.element);\n if (o.kind === 'remove') return typeof o.id === 'string';\n return o.kind === 'clear';\n}\n\ntype LayerOp = Extract<WireSyncOp, { kind: 'layer-upsert' | 'layer-remove' }>;\n\ninterface PresenceLane {\n lastSentAt: number | undefined;\n pending: { data: unknown; timer: ReturnType<typeof setTimeout> } | null;\n}\n\nconst FALLBACK_PRESENCE_LANE = '';\nconst MAX_PRESENCE_LANE_LENGTH = 64;\n\nfunction presenceLaneOf(data: unknown): string {\n if (typeof data !== 'object' || data === null) return FALLBACK_PRESENCE_LANE;\n const kind = (data as { kind?: unknown }).kind;\n if (typeof kind !== 'string' || kind.length === 0 || kind.length > MAX_PRESENCE_LANE_LENGTH) {\n return FALLBACK_PRESENCE_LANE;\n }\n return kind;\n}\n\nfunction isLayerOp(op: unknown): op is LayerOp {\n if (typeof op !== 'object' || op === null) return false;\n const k = (op as { kind?: unknown }).kind;\n // Shape is re-validated by parseEnvelope on the serial path; fanout payloads\n // come from a sibling hub that already validated them.\n return k === 'layer-upsert' || k === 'layer-remove';\n}\n\nfunction layerOpToRecord(op: LayerOp): LayerRecord {\n return op.kind === 'layer-upsert'\n ? { id: op.layer.id, version: op.version, editor: op.editor, definition: op.layer }\n : { id: op.id, version: op.version, editor: op.editor };\n}\n\nfunction layerRecordToOp(record: LayerRecord): LayerOp {\n return record.definition\n ? {\n kind: 'layer-upsert',\n layer: record.definition,\n version: record.version,\n editor: record.editor,\n }\n : { kind: 'layer-remove', id: record.id, version: record.version, editor: record.editor };\n}\n\n/**\n * Peers sharing a capability profile receive byte-identical frames, so a\n * relay encodes each (sender, op) once per profile instead of once per recipient.\n */\ntype EncodedFrames = Map<string, string | null>;\n\nfunction capabilityProfile(capabilities: SyncCapabilities): string {\n return JSON.stringify([capabilities.elementEnvelope, capabilities.extensionKinds]);\n}\n\nfunction isPresenceOp(\n op: unknown,\n): op is { kind: 'presence'; data: unknown } | { kind: 'presence-leave' } {\n if (typeof op !== 'object' || op === null) return false;\n const k = (op as { kind?: unknown }).kind;\n return k === 'presence' || k === 'presence-leave';\n}\n\nexport class SyncHub {\n private readonly backend: HubBackend;\n private readonly conns = new Map<string, Connection>();\n private readonly rooms = new Map<string, Set<string>>(); // room → connIds\n private readonly roomQueues = new Map<string, Promise<void>>(); // room → serial tail\n private readonly presenceConnections = new Set<string>();\n private readonly instanceId: string;\n private readonly fanout: HubFanout;\n private readonly fanoutUnsub: () => void;\n private readonly authorize?: Authorize;\n private readonly authorizeLayer?: AuthorizeLayer;\n private readonly pluginRegistry: ServerPluginRegistry;\n private readonly elementRegistry: ElementRegistry;\n private readonly peerCapabilities = new Map<string, SyncCapabilities>();\n private readonly canRead?: CanRead;\n private readonly memoryLayers = new Map<string, Map<string, LayerRecord>>();\n private readonly maxJsonDepth: number;\n private readonly presenceThrottleMs: number;\n private readonly maxPresenceLanes: number;\n /**\n * Presence throttle state keyed by connection, then by lane. A lane is the\n * payload's `kind` (a non-empty string of at most 64 chars) or the reserved\n * fallback lane `''`, so a rapid stream of one kind (awareness cursors) can\n * never replace a pending frame of another kind (a ping, a path `cleared`).\n * Within a lane the newest payload wins. The lane count per connection is\n * capped by `maxPresenceLanes`, counting the fallback lane, so a client\n * cannot mint timers by varying `kind`.\n */\n private readonly presenceLanes = new Map<string, Map<string, PresenceLane>>();\n\n constructor(options: SyncHubOptions = {}) {\n this.backend = options.backend ?? new MemoryHubBackend();\n this.pluginRegistry = new ServerPluginRegistry(options.plugins ?? []);\n this.elementRegistry = options.elementRegistry ?? getDefaultElementRegistry();\n this.instanceId = options.instanceId ?? generateInstanceId();\n this.fanout = options.fanout ?? new InMemoryHubFanout();\n this.authorize = options.authorize;\n this.authorizeLayer = options.authorizeLayer;\n this.canRead = options.canRead;\n this.maxJsonDepth = options.maxJsonDepth ?? DEFAULT_MAX_JSON_DEPTH;\n this.presenceThrottleMs = options.presenceThrottleMs ?? DEFAULT_PRESENCE_THROTTLE_MS;\n const maxPresenceLanes = options.maxPresenceLanes ?? DEFAULT_MAX_PRESENCE_LANES;\n if (!Number.isFinite(maxPresenceLanes) || maxPresenceLanes < 1) {\n throw new RangeError('maxPresenceLanes must be a finite number of at least 1');\n }\n this.maxPresenceLanes = Math.floor(maxPresenceLanes);\n this.fanoutUnsub = this.fanout.subscribe((payload) => this.onFanout(payload));\n }\n\n addConnection(conn: Connection): void {\n this.conns.set(conn.id, conn);\n let set = this.rooms.get(conn.room);\n if (!set) {\n set = new Set();\n this.rooms.set(conn.room, set);\n }\n set.add(conn.id);\n }\n\n removeConnection(connId: string): void {\n const conn = this.conns.get(connId);\n if (!conn) return;\n this.conns.delete(connId);\n this.peerCapabilities.delete(connId);\n const room = conn.room;\n const hadPresence = this.presenceConnections.delete(connId);\n this.clearPresenceLanes(connId);\n const members = this.rooms.get(room);\n if (members) {\n members.delete(connId);\n if (members.size === 0) {\n this.rooms.delete(room);\n this.roomQueues.delete(room);\n }\n }\n if (hadPresence) this.broadcastLeave(room, conn.id);\n }\n\n roomCount(): number {\n return this.rooms.size;\n }\n\n /**\n * Broadcasts ephemeral server-owned presence data to every connection in a room.\n * The returned count covers successful delivery on this hub instance only; configured fan-out\n * forwards the same event to other instances on a best-effort basis.\n */\n broadcastPresence<T>(room: string, data: T): number {\n const op = { kind: 'presence' as const, data };\n const sent = this.relayToRoom(room, undefined, JSON.stringify({ from: HUB_FROM, op }));\n this.safePublish(JSON.stringify({ o: this.instanceId, room, from: HUB_FROM, op }));\n return sent;\n }\n\n handleMessage(connId: string, message: string): Promise<void> {\n const conn = this.conns.get(connId);\n if (!conn) return Promise.resolve();\n if (!hasJsonDepthAtMost(message, this.maxJsonDepth)) return Promise.resolve();\n const env = parseEnvelope(message);\n if (!env) return Promise.resolve();\n if (env.op.kind === 'capabilities') {\n this.peerCapabilities.set(conn.id, env.op.capabilities);\n this.sendToConnection(conn, HUB_FROM, {\n kind: 'capabilities',\n capabilities: createCurrentCapabilities(this.pluginRegistry.extensionKinds),\n });\n return Promise.resolve();\n }\n if (env.op.kind === 'presence') {\n this.schedulePresence(conn, env.op.data); // off-queue, throttled independently\n return Promise.resolve();\n }\n const room = conn.room;\n // The per-room serial queue is the single total-order authority: ops apply in arrival order\n // (arrival-order LWW — no per-element seq; see D3 / TD-12). Different rooms run independently.\n const prev = this.roomQueues.get(room) ?? Promise.resolve();\n const operation = prev.then(() => this.process(conn, env));\n this.roomQueues.set(\n room,\n operation.catch(() => {\n // Recover only the internal tail so one failed message never wedges the room queue.\n // The caller still receives the operation rejection for observability.\n }),\n );\n return operation;\n }\n\n private async process(conn: Connection, env: WireSyncEnvelope): Promise<void> {\n let op = env.op;\n if (op.kind === 'upsert') {\n const element = this.normalizeElement(op.element);\n if (!element) return;\n op = { ...op, element };\n }\n if (op.kind === 'request-snapshot') {\n const all = this.normalizeElements(await this.backend.snapshot(conn.room));\n const elements = this.canRead ? all.filter((el) => this.mayRead(conn, el.audience)) : all;\n // Layer records are presentation-only and carry no element bytes, so no\n // audience filter applies; the field is omitted while a room has never\n // used layer sync, keeping snapshot frames byte-identical to before.\n const layers = await this.getLayerRecords(conn.room);\n const snapshotOp: Record<string, unknown> = {\n kind: 'snapshot',\n to: env.from,\n elements,\n };\n if (layers.length > 0) snapshotOp['layers'] = layers;\n const extensions: Record<string, PluginSnapshot> = {};\n for (const plugin of this.pluginRegistry.plugins) {\n let snapshot = await plugin.snapshot?.(conn.room, this.backend);\n if (!snapshot) continue;\n if (plugin.filterSnapshot) {\n snapshot =\n plugin.filterSnapshot(snapshot, { userId: conn.userId, role: conn.role }) ?? undefined;\n }\n if (!snapshot) continue;\n if (plugin.legacySnapshotKey) snapshotOp[plugin.legacySnapshotKey] = snapshot.data;\n else extensions[plugin.name] = snapshot;\n }\n if (Object.keys(extensions).length > 0) snapshotOp['extensions'] = extensions;\n this.sendToConnection(conn, HUB_FROM, snapshotOp as WireSyncOp);\n } else if (op.kind === 'layer-upsert' || op.kind === 'layer-remove') {\n await this.processLayerOp(conn, op);\n } else if (op.kind === 'extension') {\n const entry = this.pluginRegistry.extension(op.extensionKind);\n if (!entry || !entry.kind.codec.validate(op.payload)) return;\n await this.deliverPluginResult(conn, await entry.handler(op, this.pluginContext(conn)));\n } else if (this.pluginRegistry.ownerOf(op.kind)) {\n const owner = this.pluginRegistry.ownerOf(op.kind);\n if (!owner?.process) return;\n const result = await owner.process(op, this.pluginContext(conn), async () => ({\n accepted: null,\n corrections: [],\n }));\n await this.deliverPluginResult(conn, result);\n } else if (op.kind === 'upsert' || op.kind === 'remove' || op.kind === 'clear') {\n const id = op.kind === 'upsert' ? op.element.id : op.kind === 'remove' ? op.id : undefined;\n const needCurrent = (this.authorize || this.canRead) && id !== undefined;\n const storedCurrent = needCurrent ? await this.backend.get(conn.room, id) : undefined;\n const current = storedCurrent\n ? (this.normalizeElement(storedCurrent) ?? undefined)\n : undefined;\n\n let outboundOp: WireSyncOp = op;\n if (this.authorize) {\n const allowed = await this.authorize({\n userId: conn.userId,\n role: conn.role,\n room: conn.room,\n op,\n currentElement: current,\n });\n if (!allowed) {\n await this.sendCorrection(conn, env.from, op, current);\n return;\n }\n if (op.kind === 'upsert') {\n const ownerId = current?.ownerId ?? conn.userId;\n const stampedElement: OwnedElement = { ...op.element, ownerId };\n outboundOp = { kind: 'upsert', element: stampedElement };\n }\n }\n\n const prevExisted = current !== undefined;\n const prevAudience = current?.audience;\n const result = await this.runCorePlugins(conn, outboundOp);\n for (const correction of result.corrections) {\n this.sendToConnection(conn, HUB_FROM, correction);\n }\n const accepted = result.accepted;\n if (\n accepted &&\n (accepted.kind === 'upsert' || accepted.kind === 'remove' || accepted.kind === 'clear')\n ) {\n if (result.locality !== 'local') {\n await this.fanout.publish(\n JSON.stringify({\n o: this.instanceId,\n room: conn.room,\n from: conn.id,\n op: accepted,\n prev: prevAudience,\n existed: prevExisted,\n }),\n );\n }\n this.deliverToRoom(conn.room, conn.id, conn.id, accepted, prevAudience, prevExisted);\n }\n for (const broadcast of result.broadcast ?? []) {\n await this.publishPluginOp(conn, broadcast, result.locality);\n }\n }\n // 'snapshot' from a client → ignored\n }\n\n private pluginContext(conn: Connection): ServerOpContext {\n return {\n room: conn.room,\n connectionId: conn.id,\n userId: conn.userId,\n role: conn.role,\n backend: this.backend,\n backendPlugin: (key) => this.backend.getService?.(key),\n };\n }\n\n private async runCorePlugins(conn: Connection, op: WireSyncOp): Promise<ApplyResult> {\n const middleware = this.pluginRegistry.plugins.filter((plugin) => plugin.process);\n const context = this.pluginContext(conn);\n const dispatch = async (index: number, current: WireSyncOp): Promise<ApplyResult> => {\n const plugin = middleware[index];\n if (!plugin?.process) {\n await this.backend.apply(conn.room, current);\n return { accepted: current, corrections: [] };\n }\n let called = false;\n return plugin.process(current, context, async (nextOp, nextContext) => {\n if (called) throw new Error(`Server plugin \"${plugin.name}\" called next() more than once`);\n if (nextContext !== context) {\n throw new Error(`Server plugin \"${plugin.name}\" replaced the operation context`);\n }\n called = true;\n return dispatch(index + 1, nextOp);\n });\n };\n return dispatch(0, op);\n }\n\n private async deliverPluginResult(conn: Connection, result: ApplyResult): Promise<void> {\n for (const correction of result.corrections) {\n this.sendToConnection(conn, HUB_FROM, correction);\n }\n if (result.accepted) await this.publishPluginOp(conn, result.accepted, result.locality);\n for (const broadcast of result.broadcast ?? []) {\n await this.publishPluginOp(conn, broadcast, result.locality);\n }\n }\n\n private async publishPluginOp(\n conn: Connection,\n op: WireSyncOp,\n locality: ApplyResult['locality'],\n ): Promise<void> {\n if (locality !== 'local') {\n await this.fanout.publish(\n JSON.stringify({ o: this.instanceId, room: conn.room, from: conn.id, op }),\n );\n }\n this.relayOpToRoom(conn.room, conn.id, conn.id, op);\n }\n\n /**\n * Applies a layer-definition edit on the room's serial queue. Convergence is\n * last-writer-wins under the deterministic (version, editor) ordering — the\n * same rule every client applies — so arrival order never decides a race. A\n * stale or denied edit is answered with an authoritative correction to the\n * sender only.\n */\n private async processLayerOp(conn: Connection, op: LayerOp): Promise<void> {\n const record = layerOpToRecord(op);\n const current = await this.getLayerRecord(conn.room, record.id);\n if (this.authorizeLayer) {\n const allowed = await this.authorizeLayer({\n userId: conn.userId,\n role: conn.role,\n room: conn.room,\n op,\n currentRecord: current,\n });\n if (!allowed) {\n // Revert the sender to the room's record; a tombstone when there is\n // none, so the denied local edit disappears everywhere consistently.\n const correction = current ?? { id: record.id, version: record.version, editor: HUB_FROM };\n this.sendToConnection(conn, HUB_FROM, layerRecordToOp(correction));\n return;\n }\n }\n if (current && !isNewerLayerRecord(record, current)) {\n // Stale under (version, editor): converge the sender, do not broadcast.\n this.sendToConnection(conn, HUB_FROM, layerRecordToOp(current));\n return;\n }\n await this.applyLayerRecord(conn.room, record);\n await this.fanout.publish(\n JSON.stringify({ o: this.instanceId, room: conn.room, from: conn.id, op }),\n );\n this.relayOpToRoom(conn.room, conn.id, conn.id, op);\n }\n\n private layerBackend(): Required<\n Pick<HubBackend, 'layerRecords' | 'getLayerRecord' | 'applyLayerRecord'>\n > | null {\n const { layerRecords, getLayerRecord, applyLayerRecord } = this.backend;\n if (!layerRecords || !getLayerRecord || !applyLayerRecord) return null;\n return {\n layerRecords: layerRecords.bind(this.backend),\n getLayerRecord: getLayerRecord.bind(this.backend),\n applyLayerRecord: applyLayerRecord.bind(this.backend),\n };\n }\n\n private async getLayerRecords(room: string): Promise<LayerRecord[]> {\n const backend = this.layerBackend();\n if (backend) return backend.layerRecords(room);\n return [...(this.memoryLayers.get(room)?.values() ?? [])];\n }\n\n private async getLayerRecord(room: string, id: string): Promise<LayerRecord | undefined> {\n const backend = this.layerBackend();\n if (backend) return backend.getLayerRecord(room, id);\n return this.memoryLayers.get(room)?.get(id);\n }\n\n private async applyLayerRecord(room: string, record: LayerRecord): Promise<void> {\n const backend = this.layerBackend();\n if (backend) {\n await backend.applyLayerRecord(room, record);\n return;\n }\n let map = this.memoryLayers.get(room);\n if (!map) {\n map = new Map();\n this.memoryLayers.set(room, map);\n }\n map.set(record.id, record);\n }\n\n private mayRead(conn: Connection, audience: string | undefined): boolean {\n if (!this.canRead) return true;\n return this.canRead({ userId: conn.userId, role: conn.role, room: conn.room, audience });\n }\n\n private safePublish(payload: string): void {\n try {\n void Promise.resolve(this.fanout.publish(payload)).catch(() => {\n /* presence is ephemeral; a broken publisher must not create an unhandled rejection */\n });\n } catch {\n /* a broken fanout publisher must not break the un-queued presence relay */\n }\n }\n\n private relayToRoom(room: string, excludeId: string | undefined, message: string): number {\n const members = this.rooms.get(room);\n if (!members) return 0;\n let sent = 0;\n for (const cid of members) {\n if (cid === excludeId) continue;\n const conn = this.conns.get(cid);\n if (!conn) continue;\n try {\n conn.send(message);\n sent += 1;\n } catch {\n /* a throwing socket must not break the relay loop */\n }\n }\n return sent;\n }\n\n /**\n * Translates `op` for the peer and sends it. Returns false when the op is\n * lossy for this peer (no legacy encoding) or the socket throws; neither\n * may reject the room operation that produced it.\n */\n private sendToConnection(\n conn: Connection,\n from: string,\n op: WireSyncOp,\n encoded?: EncodedFrames,\n ): boolean {\n const capabilities = this.peerCapabilities.get(conn.id) ?? createLegacyCapabilities();\n const profile = encoded ? capabilityProfile(capabilities) : undefined;\n let message = profile === undefined ? undefined : encoded?.get(profile);\n if (message === undefined) {\n message = this.encodeForPeer(from, op, capabilities);\n if (profile !== undefined) encoded?.set(profile, message);\n }\n if (message === null) return false;\n try {\n conn.send(message);\n return true;\n } catch {\n return false;\n }\n }\n\n /** Returns the wire frame for `op` translated for `capabilities`, or null when lossy. */\n private encodeForPeer(\n from: string,\n op: WireSyncOp,\n capabilities: SyncCapabilities,\n ): string | null {\n try {\n const translated = translateOpForPeer(\n op,\n capabilities,\n this.elementRegistry,\n this.pluginRegistry.extensionDefinitions,\n );\n return JSON.stringify({ from, op: translated });\n } catch {\n return null;\n }\n }\n\n /**\n * Normalize registered legacy wire elements before authorization, storage,\n * and relay. A legacy type this hub has no adapter for is forwarded and\n * stored verbatim — the hub is a relay, and the peers decide whether they\n * understand it — so a hub deployed without domain adapters never erases\n * the room's existing elements. Only a malformed registered element is dropped.\n */\n private normalizeElement(element: WireSyncElement): OwnedElement | null {\n if (isValidElement(element)) return element;\n const adapter = this.elementRegistry.getAdapterByLegacyType(element.type);\n if (!adapter) return element;\n try {\n const raw = Object.fromEntries(Object.entries(element));\n const envelope = adapter.decodeLegacy(raw);\n if (!adapter.validateEnvelope(envelope)) return null;\n return {\n ...envelope,\n ...(typeof raw['audience'] === 'string' ? { audience: raw['audience'] } : {}),\n ...(typeof raw['ownerId'] === 'string' ? { ownerId: raw['ownerId'] } : {}),\n };\n } catch {\n return null;\n }\n }\n\n private normalizeElements(elements: readonly WireSyncElement[]): OwnedElement[] {\n const normalized: OwnedElement[] = [];\n for (const element of elements) {\n const runtime = this.normalizeElement(element);\n if (runtime) normalized.push(runtime);\n }\n return normalized;\n }\n\n private relayOpToRoom(\n room: string,\n excludeId: string | undefined,\n from: string,\n op: WireSyncOp,\n ): number {\n const members = this.rooms.get(room);\n if (!members) return 0;\n let sent = 0;\n const encoded: EncodedFrames = new Map();\n for (const connectionId of members) {\n if (connectionId === excludeId) continue;\n const conn = this.conns.get(connectionId);\n if (!conn) continue;\n // A lossy translation is skipped for this peer without blocking compatible peers.\n if (this.sendToConnection(conn, from, op, encoded)) sent += 1;\n }\n return sent;\n }\n\n private broadcastClientPresence(conn: Connection, data: unknown): void {\n this.presenceConnections.add(conn.id);\n const message = JSON.stringify({ from: conn.id, op: { kind: 'presence', data } });\n this.relayToRoom(conn.room, conn.id, message);\n this.safePublish(\n JSON.stringify({\n o: this.instanceId,\n room: conn.room,\n from: conn.id,\n op: { kind: 'presence', data },\n }),\n );\n }\n\n private clearPresenceLanes(connId: string): void {\n const lanes = this.presenceLanes.get(connId);\n if (!lanes) return;\n for (const lane of lanes.values()) {\n if (lane.pending) clearTimeout(lane.pending.timer);\n }\n this.presenceLanes.delete(connId);\n }\n\n private schedulePresence(conn: Connection, data: unknown): void {\n if (this.presenceThrottleMs <= 0) {\n this.broadcastClientPresence(conn, data);\n return;\n }\n const lane = this.presenceLaneFor(conn.id, presenceLaneOf(data));\n const now = Date.now();\n if (lane.lastSentAt === undefined || now - lane.lastSentAt >= this.presenceThrottleMs) {\n if (lane.pending) {\n clearTimeout(lane.pending.timer);\n lane.pending = null;\n }\n lane.lastSentAt = now;\n this.broadcastClientPresence(conn, data);\n return;\n }\n if (lane.pending) {\n lane.pending.data = data;\n return;\n }\n const timer = setTimeout(\n () => {\n const pending = lane.pending;\n lane.pending = null;\n if (!pending || !this.conns.has(conn.id)) return;\n lane.lastSentAt = Date.now();\n this.broadcastClientPresence(conn, pending.data);\n },\n this.presenceThrottleMs - (now - lane.lastSentAt),\n );\n lane.pending = { data, timer };\n }\n\n private presenceLaneFor(connId: string, requested: string): PresenceLane {\n let lanes = this.presenceLanes.get(connId);\n if (!lanes) {\n lanes = new Map();\n this.presenceLanes.set(connId, lanes);\n }\n let key = requested;\n if (key !== FALLBACK_PRESENCE_LANE && !lanes.has(key)) {\n const named = lanes.size - (lanes.has(FALLBACK_PRESENCE_LANE) ? 1 : 0);\n if (named >= this.maxPresenceLanes - 1) key = FALLBACK_PRESENCE_LANE;\n }\n let lane = lanes.get(key);\n if (!lane) {\n lane = { lastSentAt: undefined, pending: null };\n lanes.set(key, lane);\n }\n return lane;\n }\n\n private broadcastLeave(room: string, from: string): void {\n const message = JSON.stringify({ from, op: { kind: 'presence-leave' } });\n this.relayToRoom(room, undefined, message);\n this.safePublish(\n JSON.stringify({ o: this.instanceId, room, from, op: { kind: 'presence-leave' } }),\n );\n }\n\n private deliverToRoom(\n room: string,\n excludeId: string | undefined,\n from: string,\n op: WireSyncOp,\n prevAudience: string | undefined,\n prevExisted: boolean,\n ): void {\n const members = this.rooms.get(room);\n if (!members) return;\n const encoded: EncodedFrames = new Map();\n if (op.kind === 'upsert') {\n const audience = (op.element as OwnedElement).audience;\n const removeOp: WireSyncOp = { kind: 'remove', id: op.element.id };\n const encodedRemove: EncodedFrames = new Map();\n for (const cid of members) {\n if (cid === excludeId) continue;\n const conn = this.conns.get(cid);\n if (!conn) continue;\n if (this.mayRead(conn, audience)) this.sendToConnection(conn, from, op, encoded);\n else if (prevExisted && this.mayRead(conn, prevAudience)) {\n this.sendToConnection(conn, HUB_FROM, removeOp, encodedRemove);\n }\n }\n } else if (op.kind === 'remove') {\n for (const cid of members) {\n if (cid === excludeId) continue;\n const conn = this.conns.get(cid);\n if (!conn) continue;\n // No read filter → forward to all (today's behavior; current/prevExisted aren't fetched without\n // a hook). With canRead, only recipients who could see the removed element get it.\n const wasVisible = !this.canRead || (prevExisted && this.mayRead(conn, prevAudience));\n if (wasVisible) this.sendToConnection(conn, from, op, encoded);\n }\n } else if (op.kind === 'clear') {\n for (const cid of members) {\n if (cid === excludeId) continue;\n const conn = this.conns.get(cid);\n if (conn) this.sendToConnection(conn, from, op, encoded);\n }\n }\n }\n\n private async sendCorrection(\n conn: Connection,\n from: string,\n op: WireSyncOp,\n current: OwnedElement | undefined,\n ): Promise<void> {\n let correction: WireSyncOp | undefined;\n if (op.kind === 'upsert') {\n correction = current\n ? this.mayRead(conn, current.audience)\n ? { kind: 'upsert', element: current }\n : { kind: 'remove', id: current.id }\n : { kind: 'remove', id: op.element.id };\n } else if (op.kind === 'remove') {\n correction = current\n ? this.mayRead(conn, current.audience)\n ? { kind: 'upsert', element: current }\n : { kind: 'remove', id: current.id }\n : undefined;\n } else if (op.kind === 'clear') {\n const all = this.normalizeElements(await this.backend.snapshot(conn.room));\n const elements = this.canRead ? all.filter((el) => this.mayRead(conn, el.audience)) : all;\n correction = { kind: 'snapshot', to: from, elements };\n }\n if (correction) this.sendToConnection(conn, HUB_FROM, correction);\n }\n\n private onFanout(payload: string): void {\n // Off the serial queue on purpose: forward-only (the origin already applied to the SHARED backend),\n // and delivery is already ordered. Re-filter per local member (canRead runs on EVERY instance).\n let env: {\n o?: unknown;\n room?: unknown;\n from?: unknown;\n op?: unknown;\n prev?: unknown;\n existed?: unknown;\n };\n try {\n env = JSON.parse(payload);\n } catch {\n return;\n }\n if (typeof env.o !== 'string' || typeof env.room !== 'string' || typeof env.from !== 'string')\n return;\n if (env.o === this.instanceId) return; // our own publish — already delivered locally\n const op = env.op;\n if (isPresenceOp(op)) {\n // presence/leave: raw forward to all local members (the sender lives on the origin instance),\n // no backend, no canRead filter.\n this.relayToRoom(env.room, undefined, JSON.stringify({ from: env.from, op }));\n return;\n }\n if (isLayerOp(op)) {\n void this.applyFanoutLayerOp(env.room, op).catch(() => {\n /* a broken backend must not break the fanout relay */\n });\n this.relayOpToRoom(env.room, undefined, env.from, op);\n return;\n }\n const plugin =\n typeof op === 'object' && op !== null && (op as { kind?: unknown }).kind === 'extension'\n ? this.pluginRegistry.extension((op as { extensionKind?: string }).extensionKind ?? '')\n ?.plugin\n : typeof op === 'object' &&\n op !== null &&\n typeof (op as { kind?: unknown }).kind === 'string'\n ? this.pluginRegistry.ownerOf((op as { kind: string }).kind)\n : undefined;\n if (plugin && typeof op === 'object' && op !== null) {\n const previous = this.roomQueues.get(env.room) ?? Promise.resolve();\n const operation = previous.then(async () => {\n const context: ServerOpContext = {\n room: env.room as string,\n connectionId: env.from as string,\n backend: this.backend,\n backendPlugin: (key) => this.backend.getService?.(key),\n };\n const accepted = plugin.applyFanout\n ? await plugin.applyFanout(op as WireSyncOp, context)\n : (op as WireSyncOp);\n if (accepted) {\n this.relayOpToRoom(env.room as string, undefined, env.from as string, accepted);\n }\n });\n this.roomQueues.set(\n env.room,\n operation.catch(() => {\n /* a broken backend must not wedge the room queue */\n }),\n );\n return;\n }\n if (!isFanoutOp(op)) return;\n const runtimeOp =\n op.kind === 'upsert'\n ? (() => {\n const element = this.normalizeElement(op.element);\n return element ? ({ ...op, element } satisfies WireSyncOp) : null;\n })()\n : op;\n if (!runtimeOp) return;\n const prevAudience = typeof env.prev === 'string' ? env.prev : undefined;\n const prevExisted = env.existed === true;\n this.deliverToRoom(env.room, undefined, env.from, runtimeOp, prevAudience, prevExisted);\n }\n\n private async applyFanoutLayerOp(room: string, op: LayerOp): Promise<void> {\n const record = layerOpToRecord(op);\n const current = await this.getLayerRecord(room, record.id);\n if (current && !isNewerLayerRecord(record, current)) return;\n await this.applyLayerRecord(room, record);\n }\n\n close(): void {\n for (const connId of [...this.presenceLanes.keys()]) this.clearPresenceLanes(connId);\n this.fanoutUnsub();\n }\n}\n","import {\n applyOpToMap,\n type LayerRecord,\n type WireSyncElement,\n type WireSyncOp,\n} from '@fieldnotes/sync';\nimport type { HubBackend } from './hub-backend';\n\nexport class MemoryHubBackend implements HubBackend {\n private rooms = new Map<string, Map<string, WireSyncElement>>();\n private roomLayers = new Map<string, Map<string, LayerRecord>>();\n\n private room(id: string): Map<string, WireSyncElement> {\n let r = this.rooms.get(id);\n if (!r) {\n r = new Map();\n this.rooms.set(id, r);\n }\n return r;\n }\n\n private layers(room: string): Map<string, LayerRecord> {\n let r = this.roomLayers.get(room);\n if (!r) {\n r = new Map();\n this.roomLayers.set(room, r);\n }\n return r;\n }\n\n async snapshot(room: string): Promise<WireSyncElement[]> {\n return [...this.room(room).values()];\n }\n\n async get(room: string, id: string): Promise<WireSyncElement | undefined> {\n return this.room(room).get(id);\n }\n\n async apply(room: string, op: WireSyncOp): Promise<void> {\n if (op.kind === 'clear') {\n // Clears elements only; layer records are a separate, longer-lived ledger.\n this.rooms.delete(room);\n return;\n }\n applyOpToMap(this.room(room), op);\n }\n\n async layerRecords(room: string): Promise<LayerRecord[]> {\n return [...this.layers(room).values()];\n }\n\n async getLayerRecord(room: string, id: string): Promise<LayerRecord | undefined> {\n return this.layers(room).get(id);\n }\n\n async applyLayerRecord(room: string, record: LayerRecord): Promise<void> {\n this.layers(room).set(record.id, record);\n }\n}\n","export interface HubFanout {\n publish(payload: string): void | Promise<void>;\n subscribe(handler: (payload: string) => void): () => void;\n close?(): void;\n}\n\nexport class InMemoryHubFanout implements HubFanout {\n private readonly handlers = new Set<(payload: string) => void>();\n\n publish(payload: string): void {\n for (const h of this.handlers) {\n try {\n h(payload);\n } catch {\n /* one throwing subscriber must not break the publish loop */\n }\n }\n }\n\n subscribe(handler: (payload: string) => void): () => void {\n this.handlers.add(handler);\n return () => this.handlers.delete(handler);\n }\n}\n","import type { ServiceKey } from '@fieldnotes/core';\nimport type { ExtensionKind, PluginSnapshot, TypedExtensionOp, WireSyncOp } from '@fieldnotes/sync';\nimport type { HubBackend } from './hub-backend';\n\nexport interface ApplyResult {\n readonly accepted: WireSyncOp | null;\n readonly corrections: WireSyncOp[];\n readonly broadcast?: WireSyncOp[];\n readonly locality?: 'shared' | 'local';\n}\n\nexport interface ServerOpContext {\n readonly room: string;\n readonly connectionId: string;\n readonly userId?: string;\n readonly role?: string;\n readonly backend: HubBackend;\n backendPlugin<T>(key: ServiceKey<T>): T | undefined;\n}\n\nexport type ServerNext = (op: WireSyncOp, context: ServerOpContext) => Promise<ApplyResult>;\n\nexport interface ServerExtensionRegistry {\n register<TPayload>(\n kind: ExtensionKind<TPayload>,\n handler: (op: TypedExtensionOp<TPayload>, context: ServerOpContext) => Promise<ApplyResult>,\n ): void;\n}\n\nexport interface ServerSyncPlugin {\n readonly name: string;\n readonly ownedLegacyKinds?: readonly string[];\n readonly legacySnapshotKey?: string;\n process?(op: WireSyncOp, context: ServerOpContext, next: ServerNext): Promise<ApplyResult>;\n applyFanout?(op: WireSyncOp, context: ServerOpContext): Promise<WireSyncOp | null>;\n registerExtensionKinds?(registry: ServerExtensionRegistry): void;\n snapshot?(room: string, backend: HubBackend): Promise<PluginSnapshot | undefined>;\n filterSnapshot?(\n snapshot: PluginSnapshot,\n viewer: { userId?: string; role?: string },\n ): PluginSnapshot | null;\n}\n\ninterface ServerExtensionEntry {\n readonly kind: ExtensionKind<unknown>;\n readonly plugin: ServerSyncPlugin;\n readonly handler: (\n op: TypedExtensionOp<unknown>,\n context: ServerOpContext,\n ) => Promise<ApplyResult>;\n}\n\nexport class ServerPluginRegistry {\n private readonly byName = new Map<string, ServerSyncPlugin>();\n private readonly legacyOwners = new Map<string, ServerSyncPlugin>();\n private readonly extensions = new Map<string, ServerExtensionEntry>();\n private readonly definitions: ReadonlyMap<string, ExtensionKind<unknown>>;\n\n constructor(plugins: readonly ServerSyncPlugin[]) {\n for (const plugin of plugins) this.register(plugin);\n // Registration is constructor-only, so the translation view is fixed here\n // instead of rebuilt on every relayed frame.\n this.definitions = new Map([...this.extensions].map(([name, entry]) => [name, entry.kind]));\n }\n\n private register(plugin: ServerSyncPlugin): void {\n if (this.byName.has(plugin.name))\n throw new Error(`Server plugin \"${plugin.name}\" is duplicated`);\n this.byName.set(plugin.name, plugin);\n for (const kind of plugin.ownedLegacyKinds ?? []) {\n if (this.legacyOwners.has(kind)) throw new Error(`Sync op kind \"${kind}\" has two owners`);\n this.legacyOwners.set(kind, plugin);\n }\n plugin.registerExtensionKinds?.({\n register: <TPayload>(\n kind: ExtensionKind<TPayload>,\n handler: (op: TypedExtensionOp<TPayload>, context: ServerOpContext) => Promise<ApplyResult>,\n ) => {\n if (this.extensions.has(kind.extensionKind)) {\n throw new Error(`Extension kind \"${kind.extensionKind}\" has two owners`);\n }\n this.extensions.set(kind.extensionKind, {\n kind: kind as ExtensionKind<unknown>,\n plugin,\n handler: handler as ServerExtensionEntry['handler'],\n });\n },\n });\n }\n\n get plugins(): readonly ServerSyncPlugin[] {\n return [...this.byName.values()];\n }\n\n ownerOf(kind: string): ServerSyncPlugin | undefined {\n return this.legacyOwners.get(kind);\n }\n\n extension(extensionKind: string): ServerExtensionEntry | undefined {\n return this.extensions.get(extensionKind);\n }\n\n get extensionKinds(): readonly string[] {\n return [...this.extensions.keys()];\n }\n\n get extensionDefinitions(): ReadonlyMap<string, ExtensionKind<unknown>> {\n return this.definitions;\n }\n}\n\nexport type { PluginSnapshot } from '@fieldnotes/sync';\n","export const DEFAULT_MAX_MESSAGE_BYTES = 1024 * 1024;\nexport const DEFAULT_MAX_JSON_DEPTH = 32;\nexport const DEFAULT_MAX_PENDING_AUTH_MESSAGES = 100;\nexport const DEFAULT_MAX_PENDING_AUTH_BYTES = 2 * 1024 * 1024;\nexport const DEFAULT_MESSAGES_PER_SECOND = 120;\nexport const DEFAULT_MESSAGE_BURST = 240;\nexport const DEFAULT_PRESENCE_THROTTLE_MS = 50;\n/** Per-connection presence throttle lanes, INCLUDING the reserved fallback lane. */\nexport const DEFAULT_MAX_PRESENCE_LANES = 16;\n\nexport function hasJsonDepthAtMost(message: string, maxDepth: number): boolean {\n let depth = 0;\n let inString = false;\n let escaped = false;\n\n for (const char of message) {\n if (inString) {\n if (escaped) escaped = false;\n else if (char === '\\\\') escaped = true;\n else if (char === '\"') inString = false;\n continue;\n }\n if (char === '\"') inString = true;\n else if (char === '{' || char === '[') {\n depth += 1;\n if (depth > maxDepth) return false;\n } else if (char === '}' || char === ']') {\n depth -= 1;\n if (depth < 0) return false;\n }\n }\n\n return depth === 0 && !inString;\n}\n\nexport class MessageRateLimiter {\n private tokens: number;\n private updatedAt: number;\n\n constructor(\n private readonly ratePerSecond: number,\n private readonly burst: number,\n now = Date.now(),\n ) {\n this.tokens = burst;\n this.updatedAt = now;\n }\n\n take(now = Date.now()): boolean {\n const elapsedSeconds = Math.max(0, now - this.updatedAt) / 1000;\n this.tokens = Math.min(this.burst, this.tokens + elapsedSeconds * this.ratePerSecond);\n this.updatedAt = now;\n if (this.tokens < 1) return false;\n this.tokens -= 1;\n return true;\n }\n}\n","import { WebSocketServer, type RawData } from 'ws';\nimport type { Server } from 'http';\nimport { SyncHub } from './sync-hub';\nimport type { HubBackend } from './hub-backend';\nimport type { HubFanout } from './hub-fanout';\nimport type { Authenticate } from './authenticate';\nimport type { Authorize, AuthorizeLayer, CanRead } from './authorize';\nimport type { ServerSyncPlugin } from './sync-plugin';\nimport type { ElementRegistry } from '@fieldnotes/core';\nimport { startHeartbeat } from './heartbeat';\nimport {\n DEFAULT_MAX_JSON_DEPTH,\n DEFAULT_MAX_MESSAGE_BYTES,\n DEFAULT_MAX_PENDING_AUTH_BYTES,\n DEFAULT_MAX_PENDING_AUTH_MESSAGES,\n DEFAULT_MESSAGE_BURST,\n DEFAULT_MESSAGES_PER_SECOND,\n DEFAULT_PRESENCE_THROTTLE_MS,\n MessageRateLimiter,\n} from './resource-limits';\nimport { DEFAULT_SHUTDOWN_GRACE_MS, drainWebSocketServer } from './shutdown';\n\nexport interface CreateSyncServerOptions {\n port?: number;\n server?: Server;\n backend?: HubBackend;\n fanout?: HubFanout;\n instanceId?: string;\n authenticate?: Authenticate;\n authorize?: Authorize;\n authorizeLayer?: AuthorizeLayer;\n plugins?: readonly ServerSyncPlugin[];\n canRead?: CanRead;\n heartbeatIntervalMs?: number;\n maxMessageBytes?: number;\n maxJsonDepth?: number;\n maxPendingAuthMessages?: number;\n maxPendingAuthBytes?: number;\n messagesPerSecond?: number;\n messageBurst?: number;\n presenceThrottleMs?: number;\n maxPresenceLanes?: number;\n shutdownGraceMs?: number;\n /**\n * Registry used to translate extension envelopes for legacy peers. Without\n * it the hub relays unknown legacy element types verbatim and cannot encode\n * envelopes for pre-envelope clients.\n */\n elementRegistry?: ElementRegistry;\n}\n\nfunction rawDataByteLength(data: RawData): number {\n if (Array.isArray(data)) return data.reduce((total, chunk) => total + chunk.byteLength, 0);\n return data.byteLength;\n}\n\nexport function createSyncServer(options: CreateSyncServerOptions = {}): {\n hub: SyncHub;\n wss: WebSocketServer;\n close: () => Promise<void>;\n} {\n const shutdownGraceMs = options.shutdownGraceMs ?? DEFAULT_SHUTDOWN_GRACE_MS;\n if (!Number.isFinite(shutdownGraceMs) || shutdownGraceMs < 0) {\n throw new RangeError('shutdownGraceMs must be a non-negative finite number');\n }\n const hub = new SyncHub({\n backend: options.backend,\n fanout: options.fanout,\n instanceId: options.instanceId,\n authorize: options.authorize,\n authorizeLayer: options.authorizeLayer,\n plugins: options.plugins,\n canRead: options.canRead,\n maxJsonDepth: options.maxJsonDepth ?? DEFAULT_MAX_JSON_DEPTH,\n presenceThrottleMs: options.presenceThrottleMs ?? DEFAULT_PRESENCE_THROTTLE_MS,\n maxPresenceLanes: options.maxPresenceLanes,\n elementRegistry: options.elementRegistry,\n });\n const maxMessageBytes = options.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES;\n const wss = options.server\n ? new WebSocketServer({ server: options.server, maxPayload: maxMessageBytes })\n : new WebSocketServer({ port: options.port ?? 0, maxPayload: maxMessageBytes });\n const heartbeat = startHeartbeat(wss, options.heartbeatIntervalMs ?? 30000);\n let shuttingDown = false;\n let closePromise: Promise<void> | undefined;\n let counter = 0;\n wss.on('connection', (ws, req) => {\n if (shuttingDown) {\n ws.close(1001, 'server shutting down');\n return;\n }\n heartbeat.track(ws);\n // Protocol/parser failures (including maxPayload) close the peer. Consuming the socket-level\n // error keeps hostile input from becoming an uncaught process error.\n ws.on('error', () => undefined);\n const url = new URL(req.url ?? '', 'http://localhost');\n const room = url.searchParams.get('room');\n if (!room) {\n ws.close(4400, 'room required');\n return;\n }\n const connId = `c${++counter}-${Math.random().toString(36).slice(2, 8)}`;\n\n let state: 'pending' | 'ready' | 'rejected' = 'pending';\n let closed = false;\n let admitted = false;\n const queue: string[] = [];\n let queuedBytes = 0;\n const maxPendingAuthMessages =\n options.maxPendingAuthMessages ?? DEFAULT_MAX_PENDING_AUTH_MESSAGES;\n const maxPendingAuthBytes = options.maxPendingAuthBytes ?? DEFAULT_MAX_PENDING_AUTH_BYTES;\n const limiter = new MessageRateLimiter(\n options.messagesPerSecond ?? DEFAULT_MESSAGES_PER_SECOND,\n options.messageBurst ?? DEFAULT_MESSAGE_BURST,\n );\n\n const send = (m: string) => {\n try {\n ws.send(m);\n } catch {\n /* socket closed mid-send */\n }\n };\n\n ws.on('message', (data) => {\n if (state === 'rejected') return;\n const messageBytes = rawDataByteLength(data);\n if (messageBytes > maxMessageBytes) {\n state = 'rejected';\n ws.close(1009, 'message too large');\n return;\n }\n if (!limiter.take()) {\n state = 'rejected';\n ws.close(4408, 'rate limit exceeded');\n return;\n }\n const msg = String(data);\n if (state === 'pending') {\n if (\n queue.length >= maxPendingAuthMessages ||\n queuedBytes + messageBytes > maxPendingAuthBytes\n ) {\n state = 'rejected';\n ws.close(4408, 'authentication queue limit exceeded');\n return;\n }\n queue.push(msg);\n queuedBytes += messageBytes;\n return;\n }\n void hub.handleMessage(connId, msg).catch((err) => console.error('[sync-server]', err));\n });\n ws.on('close', () => {\n closed = true;\n if (admitted) hub.removeConnection(connId);\n });\n\n Promise.resolve(options.authenticate ? options.authenticate({ req, room }) : { userId: connId })\n .then((result) => {\n if (closed || state === 'rejected' || shuttingDown) return;\n if (!result) {\n state = 'rejected';\n ws.close(4401, 'unauthorized');\n return;\n }\n state = 'ready';\n admitted = true;\n hub.addConnection({ id: connId, room, userId: result.userId, role: result.role, send });\n for (const m of queue) {\n void hub.handleMessage(connId, m).catch((err) => console.error('[sync-server]', err));\n }\n queue.length = 0;\n queuedBytes = 0;\n })\n .catch(() => {\n if (!closed && !shuttingDown) {\n state = 'rejected';\n ws.close(4401, 'unauthorized');\n }\n });\n });\n const close = (): Promise<void> => {\n if (closePromise) return closePromise;\n shuttingDown = true;\n closePromise = drainWebSocketServer(wss, shutdownGraceMs).then(() => {\n heartbeat.stop();\n hub.close();\n });\n return closePromise;\n };\n\n return {\n hub,\n wss,\n close,\n };\n}\n","export interface HeartbeatSocket {\n ping(): void;\n terminate(): void;\n on(event: 'pong', listener: () => void): void;\n}\nexport interface HeartbeatServer {\n clients: Set<HeartbeatSocket>;\n}\nexport interface Heartbeat {\n track(ws: HeartbeatSocket): void;\n stop(): void;\n}\n\nexport function startHeartbeat(wss: HeartbeatServer, intervalMs: number): Heartbeat {\n if (intervalMs <= 0) {\n return {\n track: () => {\n /* disabled */\n },\n stop: () => {\n /* disabled */\n },\n };\n }\n const alive = new WeakMap<HeartbeatSocket, boolean>();\n const track = (ws: HeartbeatSocket): void => {\n alive.set(ws, true);\n ws.on('pong', () => alive.set(ws, true));\n };\n const interval = setInterval(() => {\n for (const ws of wss.clients) {\n try {\n if (alive.get(ws) === false) {\n ws.terminate();\n continue;\n }\n alive.set(ws, false);\n ws.ping();\n } catch {\n /* socket dying mid-tick — skip it */\n }\n }\n }, intervalMs);\n return { track, stop: () => clearInterval(interval) };\n}\n","export const DEFAULT_SHUTDOWN_GRACE_MS = 5000;\n\nexport interface ShutdownSocket {\n close(code?: number, reason?: string): void;\n terminate(): void;\n}\n\nexport interface ShutdownServer {\n clients: Set<ShutdownSocket>;\n close(callback: (error?: Error) => void): void;\n}\n\n/** Stop admission, ask connected peers to leave, then forcefully reap stragglers. */\nexport function drainWebSocketServer(wss: ShutdownServer, graceMs: number): Promise<void> {\n return new Promise((resolve) => {\n let settled = false;\n const finish = (): void => {\n if (settled) return;\n settled = true;\n clearTimeout(forceTimer);\n resolve();\n };\n const forceTimer = setTimeout(() => {\n for (const ws of wss.clients) {\n try {\n ws.terminate();\n } catch {\n /* socket already failed while shutdown was in progress */\n }\n }\n finish();\n }, graceMs);\n\n // Calling close first prevents new connections. Its callback runs once all clients have left.\n wss.close(() => finish());\n for (const ws of wss.clients) {\n try {\n ws.close(1001, 'server shutting down');\n } catch {\n try {\n ws.terminate();\n } catch {\n /* socket already closed */\n }\n }\n }\n });\n}\n"],"mappings":"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","names":["import_sync"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/sync-hub.ts","../src/memory-hub-backend.ts","../src/hub-fanout.ts","../src/sync-plugin.ts","../src/resource-limits.ts","../src/create-sync-server.ts","../src/authenticate.ts","../src/heartbeat.ts","../src/shutdown.ts","../src/room-name.ts"],"sourcesContent":["export { SyncHub } from './sync-hub';\nexport type { SyncHubOptions, Connection } from './sync-hub';\nexport { MemoryHubBackend } from './memory-hub-backend';\nexport type { HubBackend } from './hub-backend';\nexport { createSyncServer } from './create-sync-server';\nexport { ROOM_NAME_PATTERN, isValidRoomName } from './room-name';\nexport type { CreateSyncServerOptions } from './create-sync-server';\nexport { InMemoryHubFanout } from './hub-fanout';\nexport type { HubFanout } from './hub-fanout';\nexport type { AuthInfo, AuthResult, Authenticate } from './authenticate';\nexport { readBearerToken } from './authenticate';\nexport type {\n Authorize,\n AuthorizeContext,\n AuthorizeLayer,\n AuthorizeLayerContext,\n OwnedElement,\n ReadContext,\n CanRead,\n OwnerReadContext,\n CanReadOwnerId,\n ResolveAudienceContext,\n ResolveAudience,\n} from './authorize';\nexport { startHeartbeat } from './heartbeat';\nexport type { Heartbeat, HeartbeatSocket, HeartbeatServer } from './heartbeat';\nexport type {\n ServerSyncPlugin,\n ApplyResult,\n ServerOpContext,\n ServerNext,\n PluginSnapshot,\n ServerExtensionRegistry,\n} from './sync-plugin';\n","import {\n createCurrentCapabilities,\n createLegacyCapabilities,\n parseEnvelope,\n isValidEnvelope,\n isValidElement,\n isNewerLayerRecord,\n translateOpForPeer,\n type LayerRecord,\n type WireSyncElement,\n type WireSyncOp,\n type WireSyncEnvelope,\n type SyncCapabilities,\n type PluginSnapshot,\n} from '@fieldnotes/sync';\nimport { getDefaultElementRegistry } from '@fieldnotes/core';\nimport type { ElementRegistry } from '@fieldnotes/core';\nimport { MemoryHubBackend } from './memory-hub-backend';\nimport { InMemoryHubFanout, type HubFanout } from './hub-fanout';\nimport type { HubBackend } from './hub-backend';\nimport type {\n Authorize,\n AuthorizeLayer,\n CanRead,\n CanReadOwnerId,\n OwnedElement,\n ResolveAudience,\n} from './authorize';\nimport { ServerPluginRegistry } from './sync-plugin';\nimport type { ApplyResult, ServerOpContext, ServerSyncPlugin } from './sync-plugin';\nimport {\n DEFAULT_MAX_JSON_DEPTH,\n DEFAULT_MAX_PRESENCE_BYTES,\n DEFAULT_MAX_PRESENCE_LANES,\n DEFAULT_PRESENCE_THROTTLE_MS,\n hasJsonDepthAtMost,\n} from './resource-limits';\n\nexport interface Connection {\n id: string;\n room: string;\n userId?: string;\n role?: string;\n send(message: string): void;\n}\n\nexport interface SyncHubOptions {\n backend?: HubBackend;\n fanout?: HubFanout;\n instanceId?: string;\n authorize?: Authorize;\n authorizeLayer?: AuthorizeLayer;\n plugins?: readonly ServerSyncPlugin[];\n canRead?: CanRead;\n canReadOwnerId?: CanReadOwnerId;\n resolveAudience?: ResolveAudience;\n maxJsonDepth?: number;\n presenceThrottleMs?: number;\n maxPresenceLanes?: number;\n /** Largest `presence.data` payload relayed, in UTF-8 bytes of its JSON encoding. */\n maxPresenceBytes?: number;\n /** Registry used to translate extension elements for legacy peers. */\n elementRegistry?: ElementRegistry;\n}\n\nconst HUB_FROM = 'hub';\nconst utf8 = new TextEncoder();\nfunction utf8ByteLength(text: string): number {\n return utf8.encode(text).byteLength;\n}\nfunction generateInstanceId(): string {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function')\n return crypto.randomUUID();\n return `i-${Math.random().toString(36).slice(2)}`;\n}\n\nfunction isFanoutOp(\n op: WireSyncOp,\n): op is Extract<WireSyncOp, { kind: 'upsert' | 'remove' | 'clear' }> {\n return op.kind === 'upsert' || op.kind === 'remove' || op.kind === 'clear';\n}\n\ntype LayerOp = Extract<WireSyncOp, { kind: 'layer-upsert' | 'layer-remove' }>;\n\ninterface PresenceLane {\n lastSentAt: number | undefined;\n pending: { data: unknown; timer: ReturnType<typeof setTimeout> } | null;\n}\n\nconst FALLBACK_PRESENCE_LANE = '';\nconst MAX_PRESENCE_LANE_LENGTH = 64;\n\nfunction presenceLaneOf(data: unknown): string {\n if (typeof data !== 'object' || data === null) return FALLBACK_PRESENCE_LANE;\n const kind = (data as { kind?: unknown }).kind;\n if (typeof kind !== 'string' || kind.length === 0 || kind.length > MAX_PRESENCE_LANE_LENGTH) {\n return FALLBACK_PRESENCE_LANE;\n }\n return kind;\n}\n\nfunction isLayerOp(op: WireSyncOp): op is LayerOp {\n return op.kind === 'layer-upsert' || op.kind === 'layer-remove';\n}\n\nfunction layerOpToRecord(op: LayerOp): LayerRecord {\n return op.kind === 'layer-upsert'\n ? { id: op.layer.id, version: op.version, editor: op.editor, definition: op.layer }\n : { id: op.id, version: op.version, editor: op.editor };\n}\n\nfunction layerRecordToOp(record: LayerRecord): LayerOp {\n return record.definition\n ? {\n kind: 'layer-upsert',\n layer: record.definition,\n version: record.version,\n editor: record.editor,\n }\n : { kind: 'layer-remove', id: record.id, version: record.version, editor: record.editor };\n}\n\n/**\n * Peers sharing an encoding profile (capabilities plus ownerId privilege)\n * receive byte-identical frames, so a relay encodes each (sender, op) once\n * per profile instead of once per recipient.\n */\ntype EncodedFrames = Map<string, string | null>;\n\nfunction encodingProfile(capabilities: SyncCapabilities, revealOwner: boolean): string {\n return JSON.stringify([capabilities.elementEnvelope, capabilities.extensionKinds, revealOwner]);\n}\n\nfunction withoutOwnerId(element: WireSyncElement): WireSyncElement {\n if (element.ownerId === undefined) return element;\n const rest: WireSyncElement = { ...element };\n delete rest.ownerId;\n return rest;\n}\n\n/** Removes the server-stamped `ownerId` from every element an op carries. */\nfunction stripOwnerId(op: WireSyncOp): WireSyncOp {\n if (op.kind === 'upsert') return { ...op, element: withoutOwnerId(op.element) };\n if (op.kind === 'snapshot') return { ...op, elements: op.elements.map(withoutOwnerId) };\n return op;\n}\n\nfunction isPresenceOp(\n op: WireSyncOp,\n): op is Extract<WireSyncOp, { kind: 'presence' | 'presence-leave' }> {\n return op.kind === 'presence' || op.kind === 'presence-leave';\n}\n\nexport class SyncHub {\n private readonly backend: HubBackend;\n private readonly conns = new Map<string, Connection>();\n private readonly rooms = new Map<string, Set<string>>(); // room → connIds\n private readonly roomQueues = new Map<string, Promise<void>>(); // room → serial tail\n private readonly presenceConnections = new Set<string>();\n private readonly instanceId: string;\n private readonly fanout: HubFanout;\n private readonly fanoutUnsub: () => void;\n private readonly authorize?: Authorize;\n private readonly authorizeLayer?: AuthorizeLayer;\n private readonly pluginRegistry: ServerPluginRegistry;\n private readonly elementRegistry: ElementRegistry;\n private readonly peerCapabilities = new Map<string, SyncCapabilities>();\n private readonly canRead?: CanRead;\n private readonly canReadOwnerId?: CanReadOwnerId;\n private readonly resolveAudience?: ResolveAudience;\n private readonly memoryLayers = new Map<string, Map<string, LayerRecord>>();\n private readonly maxJsonDepth: number;\n private readonly presenceThrottleMs: number;\n private readonly maxPresenceLanes: number;\n private readonly maxPresenceBytes: number;\n /**\n * Presence throttle state keyed by connection, then by lane. A lane is the\n * payload's `kind` (a non-empty string of at most 64 chars) or the reserved\n * fallback lane `''`, so a rapid stream of one kind (awareness cursors) can\n * never replace a pending frame of another kind (a ping, a path `cleared`).\n * Within a lane the newest payload wins. The lane count per connection is\n * capped by `maxPresenceLanes`, counting the fallback lane, so a client\n * cannot mint timers by varying `kind`.\n */\n private readonly presenceLanes = new Map<string, Map<string, PresenceLane>>();\n\n constructor(options: SyncHubOptions = {}) {\n this.backend = options.backend ?? new MemoryHubBackend();\n this.pluginRegistry = new ServerPluginRegistry(options.plugins ?? []);\n this.elementRegistry = options.elementRegistry ?? getDefaultElementRegistry();\n this.instanceId = options.instanceId ?? generateInstanceId();\n this.fanout = options.fanout ?? new InMemoryHubFanout();\n this.authorize = options.authorize;\n this.authorizeLayer = options.authorizeLayer;\n this.canRead = options.canRead;\n this.canReadOwnerId = options.canReadOwnerId;\n this.resolveAudience = options.resolveAudience;\n this.maxJsonDepth = options.maxJsonDepth ?? DEFAULT_MAX_JSON_DEPTH;\n this.presenceThrottleMs = options.presenceThrottleMs ?? DEFAULT_PRESENCE_THROTTLE_MS;\n const maxPresenceLanes = options.maxPresenceLanes ?? DEFAULT_MAX_PRESENCE_LANES;\n if (!Number.isFinite(maxPresenceLanes) || maxPresenceLanes < 1) {\n throw new RangeError('maxPresenceLanes must be a finite number of at least 1');\n }\n this.maxPresenceLanes = Math.floor(maxPresenceLanes);\n const maxPresenceBytes = options.maxPresenceBytes ?? DEFAULT_MAX_PRESENCE_BYTES;\n if (!Number.isFinite(maxPresenceBytes) || maxPresenceBytes < 0) {\n throw new RangeError('maxPresenceBytes must be a non-negative finite number');\n }\n this.maxPresenceBytes = maxPresenceBytes;\n this.fanoutUnsub = this.fanout.subscribe((payload) => this.onFanout(payload));\n }\n\n addConnection(conn: Connection): void {\n this.conns.set(conn.id, conn);\n let set = this.rooms.get(conn.room);\n if (!set) {\n set = new Set();\n this.rooms.set(conn.room, set);\n }\n set.add(conn.id);\n }\n\n removeConnection(connId: string): void {\n const conn = this.conns.get(connId);\n if (!conn) return;\n this.conns.delete(connId);\n this.peerCapabilities.delete(connId);\n const room = conn.room;\n const hadPresence = this.presenceConnections.delete(connId);\n this.clearPresenceLanes(connId);\n const members = this.rooms.get(room);\n if (members) {\n members.delete(connId);\n if (members.size === 0) {\n this.rooms.delete(room);\n this.roomQueues.delete(room);\n }\n }\n if (hadPresence) this.broadcastLeave(room, conn.id);\n }\n\n roomCount(): number {\n return this.rooms.size;\n }\n\n /**\n * Broadcasts ephemeral server-owned presence data to every connection in a room.\n * The returned count covers successful delivery on this hub instance only; configured fan-out\n * forwards the same event to other instances on a best-effort basis.\n */\n broadcastPresence<T>(room: string, data: T): number {\n if (!this.isPresenceWithinLimit(data)) return 0;\n const op = { kind: 'presence' as const, data };\n const sent = this.relayToRoom(room, undefined, JSON.stringify({ from: HUB_FROM, op }));\n this.safePublish(JSON.stringify({ o: this.instanceId, room, from: HUB_FROM, op }));\n return sent;\n }\n\n handleMessage(connId: string, message: string): Promise<void> {\n const conn = this.conns.get(connId);\n if (!conn) return Promise.resolve();\n if (!hasJsonDepthAtMost(message, this.maxJsonDepth)) return Promise.resolve();\n const env = parseEnvelope(message);\n if (!env) return Promise.resolve();\n if (env.op.kind === 'capabilities') {\n this.peerCapabilities.set(conn.id, env.op.capabilities);\n this.sendToConnection(conn, HUB_FROM, {\n kind: 'capabilities',\n capabilities: createCurrentCapabilities(this.pluginRegistry.extensionKinds),\n });\n return Promise.resolve();\n }\n if (env.op.kind === 'presence') {\n // Presence is relayed to every member verbatim, so its size is the amplification factor.\n if (!this.isPresenceWithinLimit(env.op.data)) return Promise.resolve();\n this.schedulePresence(conn, env.op.data); // off-queue, throttled independently\n return Promise.resolve();\n }\n const room = conn.room;\n // The per-room serial queue is the single total-order authority: ops apply in arrival order\n // (arrival-order LWW — no per-element seq; see D3 / TD-12). Different rooms run independently.\n const prev = this.roomQueues.get(room) ?? Promise.resolve();\n const operation = prev.then(() => this.process(conn, env));\n this.roomQueues.set(\n room,\n operation.catch(() => {\n // Recover only the internal tail so one failed message never wedges the room queue.\n // The caller still receives the operation rejection for observability.\n }),\n );\n return operation;\n }\n\n private async process(conn: Connection, env: WireSyncEnvelope): Promise<void> {\n let op = env.op;\n if (op.kind === 'upsert') {\n const element = this.normalizeElement(op.element);\n if (!element) return;\n op = { ...op, element };\n }\n if (op.kind === 'request-snapshot') {\n const all = this.normalizeElements(await this.backend.snapshot(conn.room));\n const elements = this.canRead ? all.filter((el) => this.mayRead(conn, el.audience)) : all;\n // Layer records are presentation-only and carry no element bytes, so no\n // audience filter applies; the field is omitted while a room has never\n // used layer sync, keeping snapshot frames byte-identical to before.\n const layers = await this.getLayerRecords(conn.room);\n const snapshotOp: Record<string, unknown> = {\n kind: 'snapshot',\n to: env.from,\n elements,\n };\n if (layers.length > 0) snapshotOp['layers'] = layers;\n const extensions: Record<string, PluginSnapshot> = {};\n for (const plugin of this.pluginRegistry.plugins) {\n let snapshot = await plugin.snapshot?.(conn.room, this.backend);\n if (!snapshot) continue;\n if (plugin.filterSnapshot) {\n snapshot =\n plugin.filterSnapshot(snapshot, { userId: conn.userId, role: conn.role }) ?? undefined;\n }\n if (!snapshot) continue;\n if (plugin.legacySnapshotKey) snapshotOp[plugin.legacySnapshotKey] = snapshot.data;\n else extensions[plugin.name] = snapshot;\n }\n if (Object.keys(extensions).length > 0) snapshotOp['extensions'] = extensions;\n this.sendToConnection(conn, HUB_FROM, snapshotOp as WireSyncOp);\n } else if (op.kind === 'layer-upsert' || op.kind === 'layer-remove') {\n await this.processLayerOp(conn, op);\n } else if (op.kind === 'extension') {\n const entry = this.pluginRegistry.extension(op.extensionKind);\n if (!entry || !entry.kind.codec.validate(op.payload)) return;\n await this.deliverPluginResult(conn, await entry.handler(op, this.pluginContext(conn)));\n } else if (this.pluginRegistry.ownerOf(op.kind)) {\n const owner = this.pluginRegistry.ownerOf(op.kind);\n if (!owner?.process) return;\n const result = await owner.process(op, this.pluginContext(conn), async () => ({\n accepted: null,\n corrections: [],\n }));\n await this.deliverPluginResult(conn, result);\n } else if (op.kind === 'upsert' || op.kind === 'remove' || op.kind === 'clear') {\n const id = op.kind === 'upsert' ? op.element.id : op.kind === 'remove' ? op.id : undefined;\n const needCurrent =\n (this.authorize || this.canRead || this.resolveAudience) && id !== undefined;\n const storedCurrent = needCurrent ? await this.backend.get(conn.room, id) : undefined;\n const current = storedCurrent\n ? (this.normalizeElement(storedCurrent) ?? undefined)\n : undefined;\n\n if (op.kind === 'upsert' && this.resolveAudience) {\n // The hub, not the client, decides the audience; it runs before authorize so a\n // policy sees the authoritative tag.\n const audience = this.resolveAudience({\n userId: conn.userId,\n role: conn.role,\n room: conn.room,\n element: op.element,\n currentElement: current,\n });\n const element: OwnedElement = { ...op.element };\n if (audience === undefined) delete element.audience;\n else element.audience = audience;\n op = { kind: 'upsert', element };\n }\n\n let outboundOp: WireSyncOp = op;\n if (this.authorize) {\n const allowed = await this.authorize({\n userId: conn.userId,\n role: conn.role,\n room: conn.room,\n op,\n currentElement: current,\n });\n if (!allowed) {\n await this.sendCorrection(conn, env.from, op, current);\n return;\n }\n if (op.kind === 'upsert') {\n const ownerId = current?.ownerId ?? conn.userId;\n const stampedElement: OwnedElement = { ...op.element, ownerId };\n outboundOp = { kind: 'upsert', element: stampedElement };\n }\n }\n\n const prevExisted = current !== undefined;\n const prevAudience = current?.audience;\n const result = await this.runCorePlugins(conn, outboundOp);\n for (const correction of result.corrections) {\n this.sendToConnection(conn, HUB_FROM, correction);\n }\n const accepted = result.accepted;\n if (\n accepted &&\n (accepted.kind === 'upsert' || accepted.kind === 'remove' || accepted.kind === 'clear')\n ) {\n if (result.locality !== 'local') {\n await this.fanout.publish(\n JSON.stringify({\n o: this.instanceId,\n room: conn.room,\n from: conn.id,\n op: accepted,\n prev: prevAudience,\n existed: prevExisted,\n }),\n );\n }\n this.deliverToRoom(conn.room, conn.id, conn.id, accepted, prevAudience, prevExisted);\n }\n for (const broadcast of result.broadcast ?? []) {\n await this.publishPluginOp(conn, broadcast, result.locality);\n }\n }\n // 'snapshot' from a client → ignored\n }\n\n private pluginContext(conn: Connection): ServerOpContext {\n return {\n room: conn.room,\n connectionId: conn.id,\n userId: conn.userId,\n role: conn.role,\n backend: this.backend,\n backendPlugin: (key) => this.backend.getService?.(key),\n };\n }\n\n private async runCorePlugins(conn: Connection, op: WireSyncOp): Promise<ApplyResult> {\n const middleware = this.pluginRegistry.plugins.filter((plugin) => plugin.process);\n const context = this.pluginContext(conn);\n const dispatch = async (index: number, current: WireSyncOp): Promise<ApplyResult> => {\n const plugin = middleware[index];\n if (!plugin?.process) {\n await this.backend.apply(conn.room, current);\n return { accepted: current, corrections: [] };\n }\n let called = false;\n return plugin.process(current, context, async (nextOp, nextContext) => {\n if (called) throw new Error(`Server plugin \"${plugin.name}\" called next() more than once`);\n if (nextContext !== context) {\n throw new Error(`Server plugin \"${plugin.name}\" replaced the operation context`);\n }\n called = true;\n return dispatch(index + 1, nextOp);\n });\n };\n return dispatch(0, op);\n }\n\n private async deliverPluginResult(conn: Connection, result: ApplyResult): Promise<void> {\n for (const correction of result.corrections) {\n this.sendToConnection(conn, HUB_FROM, correction);\n }\n if (result.accepted) await this.publishPluginOp(conn, result.accepted, result.locality);\n for (const broadcast of result.broadcast ?? []) {\n await this.publishPluginOp(conn, broadcast, result.locality);\n }\n }\n\n private async publishPluginOp(\n conn: Connection,\n op: WireSyncOp,\n locality: ApplyResult['locality'],\n ): Promise<void> {\n if (locality !== 'local') {\n await this.fanout.publish(\n JSON.stringify({ o: this.instanceId, room: conn.room, from: conn.id, op }),\n );\n }\n this.relayOpToRoom(conn.room, conn.id, conn.id, op);\n }\n\n /**\n * Applies a layer-definition edit on the room's serial queue. Convergence is\n * last-writer-wins under the deterministic (version, editor) ordering — the\n * same rule every client applies — so arrival order never decides a race. A\n * stale or denied edit is answered with an authoritative correction to the\n * sender only.\n */\n private async processLayerOp(conn: Connection, op: LayerOp): Promise<void> {\n const record = layerOpToRecord(op);\n const current = await this.getLayerRecord(conn.room, record.id);\n if (this.authorizeLayer) {\n const allowed = await this.authorizeLayer({\n userId: conn.userId,\n role: conn.role,\n room: conn.room,\n op,\n currentRecord: current,\n });\n if (!allowed) {\n // Revert the sender to the room's record; a tombstone when there is\n // none, so the denied local edit disappears everywhere consistently.\n const correction = current ?? { id: record.id, version: record.version, editor: HUB_FROM };\n this.sendToConnection(conn, HUB_FROM, layerRecordToOp(correction));\n return;\n }\n }\n if (current && !isNewerLayerRecord(record, current)) {\n // Stale under (version, editor): converge the sender, do not broadcast.\n this.sendToConnection(conn, HUB_FROM, layerRecordToOp(current));\n return;\n }\n await this.applyLayerRecord(conn.room, record);\n await this.fanout.publish(\n JSON.stringify({ o: this.instanceId, room: conn.room, from: conn.id, op }),\n );\n this.relayOpToRoom(conn.room, conn.id, conn.id, op);\n }\n\n private layerBackend(): Required<\n Pick<HubBackend, 'layerRecords' | 'getLayerRecord' | 'applyLayerRecord'>\n > | null {\n const { layerRecords, getLayerRecord, applyLayerRecord } = this.backend;\n if (!layerRecords || !getLayerRecord || !applyLayerRecord) return null;\n return {\n layerRecords: layerRecords.bind(this.backend),\n getLayerRecord: getLayerRecord.bind(this.backend),\n applyLayerRecord: applyLayerRecord.bind(this.backend),\n };\n }\n\n private async getLayerRecords(room: string): Promise<LayerRecord[]> {\n const backend = this.layerBackend();\n if (backend) return backend.layerRecords(room);\n return [...(this.memoryLayers.get(room)?.values() ?? [])];\n }\n\n private async getLayerRecord(room: string, id: string): Promise<LayerRecord | undefined> {\n const backend = this.layerBackend();\n if (backend) return backend.getLayerRecord(room, id);\n return this.memoryLayers.get(room)?.get(id);\n }\n\n private async applyLayerRecord(room: string, record: LayerRecord): Promise<void> {\n const backend = this.layerBackend();\n if (backend) {\n await backend.applyLayerRecord(room, record);\n return;\n }\n let map = this.memoryLayers.get(room);\n if (!map) {\n map = new Map();\n this.memoryLayers.set(room, map);\n }\n map.set(record.id, record);\n }\n\n private mayReadOwnerId(conn: Connection): boolean {\n if (!this.canReadOwnerId) return false;\n return this.canReadOwnerId({ userId: conn.userId, role: conn.role, room: conn.room });\n }\n\n private mayRead(conn: Connection, audience: string | undefined): boolean {\n if (!this.canRead) return true;\n return this.canRead({ userId: conn.userId, role: conn.role, room: conn.room, audience });\n }\n\n private safePublish(payload: string): void {\n try {\n void Promise.resolve(this.fanout.publish(payload)).catch(() => {\n /* presence is ephemeral; a broken publisher must not create an unhandled rejection */\n });\n } catch {\n /* a broken fanout publisher must not break the un-queued presence relay */\n }\n }\n\n private relayToRoom(room: string, excludeId: string | undefined, message: string): number {\n const members = this.rooms.get(room);\n if (!members) return 0;\n let sent = 0;\n for (const cid of members) {\n if (cid === excludeId) continue;\n const conn = this.conns.get(cid);\n if (!conn) continue;\n try {\n conn.send(message);\n sent += 1;\n } catch {\n /* a throwing socket must not break the relay loop */\n }\n }\n return sent;\n }\n\n /**\n * Translates `op` for the peer and sends it. Returns false when the op is\n * lossy for this peer (no legacy encoding) or the socket throws; neither\n * may reject the room operation that produced it.\n */\n private sendToConnection(\n conn: Connection,\n from: string,\n op: WireSyncOp,\n encoded?: EncodedFrames,\n ): boolean {\n const capabilities = this.peerCapabilities.get(conn.id) ?? createLegacyCapabilities();\n const revealOwner = this.mayReadOwnerId(conn);\n const profile = encoded ? encodingProfile(capabilities, revealOwner) : undefined;\n let message = profile === undefined ? undefined : encoded?.get(profile);\n if (message === undefined) {\n message = this.encodeForPeer(from, op, capabilities, revealOwner);\n if (profile !== undefined) encoded?.set(profile, message);\n }\n if (message === null) return false;\n try {\n conn.send(message);\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * Returns the wire frame for `op` translated for `capabilities`, or null\n * when lossy. The server-stamped `ownerId` is a server-side authorization\n * fact: it leaves the hub only for peers `canReadOwnerId` admits.\n */\n private encodeForPeer(\n from: string,\n op: WireSyncOp,\n capabilities: SyncCapabilities,\n revealOwner: boolean,\n ): string | null {\n try {\n const translated = translateOpForPeer(\n revealOwner ? op : stripOwnerId(op),\n capabilities,\n this.elementRegistry,\n this.pluginRegistry.extensionDefinitions,\n );\n return JSON.stringify({ from, op: translated });\n } catch {\n return null;\n }\n }\n\n /**\n * Normalize registered legacy wire elements before authorization, storage,\n * and relay. A legacy type this hub has no adapter for is forwarded and\n * stored verbatim — the hub is a relay, and the peers decide whether they\n * understand it — so a hub deployed without domain adapters never erases\n * the room's existing elements. Only a malformed registered element is dropped.\n */\n private normalizeElement(element: WireSyncElement): OwnedElement | null {\n if (isValidElement(element)) return element;\n const adapter = this.elementRegistry.getAdapterByLegacyType(element.type);\n if (!adapter) return element;\n try {\n const raw = Object.fromEntries(Object.entries(element));\n const envelope = adapter.decodeLegacy(raw);\n if (!adapter.validateEnvelope(envelope)) return null;\n return {\n ...envelope,\n ...(typeof raw['audience'] === 'string' ? { audience: raw['audience'] } : {}),\n ...(typeof raw['ownerId'] === 'string' ? { ownerId: raw['ownerId'] } : {}),\n };\n } catch {\n return null;\n }\n }\n\n private normalizeElements(elements: readonly WireSyncElement[]): OwnedElement[] {\n const normalized: OwnedElement[] = [];\n for (const element of elements) {\n const runtime = this.normalizeElement(element);\n if (runtime) normalized.push(runtime);\n }\n return normalized;\n }\n\n private relayOpToRoom(\n room: string,\n excludeId: string | undefined,\n from: string,\n op: WireSyncOp,\n ): number {\n const members = this.rooms.get(room);\n if (!members) return 0;\n let sent = 0;\n const encoded: EncodedFrames = new Map();\n for (const connectionId of members) {\n if (connectionId === excludeId) continue;\n const conn = this.conns.get(connectionId);\n if (!conn) continue;\n // A lossy translation is skipped for this peer without blocking compatible peers.\n if (this.sendToConnection(conn, from, op, encoded)) sent += 1;\n }\n return sent;\n }\n\n private broadcastClientPresence(conn: Connection, data: unknown): void {\n this.presenceConnections.add(conn.id);\n const message = JSON.stringify({ from: conn.id, op: { kind: 'presence', data } });\n this.relayToRoom(conn.room, conn.id, message);\n this.safePublish(\n JSON.stringify({\n o: this.instanceId,\n room: conn.room,\n from: conn.id,\n op: { kind: 'presence', data },\n }),\n );\n }\n\n private clearPresenceLanes(connId: string): void {\n const lanes = this.presenceLanes.get(connId);\n if (!lanes) return;\n for (const lane of lanes.values()) {\n if (lane.pending) clearTimeout(lane.pending.timer);\n }\n this.presenceLanes.delete(connId);\n }\n\n private schedulePresence(conn: Connection, data: unknown): void {\n if (this.presenceThrottleMs <= 0) {\n this.broadcastClientPresence(conn, data);\n return;\n }\n const lane = this.presenceLaneFor(conn.id, presenceLaneOf(data));\n const now = Date.now();\n if (lane.lastSentAt === undefined || now - lane.lastSentAt >= this.presenceThrottleMs) {\n if (lane.pending) {\n clearTimeout(lane.pending.timer);\n lane.pending = null;\n }\n lane.lastSentAt = now;\n this.broadcastClientPresence(conn, data);\n return;\n }\n if (lane.pending) {\n lane.pending.data = data;\n return;\n }\n const timer = setTimeout(\n () => {\n const pending = lane.pending;\n lane.pending = null;\n if (!pending || !this.conns.has(conn.id)) return;\n lane.lastSentAt = Date.now();\n this.broadcastClientPresence(conn, pending.data);\n },\n this.presenceThrottleMs - (now - lane.lastSentAt),\n );\n lane.pending = { data, timer };\n }\n\n private presenceLaneFor(connId: string, requested: string): PresenceLane {\n let lanes = this.presenceLanes.get(connId);\n if (!lanes) {\n lanes = new Map();\n this.presenceLanes.set(connId, lanes);\n }\n let key = requested;\n if (key !== FALLBACK_PRESENCE_LANE && !lanes.has(key)) {\n const named = lanes.size - (lanes.has(FALLBACK_PRESENCE_LANE) ? 1 : 0);\n if (named >= this.maxPresenceLanes - 1) key = FALLBACK_PRESENCE_LANE;\n }\n let lane = lanes.get(key);\n if (!lane) {\n lane = { lastSentAt: undefined, pending: null };\n lanes.set(key, lane);\n }\n return lane;\n }\n\n private broadcastLeave(room: string, from: string): void {\n const message = JSON.stringify({ from, op: { kind: 'presence-leave' } });\n this.relayToRoom(room, undefined, message);\n this.safePublish(\n JSON.stringify({ o: this.instanceId, room, from, op: { kind: 'presence-leave' } }),\n );\n }\n\n private deliverToRoom(\n room: string,\n excludeId: string | undefined,\n from: string,\n op: WireSyncOp,\n prevAudience: string | undefined,\n prevExisted: boolean,\n ): void {\n const members = this.rooms.get(room);\n if (!members) return;\n const encoded: EncodedFrames = new Map();\n if (op.kind === 'upsert') {\n const audience = (op.element as OwnedElement).audience;\n const removeOp: WireSyncOp = { kind: 'remove', id: op.element.id };\n const encodedRemove: EncodedFrames = new Map();\n for (const cid of members) {\n if (cid === excludeId) continue;\n const conn = this.conns.get(cid);\n if (!conn) continue;\n if (this.mayRead(conn, audience)) this.sendToConnection(conn, from, op, encoded);\n else if (prevExisted && this.mayRead(conn, prevAudience)) {\n this.sendToConnection(conn, HUB_FROM, removeOp, encodedRemove);\n }\n }\n } else if (op.kind === 'remove') {\n for (const cid of members) {\n if (cid === excludeId) continue;\n const conn = this.conns.get(cid);\n if (!conn) continue;\n // No read filter → forward to all (today's behavior; current/prevExisted aren't fetched without\n // a hook). With canRead, only recipients who could see the removed element get it.\n const wasVisible = !this.canRead || (prevExisted && this.mayRead(conn, prevAudience));\n if (wasVisible) this.sendToConnection(conn, from, op, encoded);\n }\n } else if (op.kind === 'clear') {\n for (const cid of members) {\n if (cid === excludeId) continue;\n const conn = this.conns.get(cid);\n if (conn) this.sendToConnection(conn, from, op, encoded);\n }\n }\n }\n\n private async sendCorrection(\n conn: Connection,\n from: string,\n op: WireSyncOp,\n current: OwnedElement | undefined,\n ): Promise<void> {\n let correction: WireSyncOp | undefined;\n if (op.kind === 'upsert') {\n correction = current\n ? this.mayRead(conn, current.audience)\n ? { kind: 'upsert', element: current }\n : { kind: 'remove', id: current.id }\n : { kind: 'remove', id: op.element.id };\n } else if (op.kind === 'remove') {\n correction = current\n ? this.mayRead(conn, current.audience)\n ? { kind: 'upsert', element: current }\n : { kind: 'remove', id: current.id }\n : undefined;\n } else if (op.kind === 'clear') {\n const all = this.normalizeElements(await this.backend.snapshot(conn.room));\n const elements = this.canRead ? all.filter((el) => this.mayRead(conn, el.audience)) : all;\n correction = { kind: 'snapshot', to: from, elements };\n }\n if (correction) this.sendToConnection(conn, HUB_FROM, correction);\n }\n\n private onFanout(payload: string): void {\n // Off the serial queue on purpose: forward-only (the origin already applied to the SHARED backend),\n // and delivery is already ordered. Re-filter per local member (canRead runs on EVERY instance).\n // The channel is a trust boundary of its own: anyone who can publish to it reaches every\n // instance, so a fanout op passes the same shape validation as a client frame.\n if (!hasJsonDepthAtMost(payload, this.maxJsonDepth)) return;\n let env: {\n o?: unknown;\n room?: unknown;\n from?: unknown;\n op?: unknown;\n prev?: unknown;\n existed?: unknown;\n };\n try {\n env = JSON.parse(payload);\n } catch {\n return;\n }\n if (typeof env.o !== 'string' || typeof env.room !== 'string' || typeof env.from !== 'string')\n return;\n if (env.o === this.instanceId) return; // our own publish — already delivered locally\n const envelope = { from: env.from, op: env.op };\n if (!isValidEnvelope(envelope)) return;\n const op = envelope.op;\n if (isPresenceOp(op)) {\n if (op.kind === 'presence' && !this.isPresenceWithinLimit(op.data)) return;\n // presence/leave: raw forward to all local members (the sender lives on the origin instance),\n // no backend, no canRead filter.\n this.relayToRoom(env.room, undefined, JSON.stringify({ from: env.from, op }));\n return;\n }\n if (isLayerOp(op)) {\n void this.applyFanoutLayerOp(env.room, op).catch(() => {\n /* a broken backend must not break the fanout relay */\n });\n this.relayOpToRoom(env.room, undefined, env.from, op);\n return;\n }\n let plugin: ServerSyncPlugin | undefined;\n if (op.kind === 'extension') {\n const entry = this.pluginRegistry.extension(op.extensionKind);\n if (!entry || !entry.kind.codec.validate(op.payload)) return;\n plugin = entry.plugin;\n } else {\n plugin = this.pluginRegistry.ownerOf(op.kind);\n }\n if (plugin) {\n const owner = plugin;\n const previous = this.roomQueues.get(env.room) ?? Promise.resolve();\n const operation = previous.then(async () => {\n const context: ServerOpContext = {\n room: env.room as string,\n connectionId: env.from as string,\n backend: this.backend,\n backendPlugin: (key) => this.backend.getService?.(key),\n };\n const accepted = owner.applyFanout ? await owner.applyFanout(op, context) : op;\n if (accepted) {\n this.relayOpToRoom(env.room as string, undefined, env.from as string, accepted);\n }\n });\n this.roomQueues.set(\n env.room,\n operation.catch(() => {\n /* a broken backend must not wedge the room queue */\n }),\n );\n return;\n }\n if (!isFanoutOp(op)) return;\n const runtimeOp =\n op.kind === 'upsert'\n ? (() => {\n const element = this.normalizeElement(op.element);\n return element ? ({ ...op, element } satisfies WireSyncOp) : null;\n })()\n : op;\n if (!runtimeOp) return;\n const prevAudience = typeof env.prev === 'string' ? env.prev : undefined;\n const prevExisted = env.existed === true;\n this.deliverToRoom(env.room, undefined, env.from, runtimeOp, prevAudience, prevExisted);\n }\n\n private isPresenceWithinLimit(data: unknown): boolean {\n return utf8ByteLength(JSON.stringify(data) ?? '') <= this.maxPresenceBytes;\n }\n\n private async applyFanoutLayerOp(room: string, op: LayerOp): Promise<void> {\n const record = layerOpToRecord(op);\n const current = await this.getLayerRecord(room, record.id);\n if (current && !isNewerLayerRecord(record, current)) return;\n await this.applyLayerRecord(room, record);\n }\n\n close(): void {\n for (const connId of [...this.presenceLanes.keys()]) this.clearPresenceLanes(connId);\n this.fanoutUnsub();\n }\n}\n","import {\n applyOpToMap,\n type LayerRecord,\n type WireSyncElement,\n type WireSyncOp,\n} from '@fieldnotes/sync';\nimport type { HubBackend } from './hub-backend';\n\nexport class MemoryHubBackend implements HubBackend {\n private rooms = new Map<string, Map<string, WireSyncElement>>();\n private roomLayers = new Map<string, Map<string, LayerRecord>>();\n\n private room(id: string): Map<string, WireSyncElement> {\n let r = this.rooms.get(id);\n if (!r) {\n r = new Map();\n this.rooms.set(id, r);\n }\n return r;\n }\n\n private layers(room: string): Map<string, LayerRecord> {\n let r = this.roomLayers.get(room);\n if (!r) {\n r = new Map();\n this.roomLayers.set(room, r);\n }\n return r;\n }\n\n async snapshot(room: string): Promise<WireSyncElement[]> {\n return [...this.room(room).values()];\n }\n\n async get(room: string, id: string): Promise<WireSyncElement | undefined> {\n return this.room(room).get(id);\n }\n\n async apply(room: string, op: WireSyncOp): Promise<void> {\n if (op.kind === 'clear') {\n // Clears elements only; layer records are a separate, longer-lived ledger.\n this.rooms.delete(room);\n return;\n }\n applyOpToMap(this.room(room), op);\n }\n\n async layerRecords(room: string): Promise<LayerRecord[]> {\n return [...this.layers(room).values()];\n }\n\n async getLayerRecord(room: string, id: string): Promise<LayerRecord | undefined> {\n return this.layers(room).get(id);\n }\n\n async applyLayerRecord(room: string, record: LayerRecord): Promise<void> {\n this.layers(room).set(record.id, record);\n }\n}\n","export interface HubFanout {\n publish(payload: string): void | Promise<void>;\n subscribe(handler: (payload: string) => void): () => void;\n close?(): void;\n}\n\nexport class InMemoryHubFanout implements HubFanout {\n private readonly handlers = new Set<(payload: string) => void>();\n\n publish(payload: string): void {\n for (const h of this.handlers) {\n try {\n h(payload);\n } catch {\n /* one throwing subscriber must not break the publish loop */\n }\n }\n }\n\n subscribe(handler: (payload: string) => void): () => void {\n this.handlers.add(handler);\n return () => this.handlers.delete(handler);\n }\n}\n","import type { ServiceKey } from '@fieldnotes/core';\nimport type { ExtensionKind, PluginSnapshot, TypedExtensionOp, WireSyncOp } from '@fieldnotes/sync';\nimport type { HubBackend } from './hub-backend';\n\nexport interface ApplyResult {\n readonly accepted: WireSyncOp | null;\n readonly corrections: WireSyncOp[];\n readonly broadcast?: WireSyncOp[];\n readonly locality?: 'shared' | 'local';\n}\n\nexport interface ServerOpContext {\n readonly room: string;\n readonly connectionId: string;\n readonly userId?: string;\n readonly role?: string;\n readonly backend: HubBackend;\n backendPlugin<T>(key: ServiceKey<T>): T | undefined;\n}\n\nexport type ServerNext = (op: WireSyncOp, context: ServerOpContext) => Promise<ApplyResult>;\n\nexport interface ServerExtensionRegistry {\n register<TPayload>(\n kind: ExtensionKind<TPayload>,\n handler: (op: TypedExtensionOp<TPayload>, context: ServerOpContext) => Promise<ApplyResult>,\n ): void;\n}\n\nexport interface ServerSyncPlugin {\n readonly name: string;\n readonly ownedLegacyKinds?: readonly string[];\n readonly legacySnapshotKey?: string;\n process?(op: WireSyncOp, context: ServerOpContext, next: ServerNext): Promise<ApplyResult>;\n applyFanout?(op: WireSyncOp, context: ServerOpContext): Promise<WireSyncOp | null>;\n registerExtensionKinds?(registry: ServerExtensionRegistry): void;\n snapshot?(room: string, backend: HubBackend): Promise<PluginSnapshot | undefined>;\n filterSnapshot?(\n snapshot: PluginSnapshot,\n viewer: { userId?: string; role?: string },\n ): PluginSnapshot | null;\n}\n\ninterface ServerExtensionEntry {\n readonly kind: ExtensionKind<unknown>;\n readonly plugin: ServerSyncPlugin;\n readonly handler: (\n op: TypedExtensionOp<unknown>,\n context: ServerOpContext,\n ) => Promise<ApplyResult>;\n}\n\nexport class ServerPluginRegistry {\n private readonly byName = new Map<string, ServerSyncPlugin>();\n private readonly legacyOwners = new Map<string, ServerSyncPlugin>();\n private readonly extensions = new Map<string, ServerExtensionEntry>();\n private readonly definitions: ReadonlyMap<string, ExtensionKind<unknown>>;\n\n constructor(plugins: readonly ServerSyncPlugin[]) {\n for (const plugin of plugins) this.register(plugin);\n // Registration is constructor-only, so the translation view is fixed here\n // instead of rebuilt on every relayed frame.\n this.definitions = new Map([...this.extensions].map(([name, entry]) => [name, entry.kind]));\n }\n\n private register(plugin: ServerSyncPlugin): void {\n if (this.byName.has(plugin.name))\n throw new Error(`Server plugin \"${plugin.name}\" is duplicated`);\n this.byName.set(plugin.name, plugin);\n for (const kind of plugin.ownedLegacyKinds ?? []) {\n if (this.legacyOwners.has(kind)) throw new Error(`Sync op kind \"${kind}\" has two owners`);\n this.legacyOwners.set(kind, plugin);\n }\n plugin.registerExtensionKinds?.({\n register: <TPayload>(\n kind: ExtensionKind<TPayload>,\n handler: (op: TypedExtensionOp<TPayload>, context: ServerOpContext) => Promise<ApplyResult>,\n ) => {\n if (this.extensions.has(kind.extensionKind)) {\n throw new Error(`Extension kind \"${kind.extensionKind}\" has two owners`);\n }\n this.extensions.set(kind.extensionKind, {\n kind: kind as ExtensionKind<unknown>,\n plugin,\n handler: handler as ServerExtensionEntry['handler'],\n });\n },\n });\n }\n\n get plugins(): readonly ServerSyncPlugin[] {\n return [...this.byName.values()];\n }\n\n ownerOf(kind: string): ServerSyncPlugin | undefined {\n return this.legacyOwners.get(kind);\n }\n\n extension(extensionKind: string): ServerExtensionEntry | undefined {\n return this.extensions.get(extensionKind);\n }\n\n get extensionKinds(): readonly string[] {\n return [...this.extensions.keys()];\n }\n\n get extensionDefinitions(): ReadonlyMap<string, ExtensionKind<unknown>> {\n return this.definitions;\n }\n}\n\nexport type { PluginSnapshot } from '@fieldnotes/sync';\n","export const DEFAULT_MAX_MESSAGE_BYTES = 1024 * 1024;\nexport const DEFAULT_MAX_JSON_DEPTH = 32;\nexport const DEFAULT_MAX_PENDING_AUTH_MESSAGES = 100;\nexport const DEFAULT_MAX_PENDING_AUTH_BYTES = 2 * 1024 * 1024;\nexport const DEFAULT_MESSAGES_PER_SECOND = 120;\nexport const DEFAULT_MESSAGE_BURST = 240;\nexport const DEFAULT_PRESENCE_THROTTLE_MS = 50;\n/** Largest `presence.data` payload (UTF-8 bytes of its JSON) the hub relays. */\nexport const DEFAULT_MAX_PRESENCE_BYTES = 4 * 1024;\n/** Sustained inbound byte budget per connection; bounds relay amplification, not just frame count. */\nexport const DEFAULT_BYTES_PER_SECOND = 4 * 1024 * 1024;\nexport const DEFAULT_BYTE_BURST = 8 * 1024 * 1024;\n/** Concurrent sockets per client address, counted from the upgrade until close. */\nexport const DEFAULT_MAX_CONNECTIONS_PER_IP = 64;\n/** Concurrent sockets per room, pending-auth sockets included. */\nexport const DEFAULT_MAX_CONNECTIONS_PER_ROOM = 256;\n/** Per-connection presence throttle lanes, INCLUDING the reserved fallback lane. */\nexport const DEFAULT_MAX_PRESENCE_LANES = 16;\n\nexport function hasJsonDepthAtMost(message: string, maxDepth: number): boolean {\n let depth = 0;\n let inString = false;\n let escaped = false;\n\n for (const char of message) {\n if (inString) {\n if (escaped) escaped = false;\n else if (char === '\\\\') escaped = true;\n else if (char === '\"') inString = false;\n continue;\n }\n if (char === '\"') inString = true;\n else if (char === '{' || char === '[') {\n depth += 1;\n if (depth > maxDepth) return false;\n } else if (char === '}' || char === ']') {\n depth -= 1;\n if (depth < 0) return false;\n }\n }\n\n return depth === 0 && !inString;\n}\n\nexport class MessageRateLimiter {\n private tokens: number;\n private updatedAt: number;\n\n constructor(\n private readonly ratePerSecond: number,\n private readonly burst: number,\n now = Date.now(),\n ) {\n this.tokens = burst;\n this.updatedAt = now;\n }\n\n /** Charges `cost` tokens (frames or bytes); a cost above `burst` never fits. */\n take(now = Date.now(), cost = 1): boolean {\n const elapsedSeconds = Math.max(0, now - this.updatedAt) / 1000;\n this.tokens = Math.min(this.burst, this.tokens + elapsedSeconds * this.ratePerSecond);\n this.updatedAt = now;\n if (this.tokens < cost) return false;\n this.tokens -= cost;\n return true;\n }\n}\n","import { WebSocketServer, type RawData } from 'ws';\nimport type { IncomingMessage, Server } from 'http';\nimport { SyncHub } from './sync-hub';\nimport type { HubBackend } from './hub-backend';\nimport type { HubFanout } from './hub-fanout';\nimport { readBearerToken, type Authenticate } from './authenticate';\nimport type {\n Authorize,\n AuthorizeLayer,\n CanRead,\n CanReadOwnerId,\n ResolveAudience,\n} from './authorize';\nimport type { ServerSyncPlugin } from './sync-plugin';\nimport type { ElementRegistry } from '@fieldnotes/core';\nimport { startHeartbeat } from './heartbeat';\nimport { BEARER_SUBPROTOCOL_PREFIX, SYNC_WS_SUBPROTOCOL } from '@fieldnotes/sync';\nimport {\n DEFAULT_BYTES_PER_SECOND,\n DEFAULT_BYTE_BURST,\n DEFAULT_MAX_CONNECTIONS_PER_IP,\n DEFAULT_MAX_CONNECTIONS_PER_ROOM,\n DEFAULT_MAX_JSON_DEPTH,\n DEFAULT_MAX_MESSAGE_BYTES,\n DEFAULT_MAX_PENDING_AUTH_BYTES,\n DEFAULT_MAX_PENDING_AUTH_MESSAGES,\n DEFAULT_MESSAGE_BURST,\n DEFAULT_MESSAGES_PER_SECOND,\n DEFAULT_PRESENCE_THROTTLE_MS,\n MessageRateLimiter,\n} from './resource-limits';\nimport { DEFAULT_SHUTDOWN_GRACE_MS, drainWebSocketServer } from './shutdown';\nimport { isValidRoomName } from './room-name';\n\nexport interface CreateSyncServerOptions {\n port?: number;\n server?: Server;\n backend?: HubBackend;\n fanout?: HubFanout;\n instanceId?: string;\n authenticate?: Authenticate;\n authorize?: Authorize;\n authorizeLayer?: AuthorizeLayer;\n plugins?: readonly ServerSyncPlugin[];\n canRead?: CanRead;\n canReadOwnerId?: CanReadOwnerId;\n resolveAudience?: ResolveAudience;\n heartbeatIntervalMs?: number;\n maxMessageBytes?: number;\n maxJsonDepth?: number;\n maxPendingAuthMessages?: number;\n maxPendingAuthBytes?: number;\n messagesPerSecond?: number;\n messageBurst?: number;\n /** Sustained inbound bytes per second per connection (token bucket). */\n bytesPerSecond?: number;\n /** Inbound byte spike allowance per connection; a frame larger than this is never admitted. */\n byteBurst?: number;\n presenceThrottleMs?: number;\n maxPresenceLanes?: number;\n maxPresenceBytes?: number;\n /** Concurrent sockets per client address; `Infinity` disables the cap. */\n maxConnectionsPerIp?: number;\n /** Concurrent sockets per room, pending-auth sockets included; `Infinity` disables the cap. */\n maxConnectionsPerRoom?: number;\n /**\n * Resolves the client address the per-IP cap keys on. Defaults to the socket's\n * remote address; behind a trusted proxy read the forwarded header here.\n * Returning `undefined` or `''` exempts the connection from the per-IP cap.\n */\n clientAddress?: (req: IncomingMessage) => string | undefined;\n shutdownGraceMs?: number;\n /**\n * Registry used to translate extension envelopes for legacy peers. Without\n * it the hub relays unknown legacy element types verbatim and cannot encode\n * envelopes for pre-envelope clients.\n */\n elementRegistry?: ElementRegistry;\n}\n\nclass ConcurrencyCounter {\n private readonly counts = new Map<string, number>();\n\n constructor(private readonly limit: number) {}\n\n /** Reserves a slot for `key`; returns a release function, or null when the cap is reached. */\n acquire(key: string): (() => void) | null {\n const current = this.counts.get(key) ?? 0;\n if (current >= this.limit) return null;\n this.counts.set(key, current + 1);\n let released = false;\n return () => {\n if (released) return;\n released = true;\n const remaining = (this.counts.get(key) ?? 1) - 1;\n if (remaining <= 0) this.counts.delete(key);\n else this.counts.set(key, remaining);\n };\n }\n}\n\nfunction rawDataByteLength(data: RawData): number {\n if (Array.isArray(data)) return data.reduce((total, chunk) => total + chunk.byteLength, 0);\n return data.byteLength;\n}\n\nfunction requirePositiveFinite(name: string, value: number): number {\n if (!Number.isFinite(value) || value <= 0) {\n throw new RangeError(`${name} must be a positive finite number`);\n }\n return value;\n}\n\nfunction requirePositiveIntegerOrInfinity(name: string, value: number): number {\n if (value !== Infinity && (!Number.isSafeInteger(value) || value <= 0)) {\n throw new RangeError(`${name} must be a positive safe integer or Infinity`);\n }\n return value;\n}\n\nexport function createSyncServer(options: CreateSyncServerOptions = {}): {\n hub: SyncHub;\n wss: WebSocketServer;\n close: () => Promise<void>;\n} {\n const shutdownGraceMs = options.shutdownGraceMs ?? DEFAULT_SHUTDOWN_GRACE_MS;\n if (!Number.isFinite(shutdownGraceMs) || shutdownGraceMs < 0) {\n throw new RangeError('shutdownGraceMs must be a non-negative finite number');\n }\n const messagesPerSecond = requirePositiveFinite(\n 'messagesPerSecond',\n options.messagesPerSecond ?? DEFAULT_MESSAGES_PER_SECOND,\n );\n const messageBurst = requirePositiveFinite(\n 'messageBurst',\n options.messageBurst ?? DEFAULT_MESSAGE_BURST,\n );\n const bytesPerSecond = requirePositiveFinite(\n 'bytesPerSecond',\n options.bytesPerSecond ?? DEFAULT_BYTES_PER_SECOND,\n );\n const byteBurst = requirePositiveFinite('byteBurst', options.byteBurst ?? DEFAULT_BYTE_BURST);\n const maxConnectionsPerIp = requirePositiveIntegerOrInfinity(\n 'maxConnectionsPerIp',\n options.maxConnectionsPerIp ?? DEFAULT_MAX_CONNECTIONS_PER_IP,\n );\n const maxConnectionsPerRoom = requirePositiveIntegerOrInfinity(\n 'maxConnectionsPerRoom',\n options.maxConnectionsPerRoom ?? DEFAULT_MAX_CONNECTIONS_PER_ROOM,\n );\n if (options.authorize && !options.authenticate) {\n // Ownership authorization needs a stable userId; the anonymous default is\n // the per-socket connId, which changes on every reconnect.\n throw new Error('createSyncServer: `authorize` requires an `authenticate` hook');\n }\n const hub = new SyncHub({\n backend: options.backend,\n fanout: options.fanout,\n instanceId: options.instanceId,\n authorize: options.authorize,\n authorizeLayer: options.authorizeLayer,\n plugins: options.plugins,\n canRead: options.canRead,\n canReadOwnerId: options.canReadOwnerId,\n resolveAudience: options.resolveAudience,\n maxJsonDepth: options.maxJsonDepth ?? DEFAULT_MAX_JSON_DEPTH,\n presenceThrottleMs: options.presenceThrottleMs ?? DEFAULT_PRESENCE_THROTTLE_MS,\n maxPresenceLanes: options.maxPresenceLanes,\n maxPresenceBytes: options.maxPresenceBytes,\n elementRegistry: options.elementRegistry,\n });\n const maxMessageBytes = options.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES;\n // A browser fails the handshake unless one offered subprotocol is selected, so select the\n // sync subprotocol when offered, otherwise the first non-bearer one (ws's default choice).\n // The bearer entry carries the token and is never echoed back.\n const handleProtocols = (protocols: Set<string>): string | false => {\n if (protocols.has(SYNC_WS_SUBPROTOCOL)) return SYNC_WS_SUBPROTOCOL;\n for (const protocol of protocols) {\n if (!protocol.startsWith(BEARER_SUBPROTOCOL_PREFIX)) return protocol;\n }\n return false;\n };\n const wss = options.server\n ? new WebSocketServer({ server: options.server, maxPayload: maxMessageBytes, handleProtocols })\n : new WebSocketServer({\n port: options.port ?? 0,\n maxPayload: maxMessageBytes,\n handleProtocols,\n });\n const heartbeat = startHeartbeat(wss, options.heartbeatIntervalMs ?? 30000);\n let shuttingDown = false;\n let closePromise: Promise<void> | undefined;\n let counter = 0;\n const perIp = new ConcurrencyCounter(maxConnectionsPerIp);\n const perRoom = new ConcurrencyCounter(maxConnectionsPerRoom);\n const clientAddress = options.clientAddress ?? ((req) => req.socket.remoteAddress);\n wss.on('connection', (ws, req) => {\n if (shuttingDown) {\n ws.close(1001, 'server shutting down');\n return;\n }\n heartbeat.track(ws);\n // Protocol/parser failures (including maxPayload) close the peer. Consuming the socket-level\n // error keeps hostile input from becoming an uncaught process error.\n ws.on('error', () => undefined);\n const url = new URL(req.url ?? '', 'http://localhost');\n const room = url.searchParams.get('room');\n if (!room) {\n ws.close(4400, 'room required');\n return;\n }\n if (!isValidRoomName(room)) {\n ws.close(4400, 'invalid room');\n return;\n }\n // Caps are taken before auth: the pending-auth window is exactly what a flood targets.\n const address = clientAddress(req);\n const releaseIp = address ? perIp.acquire(address) : () => undefined;\n if (!releaseIp) {\n ws.close(4429, 'too many connections');\n return;\n }\n const releaseRoom = perRoom.acquire(room);\n if (!releaseRoom) {\n releaseIp();\n ws.close(4429, 'too many connections');\n return;\n }\n const connId = `c${++counter}-${Math.random().toString(36).slice(2, 8)}`;\n\n let state: 'pending' | 'ready' | 'rejected' = 'pending';\n let closed = false;\n let admitted = false;\n const queue: string[] = [];\n let queuedBytes = 0;\n const maxPendingAuthMessages =\n options.maxPendingAuthMessages ?? DEFAULT_MAX_PENDING_AUTH_MESSAGES;\n const maxPendingAuthBytes = options.maxPendingAuthBytes ?? DEFAULT_MAX_PENDING_AUTH_BYTES;\n const limiter = new MessageRateLimiter(messagesPerSecond, messageBurst);\n const byteLimiter = new MessageRateLimiter(bytesPerSecond, byteBurst);\n\n const send = (m: string) => {\n try {\n ws.send(m);\n } catch {\n /* socket closed mid-send */\n }\n };\n\n ws.on('message', (data) => {\n if (state === 'rejected') return;\n const messageBytes = rawDataByteLength(data);\n if (messageBytes > maxMessageBytes) {\n state = 'rejected';\n ws.close(1009, 'message too large');\n return;\n }\n const now = Date.now();\n if (!limiter.take(now) || !byteLimiter.take(now, messageBytes)) {\n state = 'rejected';\n ws.close(4408, 'rate limit exceeded');\n return;\n }\n const msg = String(data);\n if (state === 'pending') {\n if (\n queue.length >= maxPendingAuthMessages ||\n queuedBytes + messageBytes > maxPendingAuthBytes\n ) {\n state = 'rejected';\n ws.close(4408, 'authentication queue limit exceeded');\n return;\n }\n queue.push(msg);\n queuedBytes += messageBytes;\n return;\n }\n void hub.handleMessage(connId, msg).catch((err) => console.error('[sync-server]', err));\n });\n ws.on('close', () => {\n closed = true;\n releaseIp();\n releaseRoom();\n if (admitted) hub.removeConnection(connId);\n });\n\n Promise.resolve(\n options.authenticate\n ? options.authenticate({ req, room, token: readBearerToken(req) })\n : { userId: connId },\n )\n .then((result) => {\n if (closed || state === 'rejected' || shuttingDown) return;\n if (!result) {\n state = 'rejected';\n ws.close(4401, 'unauthorized');\n return;\n }\n state = 'ready';\n admitted = true;\n hub.addConnection({ id: connId, room, userId: result.userId, role: result.role, send });\n for (const m of queue) {\n void hub.handleMessage(connId, m).catch((err) => console.error('[sync-server]', err));\n }\n queue.length = 0;\n queuedBytes = 0;\n })\n .catch(() => {\n if (!closed && !shuttingDown) {\n state = 'rejected';\n ws.close(4401, 'unauthorized');\n }\n });\n });\n const close = (): Promise<void> => {\n if (closePromise) return closePromise;\n shuttingDown = true;\n closePromise = drainWebSocketServer(wss, shutdownGraceMs).then(() => {\n heartbeat.stop();\n hub.close();\n });\n return closePromise;\n };\n\n return {\n hub,\n wss,\n close,\n };\n}\n","import type { IncomingMessage } from 'http';\nimport { readBearerSubprotocol } from '@fieldnotes/sync';\n\nexport interface AuthInfo {\n req: IncomingMessage; // the raw WS upgrade request — read a token from req.url query or req.headers\n room: string;\n /**\n * Bearer token resolved by `readBearerToken(req)`: a `fieldnotes-bearer.<token>`\n * `Sec-WebSocket-Protocol` entry, else an `Authorization: Bearer` header, else the\n * `token` URL query parameter (legacy; URLs land in proxy logs). `undefined` when\n * none is present.\n */\n token?: string;\n}\n\nexport interface AuthResult {\n userId: string;\n role?: string; // captured now; enforced in a later release (D2/D3)\n}\n\nexport type Authenticate = (info: AuthInfo) => AuthResult | null | Promise<AuthResult | null>;\n\n/**\n * Resolves the bearer token of an upgrade request, preferring channels that stay\n * out of access logs: `Sec-WebSocket-Protocol` bearer entry, then\n * `Authorization: Bearer`, then the `token` query parameter.\n */\nexport function readBearerToken(req: IncomingMessage): string | undefined {\n const fromProtocol = readBearerSubprotocol(headerValue(req.headers['sec-websocket-protocol']));\n if (fromProtocol) return fromProtocol;\n const authorization = headerValue(req.headers['authorization']);\n if (authorization) {\n const match = /^Bearer\\s+(\\S+)$/i.exec(authorization.trim());\n if (match?.[1]) return match[1];\n }\n const query = new URL(req.url ?? '', 'http://localhost').searchParams.get('token');\n return query || undefined;\n}\n\nfunction headerValue(value: string | string[] | undefined): string | undefined {\n return Array.isArray(value) ? value.join(',') : value;\n}\n","export interface HeartbeatSocket {\n ping(): void;\n terminate(): void;\n on(event: 'pong', listener: () => void): void;\n}\nexport interface HeartbeatServer {\n clients: Set<HeartbeatSocket>;\n}\nexport interface Heartbeat {\n track(ws: HeartbeatSocket): void;\n stop(): void;\n}\n\nexport function startHeartbeat(wss: HeartbeatServer, intervalMs: number): Heartbeat {\n if (intervalMs <= 0) {\n return {\n track: () => {\n /* disabled */\n },\n stop: () => {\n /* disabled */\n },\n };\n }\n const alive = new WeakMap<HeartbeatSocket, boolean>();\n const track = (ws: HeartbeatSocket): void => {\n alive.set(ws, true);\n ws.on('pong', () => alive.set(ws, true));\n };\n const interval = setInterval(() => {\n for (const ws of wss.clients) {\n try {\n if (alive.get(ws) === false) {\n ws.terminate();\n continue;\n }\n alive.set(ws, false);\n ws.ping();\n } catch {\n /* socket dying mid-tick — skip it */\n }\n }\n }, intervalMs);\n return { track, stop: () => clearInterval(interval) };\n}\n","export const DEFAULT_SHUTDOWN_GRACE_MS = 5000;\n\nexport interface ShutdownSocket {\n close(code?: number, reason?: string): void;\n terminate(): void;\n}\n\nexport interface ShutdownServer {\n clients: Set<ShutdownSocket>;\n close(callback: (error?: Error) => void): void;\n}\n\n/** Stop admission, ask connected peers to leave, then forcefully reap stragglers. */\nexport function drainWebSocketServer(wss: ShutdownServer, graceMs: number): Promise<void> {\n return new Promise((resolve) => {\n let settled = false;\n const finish = (): void => {\n if (settled) return;\n settled = true;\n clearTimeout(forceTimer);\n resolve();\n };\n const forceTimer = setTimeout(() => {\n for (const ws of wss.clients) {\n try {\n ws.terminate();\n } catch {\n /* socket already failed while shutdown was in progress */\n }\n }\n finish();\n }, graceMs);\n\n // Calling close first prevents new connections. Its callback runs once all clients have left.\n wss.close(() => finish());\n for (const ws of wss.clients) {\n try {\n ws.close(1001, 'server shutting down');\n } catch {\n try {\n ws.terminate();\n } catch {\n /* socket already closed */\n }\n }\n }\n });\n}\n","/**\n * Room names the relay admits. Backends derive storage keys from the room\n * name (`<prefix><room>:layers`, `<prefix><room>:fog:meta`, ...), so the\n * alphabet excludes every separator a key builder uses and bounds the length\n * before `authenticate` runs.\n */\nexport const ROOM_NAME_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;\n\nexport function isValidRoomName(room: unknown): room is string {\n return typeof room === 'string' && ROOM_NAME_PATTERN.test(room);\n}\n"],"mappings":"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","names":["import_sync","import_sync","import_sync"]}
package/dist/index.d.cts CHANGED
@@ -60,6 +60,34 @@ interface ReadContext {
60
60
  audience: string | undefined;
61
61
  }
62
62
  type CanRead = (ctx: ReadContext) => boolean;
63
+ interface OwnerReadContext {
64
+ userId?: string;
65
+ role?: string;
66
+ room: string;
67
+ }
68
+ /**
69
+ * Decides whether a viewer may see the server-stamped `ownerId` on elements it
70
+ * receives (live ops, snapshots and corrections). Without a hook `ownerId` is
71
+ * stripped from every outbound frame; it stays in the backend for `authorize`.
72
+ */
73
+ type CanReadOwnerId = (ctx: OwnerReadContext) => boolean;
74
+ interface ResolveAudienceContext {
75
+ userId?: string;
76
+ role?: string;
77
+ room: string;
78
+ /** The incoming element as the client sent it, client-asserted `audience` included. */
79
+ element: WireSyncElement;
80
+ /** The hub's stored element for the same id, if any. */
81
+ currentElement?: OwnedElement;
82
+ }
83
+ /**
84
+ * Decides the authoritative `audience` of an upserted element. The returned
85
+ * value replaces whatever the client asserted (`undefined` clears the field)
86
+ * before `authorize`, storage and relay, so a client can neither hide content
87
+ * by tagging it nor reveal content by retagging it. Without a hook the
88
+ * client's tag passes through and `authorize` must check it.
89
+ */
90
+ type ResolveAudience = (ctx: ResolveAudienceContext) => string | undefined;
63
91
 
64
92
  interface ApplyResult {
65
93
  readonly accepted: WireSyncOp | null;
@@ -108,9 +136,13 @@ interface SyncHubOptions {
108
136
  authorizeLayer?: AuthorizeLayer;
109
137
  plugins?: readonly ServerSyncPlugin[];
110
138
  canRead?: CanRead;
139
+ canReadOwnerId?: CanReadOwnerId;
140
+ resolveAudience?: ResolveAudience;
111
141
  maxJsonDepth?: number;
112
142
  presenceThrottleMs?: number;
113
143
  maxPresenceLanes?: number;
144
+ /** Largest `presence.data` payload relayed, in UTF-8 bytes of its JSON encoding. */
145
+ maxPresenceBytes?: number;
114
146
  /** Registry used to translate extension elements for legacy peers. */
115
147
  elementRegistry?: ElementRegistry;
116
148
  }
@@ -129,10 +161,13 @@ declare class SyncHub {
129
161
  private readonly elementRegistry;
130
162
  private readonly peerCapabilities;
131
163
  private readonly canRead?;
164
+ private readonly canReadOwnerId?;
165
+ private readonly resolveAudience?;
132
166
  private readonly memoryLayers;
133
167
  private readonly maxJsonDepth;
134
168
  private readonly presenceThrottleMs;
135
169
  private readonly maxPresenceLanes;
170
+ private readonly maxPresenceBytes;
136
171
  /**
137
172
  * Presence throttle state keyed by connection, then by lane. A lane is the
138
173
  * payload's `kind` (a non-empty string of at most 64 chars) or the reserved
@@ -171,6 +206,7 @@ declare class SyncHub {
171
206
  private getLayerRecords;
172
207
  private getLayerRecord;
173
208
  private applyLayerRecord;
209
+ private mayReadOwnerId;
174
210
  private mayRead;
175
211
  private safePublish;
176
212
  private relayToRoom;
@@ -180,7 +216,11 @@ declare class SyncHub {
180
216
  * may reject the room operation that produced it.
181
217
  */
182
218
  private sendToConnection;
183
- /** Returns the wire frame for `op` translated for `capabilities`, or null when lossy. */
219
+ /**
220
+ * Returns the wire frame for `op` translated for `capabilities`, or null
221
+ * when lossy. The server-stamped `ownerId` is a server-side authorization
222
+ * fact: it leaves the hub only for peers `canReadOwnerId` admits.
223
+ */
184
224
  private encodeForPeer;
185
225
  /**
186
226
  * Normalize registered legacy wire elements before authorization, storage,
@@ -200,6 +240,7 @@ declare class SyncHub {
200
240
  private deliverToRoom;
201
241
  private sendCorrection;
202
242
  private onFanout;
243
+ private isPresenceWithinLimit;
203
244
  private applyFanoutLayerOp;
204
245
  close(): void;
205
246
  }
@@ -220,12 +261,25 @@ declare class MemoryHubBackend implements HubBackend {
220
261
  interface AuthInfo {
221
262
  req: IncomingMessage;
222
263
  room: string;
264
+ /**
265
+ * Bearer token resolved by `readBearerToken(req)`: a `fieldnotes-bearer.<token>`
266
+ * `Sec-WebSocket-Protocol` entry, else an `Authorization: Bearer` header, else the
267
+ * `token` URL query parameter (legacy; URLs land in proxy logs). `undefined` when
268
+ * none is present.
269
+ */
270
+ token?: string;
223
271
  }
224
272
  interface AuthResult {
225
273
  userId: string;
226
274
  role?: string;
227
275
  }
228
276
  type Authenticate = (info: AuthInfo) => AuthResult | null | Promise<AuthResult | null>;
277
+ /**
278
+ * Resolves the bearer token of an upgrade request, preferring channels that stay
279
+ * out of access logs: `Sec-WebSocket-Protocol` bearer entry, then
280
+ * `Authorization: Bearer`, then the `token` query parameter.
281
+ */
282
+ declare function readBearerToken(req: IncomingMessage): string | undefined;
229
283
 
230
284
  interface CreateSyncServerOptions {
231
285
  port?: number;
@@ -238,6 +292,8 @@ interface CreateSyncServerOptions {
238
292
  authorizeLayer?: AuthorizeLayer;
239
293
  plugins?: readonly ServerSyncPlugin[];
240
294
  canRead?: CanRead;
295
+ canReadOwnerId?: CanReadOwnerId;
296
+ resolveAudience?: ResolveAudience;
241
297
  heartbeatIntervalMs?: number;
242
298
  maxMessageBytes?: number;
243
299
  maxJsonDepth?: number;
@@ -245,8 +301,23 @@ interface CreateSyncServerOptions {
245
301
  maxPendingAuthBytes?: number;
246
302
  messagesPerSecond?: number;
247
303
  messageBurst?: number;
304
+ /** Sustained inbound bytes per second per connection (token bucket). */
305
+ bytesPerSecond?: number;
306
+ /** Inbound byte spike allowance per connection; a frame larger than this is never admitted. */
307
+ byteBurst?: number;
248
308
  presenceThrottleMs?: number;
249
309
  maxPresenceLanes?: number;
310
+ maxPresenceBytes?: number;
311
+ /** Concurrent sockets per client address; `Infinity` disables the cap. */
312
+ maxConnectionsPerIp?: number;
313
+ /** Concurrent sockets per room, pending-auth sockets included; `Infinity` disables the cap. */
314
+ maxConnectionsPerRoom?: number;
315
+ /**
316
+ * Resolves the client address the per-IP cap keys on. Defaults to the socket's
317
+ * remote address; behind a trusted proxy read the forwarded header here.
318
+ * Returning `undefined` or `''` exempts the connection from the per-IP cap.
319
+ */
320
+ clientAddress?: (req: IncomingMessage) => string | undefined;
250
321
  shutdownGraceMs?: number;
251
322
  /**
252
323
  * Registry used to translate extension envelopes for legacy peers. Without
@@ -261,6 +332,15 @@ declare function createSyncServer(options?: CreateSyncServerOptions): {
261
332
  close: () => Promise<void>;
262
333
  };
263
334
 
335
+ /**
336
+ * Room names the relay admits. Backends derive storage keys from the room
337
+ * name (`<prefix><room>:layers`, `<prefix><room>:fog:meta`, ...), so the
338
+ * alphabet excludes every separator a key builder uses and bounds the length
339
+ * before `authenticate` runs.
340
+ */
341
+ declare const ROOM_NAME_PATTERN: RegExp;
342
+ declare function isValidRoomName(room: unknown): room is string;
343
+
264
344
  interface HeartbeatSocket {
265
345
  ping(): void;
266
346
  terminate(): void;
@@ -275,4 +355,4 @@ interface Heartbeat {
275
355
  }
276
356
  declare function startHeartbeat(wss: HeartbeatServer, intervalMs: number): Heartbeat;
277
357
 
278
- export { type ApplyResult, type AuthInfo, type AuthResult, type Authenticate, type Authorize, type AuthorizeContext, type AuthorizeLayer, type AuthorizeLayerContext, type CanRead, type Connection, type CreateSyncServerOptions, type Heartbeat, type HeartbeatServer, type HeartbeatSocket, type HubBackend, type HubFanout, InMemoryHubFanout, MemoryHubBackend, type OwnedElement, type ReadContext, type ServerExtensionRegistry, type ServerNext, type ServerOpContext, type ServerSyncPlugin, SyncHub, type SyncHubOptions, createSyncServer, startHeartbeat };
358
+ export { type ApplyResult, type AuthInfo, type AuthResult, type Authenticate, type Authorize, type AuthorizeContext, type AuthorizeLayer, type AuthorizeLayerContext, type CanRead, type CanReadOwnerId, type Connection, type CreateSyncServerOptions, type Heartbeat, type HeartbeatServer, type HeartbeatSocket, type HubBackend, type HubFanout, InMemoryHubFanout, MemoryHubBackend, type OwnedElement, type OwnerReadContext, ROOM_NAME_PATTERN, type ReadContext, type ResolveAudience, type ResolveAudienceContext, type ServerExtensionRegistry, type ServerNext, type ServerOpContext, type ServerSyncPlugin, SyncHub, type SyncHubOptions, createSyncServer, isValidRoomName, readBearerToken, startHeartbeat };