@agimon-ai/doompi-session 0.0.1-alpha.2 → 0.0.1-alpha.21
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.
- package/dist/{src/schemas/apiContracts.cjs → apiContracts.cjs} +7 -3
- package/dist/apiContracts.cjs.map +1 -0
- package/dist/{src/schemas/apiContracts.d.mts.map → apiContracts.d.mts.map} +1 -1
- package/dist/{src/schemas/apiContracts.mjs → apiContracts.mjs} +1 -1
- package/dist/apiContracts.mjs.map +1 -0
- package/dist/{src/services/backgroundWorkService/index.cjs → backgroundWorkService-BC0743Nn.cjs} +17 -7
- package/dist/backgroundWorkService-BC0743Nn.cjs.map +1 -0
- package/dist/{src/services/backgroundWorkService/index.mjs → backgroundWorkService-ChVXCzxj.mjs} +3 -3
- package/dist/backgroundWorkService-ChVXCzxj.mjs.map +1 -0
- package/dist/extensions/pi.cjs +12 -6
- package/dist/extensions/pi.cjs.map +1 -1
- package/dist/extensions/pi.d.mts +1 -1
- package/dist/extensions/pi.d.mts.map +1 -1
- package/dist/extensions/pi.mjs +9 -3
- package/dist/extensions/pi.mjs.map +1 -1
- package/dist/extensions/server.cjs +154 -10
- package/dist/extensions/server.cjs.map +1 -1
- package/dist/extensions/server.d.mts +1 -1
- package/dist/extensions/server.d.mts.map +1 -1
- package/dist/extensions/server.mjs +146 -4
- package/dist/extensions/server.mjs.map +1 -1
- package/dist/index.cjs +24 -31
- package/dist/index.d.mts +295 -11
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2 -9
- package/dist/sessionDelivery-C_2qtUoE.cjs +954 -0
- package/dist/sessionDelivery-C_2qtUoE.cjs.map +1 -0
- package/dist/sessionDelivery-DNq4a8LB.mjs +799 -0
- package/dist/sessionDelivery-DNq4a8LB.mjs.map +1 -0
- package/package.json +16 -16
- package/dist/_virtual/_rolldown/runtime.cjs +0 -23
- package/dist/api-contracts.cjs +0 -7
- package/dist/api-contracts.d.cts +0 -2
- package/dist/api-contracts.d.mts +0 -2
- package/dist/api-contracts.mjs +0 -2
- package/dist/extensions/pi.d.cts +0 -5
- package/dist/extensions/pi.d.cts.map +0 -1
- package/dist/extensions/server.d.cts +0 -5
- package/dist/extensions/server.d.cts.map +0 -1
- package/dist/index.d.cts +0 -11
- package/dist/src/extensions/(backend)/api/session-peer/route.server.cjs +0 -7
- package/dist/src/extensions/(backend)/api/session-peer/route.server.cjs.map +0 -1
- package/dist/src/extensions/(backend)/api/session-peer/route.server.mjs +0 -8
- package/dist/src/extensions/(backend)/api/session-peer/route.server.mjs.map +0 -1
- package/dist/src/extensions/workspaces/sessions/(backend)/root.cli.cjs +0 -10
- package/dist/src/extensions/workspaces/sessions/(backend)/root.cli.cjs.map +0 -1
- package/dist/src/extensions/workspaces/sessions/(backend)/root.cli.mjs +0 -11
- package/dist/src/extensions/workspaces/sessions/(backend)/root.cli.mjs.map +0 -1
- package/dist/src/extensions/workspaces/sessions/(backend)/root.server.cjs +0 -101
- package/dist/src/extensions/workspaces/sessions/(backend)/root.server.cjs.map +0 -1
- package/dist/src/extensions/workspaces/sessions/(backend)/root.server.mjs +0 -99
- package/dist/src/extensions/workspaces/sessions/(backend)/root.server.mjs.map +0 -1
- package/dist/src/schemas/apiContracts.cjs.map +0 -1
- package/dist/src/schemas/apiContracts.d.cts +0 -52
- package/dist/src/schemas/apiContracts.d.cts.map +0 -1
- package/dist/src/schemas/apiContracts.mjs.map +0 -1
- package/dist/src/services/backgroundWorkService/index.cjs.map +0 -1
- package/dist/src/services/backgroundWorkService/index.d.cts +0 -9
- package/dist/src/services/backgroundWorkService/index.d.cts.map +0 -1
- package/dist/src/services/backgroundWorkService/index.d.mts +0 -9
- package/dist/src/services/backgroundWorkService/index.d.mts.map +0 -1
- package/dist/src/services/backgroundWorkService/index.mjs.map +0 -1
- package/dist/src/services/peerInbox/index.cjs +0 -18
- package/dist/src/services/peerInbox/index.cjs.map +0 -1
- package/dist/src/services/peerInbox/index.mjs +0 -17
- package/dist/src/services/peerInbox/index.mjs.map +0 -1
- package/dist/src/services/peerInboxApi/index.cjs +0 -48
- package/dist/src/services/peerInboxApi/index.cjs.map +0 -1
- package/dist/src/services/peerInboxApi/index.mjs +0 -48
- package/dist/src/services/peerInboxApi/index.mjs.map +0 -1
- package/dist/src/services/peerTransport/index.cjs +0 -206
- package/dist/src/services/peerTransport/index.cjs.map +0 -1
- package/dist/src/services/peerTransport/index.d.cts +0 -49
- package/dist/src/services/peerTransport/index.d.cts.map +0 -1
- package/dist/src/services/peerTransport/index.d.mts +0 -49
- package/dist/src/services/peerTransport/index.d.mts.map +0 -1
- package/dist/src/services/peerTransport/index.mjs +0 -195
- package/dist/src/services/peerTransport/index.mjs.map +0 -1
- package/dist/src/services/sessionDelivery/index.cjs +0 -277
- package/dist/src/services/sessionDelivery/index.cjs.map +0 -1
- package/dist/src/services/sessionDelivery/index.d.cts +0 -10
- package/dist/src/services/sessionDelivery/index.d.cts.map +0 -1
- package/dist/src/services/sessionDelivery/index.d.mts +0 -10
- package/dist/src/services/sessionDelivery/index.d.mts.map +0 -1
- package/dist/src/services/sessionDelivery/index.mjs +0 -274
- package/dist/src/services/sessionDelivery/index.mjs.map +0 -1
- package/dist/src/services/sessionDelivery/type.cjs +0 -11
- package/dist/src/services/sessionDelivery/type.cjs.map +0 -1
- package/dist/src/services/sessionDelivery/type.d.cts +0 -73
- package/dist/src/services/sessionDelivery/type.d.cts.map +0 -1
- package/dist/src/services/sessionDelivery/type.d.mts +0 -73
- package/dist/src/services/sessionDelivery/type.d.mts.map +0 -1
- package/dist/src/services/sessionDelivery/type.mjs +0 -10
- package/dist/src/services/sessionDelivery/type.mjs.map +0 -1
- package/dist/src/services/sessionDirectory/index.cjs +0 -120
- package/dist/src/services/sessionDirectory/index.cjs.map +0 -1
- package/dist/src/services/sessionDirectory/index.d.cts +0 -7
- package/dist/src/services/sessionDirectory/index.d.cts.map +0 -1
- package/dist/src/services/sessionDirectory/index.d.mts +0 -7
- package/dist/src/services/sessionDirectory/index.d.mts.map +0 -1
- package/dist/src/services/sessionDirectory/index.mjs +0 -120
- package/dist/src/services/sessionDirectory/index.mjs.map +0 -1
- package/dist/src/services/sessionDirectory/type.d.cts +0 -54
- package/dist/src/services/sessionDirectory/type.d.cts.map +0 -1
- package/dist/src/services/sessionDirectory/type.d.mts +0 -54
- package/dist/src/services/sessionDirectory/type.d.mts.map +0 -1
- package/dist/src/services/sessionGroups/index.cjs +0 -105
- package/dist/src/services/sessionGroups/index.cjs.map +0 -1
- package/dist/src/services/sessionGroups/index.d.cts +0 -7
- package/dist/src/services/sessionGroups/index.d.cts.map +0 -1
- package/dist/src/services/sessionGroups/index.d.mts +0 -7
- package/dist/src/services/sessionGroups/index.d.mts.map +0 -1
- package/dist/src/services/sessionGroups/index.mjs +0 -104
- package/dist/src/services/sessionGroups/index.mjs.map +0 -1
- package/dist/src/services/sessionGroups/type.cjs +0 -11
- package/dist/src/services/sessionGroups/type.cjs.map +0 -1
- package/dist/src/services/sessionGroups/type.d.cts +0 -32
- package/dist/src/services/sessionGroups/type.d.cts.map +0 -1
- package/dist/src/services/sessionGroups/type.d.mts +0 -32
- package/dist/src/services/sessionGroups/type.d.mts.map +0 -1
- package/dist/src/services/sessionGroups/type.mjs +0 -11
- package/dist/src/services/sessionGroups/type.mjs.map +0 -1
- package/dist/src/services/sessionIntercom/index.cjs +0 -89
- package/dist/src/services/sessionIntercom/index.cjs.map +0 -1
- package/dist/src/services/sessionIntercom/index.d.cts +0 -11
- package/dist/src/services/sessionIntercom/index.d.cts.map +0 -1
- package/dist/src/services/sessionIntercom/index.d.mts +0 -11
- package/dist/src/services/sessionIntercom/index.d.mts.map +0 -1
- package/dist/src/services/sessionIntercom/index.mjs +0 -85
- package/dist/src/services/sessionIntercom/index.mjs.map +0 -1
- package/dist/src/services/sessionIntercom/type.cjs +0 -16
- package/dist/src/services/sessionIntercom/type.cjs.map +0 -1
- package/dist/src/services/sessionIntercom/type.d.cts +0 -60
- package/dist/src/services/sessionIntercom/type.d.cts.map +0 -1
- package/dist/src/services/sessionIntercom/type.d.mts +0 -60
- package/dist/src/services/sessionIntercom/type.d.mts.map +0 -1
- package/dist/src/services/sessionIntercom/type.mjs +0 -14
- package/dist/src/services/sessionIntercom/type.mjs.map +0 -1
- /package/dist/{src/schemas/apiContracts.d.mts → apiContracts.d.mts} +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["path","fs","createHash","createHmac","DatabaseSync","timingSafeEqual"],"sources":["../../../../src/services/peerTransport/index.ts"],"sourcesContent":["import { createHash, createHmac, timingSafeEqual } from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { DatabaseSync } from 'node:sqlite';\n\nimport type { DoomSessionCommunicationEndpoint } from '@agimon-ai/doompi-core/hub-channel';\n\nconst PEER_CONFIG_VERSION = 1;\nconst PEER_CONFIG_FILE = 'peers.json';\nconst PEER_ROUTE = '/api/plugins/session-peer/inbox';\nconst PEER_HEADER = 'x-doompi-session-peer';\nconst TIMESTAMP_HEADER = 'x-doompi-session-timestamp';\nconst SIGNATURE_HEADER = 'x-doompi-session-signature';\nconst MAX_CLOCK_SKEW_MS = 5 * 60_000;\nconst MAX_REPLAY_ENTRIES = 10_000;\nconst REPLAY_DATABASE_FILE = 'peer-replay.sqlite';\n\nexport interface SessionPeer {\n readonly hostId: string;\n readonly url: string;\n readonly secret: string;\n /** Explicit remote durable-message targets. Empty means no delivery grant. */\n readonly allowedSessionIds: readonly string[];\n /** Explicit remote Voice media targets. Empty means no media grant. */\n readonly allowedVoiceSessionIds: readonly string[];\n}\n\nexport interface SessionPeerConfig {\n readonly version: typeof PEER_CONFIG_VERSION;\n readonly hostId: string;\n readonly peers: readonly SessionPeer[];\n}\n\nexport interface RemoteSessionReference {\n readonly hostId: string;\n readonly sessionId: string;\n}\n\nexport interface SessionPeerEnvelope {\n readonly sourceSessionId: string;\n readonly targetSessionId: string;\n readonly type: string;\n readonly payload: unknown;\n}\n\nexport interface SessionPeerReplayGuard {\n admit(request: Request, now?: number): boolean;\n close(): void;\n}\n\nfunction validId(value: unknown): value is string {\n return typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(value);\n}\n\nfunction peerOf(value: unknown): SessionPeer | undefined {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined;\n const record = value as Record<string, unknown>;\n if (!validId(record.hostId) || typeof record.url !== 'string' || typeof record.secret !== 'string') return undefined;\n let url: URL;\n try {\n url = new URL(record.url);\n } catch {\n return undefined;\n }\n if (url.protocol !== 'https:' || url.username !== '' || url.password !== '' || url.search !== '' || url.hash !== '')\n return undefined;\n const allowedSessionIds =\n Array.isArray(record.allowedSessionIds) && record.allowedSessionIds.every(validId)\n ? record.allowedSessionIds\n : undefined;\n const allowedVoiceSessionIds =\n Array.isArray(record.allowedVoiceSessionIds) && record.allowedVoiceSessionIds.every(validId)\n ? record.allowedVoiceSessionIds\n : [];\n if (allowedSessionIds === undefined || record.secret.length < 32) return undefined;\n return {\n hostId: record.hostId,\n url: url.toString(),\n secret: record.secret,\n allowedSessionIds,\n allowedVoiceSessionIds,\n };\n}\n\nexport function sessionPeerConfigPath(homeDirectory: string): string {\n return path.join(homeDirectory, '.pi', '.doom', 'session', PEER_CONFIG_FILE);\n}\n\n/** Reads operator-provisioned peers. Invalid or unreadable config fails closed. */\nexport function readSessionPeerConfig(homeDirectory: string): SessionPeerConfig | undefined {\n let parsed: unknown;\n try {\n const file = sessionPeerConfigPath(homeDirectory);\n if ((fs.statSync(file).mode & 0o077) !== 0) return undefined;\n parsed = JSON.parse(fs.readFileSync(file, 'utf8')) as unknown;\n } catch {\n return undefined;\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return undefined;\n const record = parsed as Record<string, unknown>;\n if (record.version !== PEER_CONFIG_VERSION || !validId(record.hostId) || !Array.isArray(record.peers))\n return undefined;\n const peers = record.peers.map(peerOf);\n if (peers.some((peer) => peer === undefined)) return undefined;\n const resolved = peers as SessionPeer[];\n if (\n new Set(resolved.map((peer) => peer.hostId)).size !== resolved.length ||\n resolved.some((peer) => peer.hostId === record.hostId)\n )\n return undefined;\n return { version: PEER_CONFIG_VERSION, hostId: record.hostId, peers: resolved };\n}\n\n/** `peer/<host-id>/<session-id>` is the only remote target spelling. */\nexport function parseRemoteSessionReference(value: string): RemoteSessionReference | undefined {\n const match = /^peer\\/([^/]+)\\/([^/]+)$/u.exec(value);\n return match && validId(match[1]) && validId(match[2]) ? { hostId: match[1], sessionId: match[2] } : undefined;\n}\n\nexport function peerSessionReference(hostId: string, sessionId: string): string {\n if (!validId(hostId) || !validId(sessionId))\n throw new TypeError('Remote host and session identifiers must be safe opaque IDs.');\n return `peer/${hostId}/${sessionId}`;\n}\n\nfunction signature(secret: string, method: string, pathname: string, timestamp: string, body: string): string {\n const digest = createHash('sha256').update(body).digest('base64url');\n return createHmac('sha256', secret).update(`${method}\\n${pathname}\\n${timestamp}\\n${digest}`).digest('base64url');\n}\n\nexport function peerRequestHeaders(\n sourceHostId: string,\n peer: SessionPeer,\n method: string,\n pathname: string,\n body: string,\n now = Date.now(),\n): Headers {\n if (!validId(sourceHostId)) throw new TypeError('Source host identity must be a safe opaque ID.');\n const timestamp = String(now);\n return new Headers({\n 'content-type': 'application/json',\n [PEER_HEADER]: sourceHostId,\n [TIMESTAMP_HEADER]: timestamp,\n [SIGNATURE_HEADER]: signature(peer.secret, method, pathname, timestamp, body),\n });\n}\n\n/** Persists accepted peer signatures so a valid request cannot be replayed within its clock-skew window. */\nexport function createPeerReplayGuard(homeDirectory: string): SessionPeerReplayGuard {\n const directory = path.dirname(sessionPeerConfigPath(homeDirectory));\n fs.mkdirSync(directory, { recursive: true, mode: 0o700 });\n const database = new DatabaseSync(path.join(directory, REPLAY_DATABASE_FILE));\n database.exec(`\n PRAGMA journal_mode = WAL;\n CREATE TABLE IF NOT EXISTS peer_request_replays (\n signature TEXT PRIMARY KEY,\n expires_at INTEGER NOT NULL\n );\n `);\n const removeExpired = database.prepare('DELETE FROM peer_request_replays WHERE expires_at < ?');\n const insert = database.prepare('INSERT OR IGNORE INTO peer_request_replays (signature, expires_at) VALUES (?, ?)');\n const count = database.prepare('SELECT COUNT(*) AS count FROM peer_request_replays');\n const trim = database.prepare(`\n DELETE FROM peer_request_replays\n WHERE signature IN (\n SELECT signature FROM peer_request_replays ORDER BY expires_at ASC, signature ASC LIMIT ?\n )\n `);\n return {\n admit(request, now = Date.now()) {\n const supplied = request.headers.get(SIGNATURE_HEADER);\n if (!supplied) return false;\n database.exec('BEGIN IMMEDIATE');\n try {\n removeExpired.run(now);\n const accepted = insert.run(supplied, now + MAX_CLOCK_SKEW_MS).changes === 1;\n const total = Number((count.get() as { count: number | bigint }).count);\n if (total > MAX_REPLAY_ENTRIES) trim.run(total - MAX_REPLAY_ENTRIES);\n database.exec('COMMIT');\n return accepted;\n } catch (cause) {\n database.exec('ROLLBACK');\n throw cause;\n }\n },\n close: () => database.close(),\n };\n}\n\nexport function verifyPeerRequest(\n config: SessionPeerConfig,\n request: Request,\n body: string,\n now = Date.now(),\n): SessionPeer | undefined {\n const hostId = request.headers.get(PEER_HEADER);\n const timestamp = request.headers.get(TIMESTAMP_HEADER);\n const supplied = request.headers.get(SIGNATURE_HEADER);\n if (!hostId || !timestamp || !supplied || !/^\\d{1,16}$/u.test(timestamp)) return undefined;\n const sentAt = Number(timestamp);\n if (!Number.isSafeInteger(sentAt) || Math.abs(now - sentAt) > MAX_CLOCK_SKEW_MS) return undefined;\n const peer = config.peers.find((candidate) => candidate.hostId === hostId);\n if (!peer) return undefined;\n const expected = Buffer.from(signature(peer.secret, request.method, new URL(request.url).pathname, timestamp, body));\n const actual = Buffer.from(supplied);\n return actual.byteLength === expected.byteLength && timingSafeEqual(actual, expected) ? peer : undefined;\n}\n\nfunction envelopeOf(value: unknown): SessionPeerEnvelope | undefined {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined;\n const record = value as Record<string, unknown>;\n return validId(record.sourceSessionId) &&\n validId(record.targetSessionId) &&\n typeof record.type === 'string' &&\n record.type !== ''\n ? {\n sourceSessionId: record.sourceSessionId,\n targetSessionId: record.targetSessionId,\n type: record.type,\n payload: record.payload,\n }\n : undefined;\n}\n\nexport function parseSessionPeerEnvelope(value: unknown): SessionPeerEnvelope | undefined {\n return envelopeOf(value);\n}\n\n/** Adds asynchronous tunnel publication to a host-bound local communication endpoint. */\nexport function createPeerCommunication(options: {\n readonly local: DoomSessionCommunicationEndpoint;\n readonly homeDirectory: string;\n readonly fetch?: typeof fetch;\n}): DoomSessionCommunicationEndpoint {\n const config = readSessionPeerConfig(options.homeDirectory);\n const send = options.fetch ?? fetch;\n return {\n sessionId: options.local.sessionId,\n publish(target, type, payload) {\n const remote = parseRemoteSessionReference(target);\n if (!remote) return options.local.publish(target, type, payload);\n const peer = config?.peers.find((candidate) => candidate.hostId === remote.hostId);\n // Pairing is not a grant. Outbound delivery needs the same explicit target\n // allowlist that the receiver enforces for inbound envelopes.\n if (!config || !peer || !peer.allowedSessionIds.includes(remote.sessionId)) return false;\n const body = JSON.stringify({\n sourceSessionId: options.local.sessionId,\n targetSessionId: remote.sessionId,\n type,\n payload,\n });\n const url = new URL(PEER_ROUTE, peer.url);\n void send(url, {\n method: 'POST',\n headers: peerRequestHeaders(config.hostId, peer, 'POST', '/inbox', body),\n body,\n }).catch(() => undefined);\n return true;\n },\n subscribe: (type, listener) => options.local.subscribe(type, listener),\n onPeerReady: (listener) => options.local.onPeerReady(listener),\n close: () => options.local.close(),\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { DoomSessionCommunicationEndpoint } from "@agimon-ai/doompi-core/hub-channel";
|
|
2
|
-
//#region src/services/peerTransport/index.d.ts
|
|
3
|
-
declare const PEER_CONFIG_VERSION = 1;
|
|
4
|
-
export interface SessionPeer {
|
|
5
|
-
readonly hostId: string;
|
|
6
|
-
readonly url: string;
|
|
7
|
-
readonly secret: string;
|
|
8
|
-
/** Explicit remote durable-message targets. Empty means no delivery grant. */
|
|
9
|
-
readonly allowedSessionIds: readonly string[];
|
|
10
|
-
/** Explicit remote Voice media targets. Empty means no media grant. */
|
|
11
|
-
readonly allowedVoiceSessionIds: readonly string[];
|
|
12
|
-
}
|
|
13
|
-
export interface SessionPeerConfig {
|
|
14
|
-
readonly version: typeof PEER_CONFIG_VERSION;
|
|
15
|
-
readonly hostId: string;
|
|
16
|
-
readonly peers: readonly SessionPeer[];
|
|
17
|
-
}
|
|
18
|
-
export interface RemoteSessionReference {
|
|
19
|
-
readonly hostId: string;
|
|
20
|
-
readonly sessionId: string;
|
|
21
|
-
}
|
|
22
|
-
export interface SessionPeerEnvelope {
|
|
23
|
-
readonly sourceSessionId: string;
|
|
24
|
-
readonly targetSessionId: string;
|
|
25
|
-
readonly type: string;
|
|
26
|
-
readonly payload: unknown;
|
|
27
|
-
}
|
|
28
|
-
export interface SessionPeerReplayGuard {
|
|
29
|
-
admit(request: Request, now?: number): boolean;
|
|
30
|
-
close(): void;
|
|
31
|
-
}
|
|
32
|
-
export declare function sessionPeerConfigPath(homeDirectory: string): string;
|
|
33
|
-
/** Reads operator-provisioned peers. Invalid or unreadable config fails closed. */
|
|
34
|
-
export declare function readSessionPeerConfig(homeDirectory: string): SessionPeerConfig | undefined;
|
|
35
|
-
/** `peer/<host-id>/<session-id>` is the only remote target spelling. */
|
|
36
|
-
export declare function parseRemoteSessionReference(value: string): RemoteSessionReference | undefined;
|
|
37
|
-
export declare function peerSessionReference(hostId: string, sessionId: string): string;
|
|
38
|
-
export declare function peerRequestHeaders(sourceHostId: string, peer: SessionPeer, method: string, pathname: string, body: string, now?: number): Headers;
|
|
39
|
-
/** Persists accepted peer signatures so a valid request cannot be replayed within its clock-skew window. */
|
|
40
|
-
export declare function createPeerReplayGuard(homeDirectory: string): SessionPeerReplayGuard;
|
|
41
|
-
export declare function verifyPeerRequest(config: SessionPeerConfig, request: Request, body: string, now?: number): SessionPeer | undefined;
|
|
42
|
-
/** Adds asynchronous tunnel publication to a host-bound local communication endpoint. */
|
|
43
|
-
export declare function createPeerCommunication(options: {
|
|
44
|
-
readonly local: DoomSessionCommunicationEndpoint;
|
|
45
|
-
readonly homeDirectory: string;
|
|
46
|
-
readonly fetch?: typeof fetch;
|
|
47
|
-
}): DoomSessionCommunicationEndpoint;
|
|
48
|
-
//#endregion
|
|
49
|
-
//# sourceMappingURL=index.d.cts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../../../../src/services/peerTransport/index.ts"],"mappings":";;cAOM;iBAUW;WACN;WACA;WACA;;WAEA;;WAEA;;iBAGM;WACN,gBAAgB;WAChB;WACA,gBAAgB;;iBAGV;WACN;WACA;;iBAGM;WACN;WACA;WACA;WACA;;iBAGM;EACf,MAAM,SAAS,SAAS;EACxB;;wBAqCc,sBAAsB;;wBAKtB,sBAAsB,wBAAwB;;wBAyB9C,4BAA4B,gBAAgB;wBAK5C,qBAAqB,gBAAgB;wBAWrC,mBACd,sBACA,MAAM,aACN,gBACA,kBACA,cACA,eACC;;wBAYa,sBAAsB,wBAAwB;wBAyC9C,kBACd,QAAQ,mBACR,SAAS,SACT,cACA,eACC;;wBAmCa,wBAAwB;WAC7B,OAAO;WACP;WACA,eAAe;IACtB"}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { DoomSessionCommunicationEndpoint } from "@agimon-ai/doompi-core/hub-channel";
|
|
2
|
-
//#region src/services/peerTransport/index.d.ts
|
|
3
|
-
declare const PEER_CONFIG_VERSION = 1;
|
|
4
|
-
export interface SessionPeer {
|
|
5
|
-
readonly hostId: string;
|
|
6
|
-
readonly url: string;
|
|
7
|
-
readonly secret: string;
|
|
8
|
-
/** Explicit remote durable-message targets. Empty means no delivery grant. */
|
|
9
|
-
readonly allowedSessionIds: readonly string[];
|
|
10
|
-
/** Explicit remote Voice media targets. Empty means no media grant. */
|
|
11
|
-
readonly allowedVoiceSessionIds: readonly string[];
|
|
12
|
-
}
|
|
13
|
-
export interface SessionPeerConfig {
|
|
14
|
-
readonly version: typeof PEER_CONFIG_VERSION;
|
|
15
|
-
readonly hostId: string;
|
|
16
|
-
readonly peers: readonly SessionPeer[];
|
|
17
|
-
}
|
|
18
|
-
export interface RemoteSessionReference {
|
|
19
|
-
readonly hostId: string;
|
|
20
|
-
readonly sessionId: string;
|
|
21
|
-
}
|
|
22
|
-
export interface SessionPeerEnvelope {
|
|
23
|
-
readonly sourceSessionId: string;
|
|
24
|
-
readonly targetSessionId: string;
|
|
25
|
-
readonly type: string;
|
|
26
|
-
readonly payload: unknown;
|
|
27
|
-
}
|
|
28
|
-
export interface SessionPeerReplayGuard {
|
|
29
|
-
admit(request: Request, now?: number): boolean;
|
|
30
|
-
close(): void;
|
|
31
|
-
}
|
|
32
|
-
export declare function sessionPeerConfigPath(homeDirectory: string): string;
|
|
33
|
-
/** Reads operator-provisioned peers. Invalid or unreadable config fails closed. */
|
|
34
|
-
export declare function readSessionPeerConfig(homeDirectory: string): SessionPeerConfig | undefined;
|
|
35
|
-
/** `peer/<host-id>/<session-id>` is the only remote target spelling. */
|
|
36
|
-
export declare function parseRemoteSessionReference(value: string): RemoteSessionReference | undefined;
|
|
37
|
-
export declare function peerSessionReference(hostId: string, sessionId: string): string;
|
|
38
|
-
export declare function peerRequestHeaders(sourceHostId: string, peer: SessionPeer, method: string, pathname: string, body: string, now?: number): Headers;
|
|
39
|
-
/** Persists accepted peer signatures so a valid request cannot be replayed within its clock-skew window. */
|
|
40
|
-
export declare function createPeerReplayGuard(homeDirectory: string): SessionPeerReplayGuard;
|
|
41
|
-
export declare function verifyPeerRequest(config: SessionPeerConfig, request: Request, body: string, now?: number): SessionPeer | undefined;
|
|
42
|
-
/** Adds asynchronous tunnel publication to a host-bound local communication endpoint. */
|
|
43
|
-
export declare function createPeerCommunication(options: {
|
|
44
|
-
readonly local: DoomSessionCommunicationEndpoint;
|
|
45
|
-
readonly homeDirectory: string;
|
|
46
|
-
readonly fetch?: typeof fetch;
|
|
47
|
-
}): DoomSessionCommunicationEndpoint;
|
|
48
|
-
//#endregion
|
|
49
|
-
//# sourceMappingURL=index.d.mts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../../src/services/peerTransport/index.ts"],"mappings":";;cAOM;iBAUW;WACN;WACA;WACA;;WAEA;;WAEA;;iBAGM;WACN,gBAAgB;WAChB;WACA,gBAAgB;;iBAGV;WACN;WACA;;iBAGM;WACN;WACA;WACA;WACA;;iBAGM;EACf,MAAM,SAAS,SAAS;EACxB;;wBAqCc,sBAAsB;;wBAKtB,sBAAsB,wBAAwB;;wBAyB9C,4BAA4B,gBAAgB;wBAK5C,qBAAqB,gBAAgB;wBAWrC,mBACd,sBACA,MAAM,aACN,gBACA,kBACA,cACA,eACC;;wBAYa,sBAAsB,wBAAwB;wBAyC9C,kBACd,QAAQ,mBACR,SAAS,SACT,cACA,eACC;;wBAmCa,wBAAwB;WAC7B,OAAO;WACP;WACA,eAAe;IACtB"}
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { DatabaseSync } from "node:sqlite";
|
|
5
|
-
//#region src/services/peerTransport/index.ts
|
|
6
|
-
const PEER_CONFIG_VERSION = 1;
|
|
7
|
-
const PEER_CONFIG_FILE = "peers.json";
|
|
8
|
-
const PEER_ROUTE = "/api/plugins/session-peer/inbox";
|
|
9
|
-
const PEER_HEADER = "x-doompi-session-peer";
|
|
10
|
-
const TIMESTAMP_HEADER = "x-doompi-session-timestamp";
|
|
11
|
-
const SIGNATURE_HEADER = "x-doompi-session-signature";
|
|
12
|
-
const MAX_CLOCK_SKEW_MS = 3e5;
|
|
13
|
-
const MAX_REPLAY_ENTRIES = 1e4;
|
|
14
|
-
const REPLAY_DATABASE_FILE = "peer-replay.sqlite";
|
|
15
|
-
function validId(value) {
|
|
16
|
-
return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(value);
|
|
17
|
-
}
|
|
18
|
-
function peerOf(value) {
|
|
19
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
20
|
-
const record = value;
|
|
21
|
-
if (!validId(record.hostId) || typeof record.url !== "string" || typeof record.secret !== "string") return void 0;
|
|
22
|
-
let url;
|
|
23
|
-
try {
|
|
24
|
-
url = new URL(record.url);
|
|
25
|
-
} catch {
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "") return void 0;
|
|
29
|
-
const allowedSessionIds = Array.isArray(record.allowedSessionIds) && record.allowedSessionIds.every(validId) ? record.allowedSessionIds : void 0;
|
|
30
|
-
const allowedVoiceSessionIds = Array.isArray(record.allowedVoiceSessionIds) && record.allowedVoiceSessionIds.every(validId) ? record.allowedVoiceSessionIds : [];
|
|
31
|
-
if (allowedSessionIds === void 0 || record.secret.length < 32) return void 0;
|
|
32
|
-
return {
|
|
33
|
-
hostId: record.hostId,
|
|
34
|
-
url: url.toString(),
|
|
35
|
-
secret: record.secret,
|
|
36
|
-
allowedSessionIds,
|
|
37
|
-
allowedVoiceSessionIds
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
function sessionPeerConfigPath(homeDirectory) {
|
|
41
|
-
return path.join(homeDirectory, ".pi", ".doom", "session", PEER_CONFIG_FILE);
|
|
42
|
-
}
|
|
43
|
-
/** Reads operator-provisioned peers. Invalid or unreadable config fails closed. */
|
|
44
|
-
function readSessionPeerConfig(homeDirectory) {
|
|
45
|
-
let parsed;
|
|
46
|
-
try {
|
|
47
|
-
const file = sessionPeerConfigPath(homeDirectory);
|
|
48
|
-
if ((fs.statSync(file).mode & 63) !== 0) return void 0;
|
|
49
|
-
parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
50
|
-
} catch {
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
54
|
-
const record = parsed;
|
|
55
|
-
if (record.version !== PEER_CONFIG_VERSION || !validId(record.hostId) || !Array.isArray(record.peers)) return void 0;
|
|
56
|
-
const peers = record.peers.map(peerOf);
|
|
57
|
-
if (peers.some((peer) => peer === void 0)) return void 0;
|
|
58
|
-
const resolved = peers;
|
|
59
|
-
if (new Set(resolved.map((peer) => peer.hostId)).size !== resolved.length || resolved.some((peer) => peer.hostId === record.hostId)) return void 0;
|
|
60
|
-
return {
|
|
61
|
-
version: PEER_CONFIG_VERSION,
|
|
62
|
-
hostId: record.hostId,
|
|
63
|
-
peers: resolved
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
/** `peer/<host-id>/<session-id>` is the only remote target spelling. */
|
|
67
|
-
function parseRemoteSessionReference(value) {
|
|
68
|
-
const match = /^peer\/([^/]+)\/([^/]+)$/u.exec(value);
|
|
69
|
-
return match && validId(match[1]) && validId(match[2]) ? {
|
|
70
|
-
hostId: match[1],
|
|
71
|
-
sessionId: match[2]
|
|
72
|
-
} : void 0;
|
|
73
|
-
}
|
|
74
|
-
function peerSessionReference(hostId, sessionId) {
|
|
75
|
-
if (!validId(hostId) || !validId(sessionId)) throw new TypeError("Remote host and session identifiers must be safe opaque IDs.");
|
|
76
|
-
return `peer/${hostId}/${sessionId}`;
|
|
77
|
-
}
|
|
78
|
-
function signature(secret, method, pathname, timestamp, body) {
|
|
79
|
-
const digest = createHash("sha256").update(body).digest("base64url");
|
|
80
|
-
return createHmac("sha256", secret).update(`${method}\n${pathname}\n${timestamp}\n${digest}`).digest("base64url");
|
|
81
|
-
}
|
|
82
|
-
function peerRequestHeaders(sourceHostId, peer, method, pathname, body, now = Date.now()) {
|
|
83
|
-
if (!validId(sourceHostId)) throw new TypeError("Source host identity must be a safe opaque ID.");
|
|
84
|
-
const timestamp = String(now);
|
|
85
|
-
return new Headers({
|
|
86
|
-
"content-type": "application/json",
|
|
87
|
-
[PEER_HEADER]: sourceHostId,
|
|
88
|
-
[TIMESTAMP_HEADER]: timestamp,
|
|
89
|
-
[SIGNATURE_HEADER]: signature(peer.secret, method, pathname, timestamp, body)
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
/** Persists accepted peer signatures so a valid request cannot be replayed within its clock-skew window. */
|
|
93
|
-
function createPeerReplayGuard(homeDirectory) {
|
|
94
|
-
const directory = path.dirname(sessionPeerConfigPath(homeDirectory));
|
|
95
|
-
fs.mkdirSync(directory, {
|
|
96
|
-
recursive: true,
|
|
97
|
-
mode: 448
|
|
98
|
-
});
|
|
99
|
-
const database = new DatabaseSync(path.join(directory, REPLAY_DATABASE_FILE));
|
|
100
|
-
database.exec(`
|
|
101
|
-
PRAGMA journal_mode = WAL;
|
|
102
|
-
CREATE TABLE IF NOT EXISTS peer_request_replays (
|
|
103
|
-
signature TEXT PRIMARY KEY,
|
|
104
|
-
expires_at INTEGER NOT NULL
|
|
105
|
-
);
|
|
106
|
-
`);
|
|
107
|
-
const removeExpired = database.prepare("DELETE FROM peer_request_replays WHERE expires_at < ?");
|
|
108
|
-
const insert = database.prepare("INSERT OR IGNORE INTO peer_request_replays (signature, expires_at) VALUES (?, ?)");
|
|
109
|
-
const count = database.prepare("SELECT COUNT(*) AS count FROM peer_request_replays");
|
|
110
|
-
const trim = database.prepare(`
|
|
111
|
-
DELETE FROM peer_request_replays
|
|
112
|
-
WHERE signature IN (
|
|
113
|
-
SELECT signature FROM peer_request_replays ORDER BY expires_at ASC, signature ASC LIMIT ?
|
|
114
|
-
)
|
|
115
|
-
`);
|
|
116
|
-
return {
|
|
117
|
-
admit(request, now = Date.now()) {
|
|
118
|
-
const supplied = request.headers.get(SIGNATURE_HEADER);
|
|
119
|
-
if (!supplied) return false;
|
|
120
|
-
database.exec("BEGIN IMMEDIATE");
|
|
121
|
-
try {
|
|
122
|
-
removeExpired.run(now);
|
|
123
|
-
const accepted = insert.run(supplied, now + MAX_CLOCK_SKEW_MS).changes === 1;
|
|
124
|
-
const total = Number(count.get().count);
|
|
125
|
-
if (total > MAX_REPLAY_ENTRIES) trim.run(total - MAX_REPLAY_ENTRIES);
|
|
126
|
-
database.exec("COMMIT");
|
|
127
|
-
return accepted;
|
|
128
|
-
} catch (cause) {
|
|
129
|
-
database.exec("ROLLBACK");
|
|
130
|
-
throw cause;
|
|
131
|
-
}
|
|
132
|
-
},
|
|
133
|
-
close: () => database.close()
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
function verifyPeerRequest(config, request, body, now = Date.now()) {
|
|
137
|
-
const hostId = request.headers.get(PEER_HEADER);
|
|
138
|
-
const timestamp = request.headers.get(TIMESTAMP_HEADER);
|
|
139
|
-
const supplied = request.headers.get(SIGNATURE_HEADER);
|
|
140
|
-
if (!hostId || !timestamp || !supplied || !/^\d{1,16}$/u.test(timestamp)) return void 0;
|
|
141
|
-
const sentAt = Number(timestamp);
|
|
142
|
-
if (!Number.isSafeInteger(sentAt) || Math.abs(now - sentAt) > MAX_CLOCK_SKEW_MS) return void 0;
|
|
143
|
-
const peer = config.peers.find((candidate) => candidate.hostId === hostId);
|
|
144
|
-
if (!peer) return void 0;
|
|
145
|
-
const expected = Buffer.from(signature(peer.secret, request.method, new URL(request.url).pathname, timestamp, body));
|
|
146
|
-
const actual = Buffer.from(supplied);
|
|
147
|
-
return actual.byteLength === expected.byteLength && timingSafeEqual(actual, expected) ? peer : void 0;
|
|
148
|
-
}
|
|
149
|
-
function envelopeOf(value) {
|
|
150
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
151
|
-
const record = value;
|
|
152
|
-
return validId(record.sourceSessionId) && validId(record.targetSessionId) && typeof record.type === "string" && record.type !== "" ? {
|
|
153
|
-
sourceSessionId: record.sourceSessionId,
|
|
154
|
-
targetSessionId: record.targetSessionId,
|
|
155
|
-
type: record.type,
|
|
156
|
-
payload: record.payload
|
|
157
|
-
} : void 0;
|
|
158
|
-
}
|
|
159
|
-
function parseSessionPeerEnvelope(value) {
|
|
160
|
-
return envelopeOf(value);
|
|
161
|
-
}
|
|
162
|
-
/** Adds asynchronous tunnel publication to a host-bound local communication endpoint. */
|
|
163
|
-
function createPeerCommunication(options) {
|
|
164
|
-
const config = readSessionPeerConfig(options.homeDirectory);
|
|
165
|
-
const send = options.fetch ?? fetch;
|
|
166
|
-
return {
|
|
167
|
-
sessionId: options.local.sessionId,
|
|
168
|
-
publish(target, type, payload) {
|
|
169
|
-
const remote = parseRemoteSessionReference(target);
|
|
170
|
-
if (!remote) return options.local.publish(target, type, payload);
|
|
171
|
-
const peer = config?.peers.find((candidate) => candidate.hostId === remote.hostId);
|
|
172
|
-
if (!config || !peer || !peer.allowedSessionIds.includes(remote.sessionId)) return false;
|
|
173
|
-
const body = JSON.stringify({
|
|
174
|
-
sourceSessionId: options.local.sessionId,
|
|
175
|
-
targetSessionId: remote.sessionId,
|
|
176
|
-
type,
|
|
177
|
-
payload
|
|
178
|
-
});
|
|
179
|
-
const url = new URL(PEER_ROUTE, peer.url);
|
|
180
|
-
send(url, {
|
|
181
|
-
method: "POST",
|
|
182
|
-
headers: peerRequestHeaders(config.hostId, peer, "POST", "/inbox", body),
|
|
183
|
-
body
|
|
184
|
-
}).catch(() => void 0);
|
|
185
|
-
return true;
|
|
186
|
-
},
|
|
187
|
-
subscribe: (type, listener) => options.local.subscribe(type, listener),
|
|
188
|
-
onPeerReady: (listener) => options.local.onPeerReady(listener),
|
|
189
|
-
close: () => options.local.close()
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
//#endregion
|
|
193
|
-
export { createPeerCommunication, createPeerReplayGuard, parseRemoteSessionReference, parseSessionPeerEnvelope, peerRequestHeaders, peerSessionReference, readSessionPeerConfig, sessionPeerConfigPath, verifyPeerRequest };
|
|
194
|
-
|
|
195
|
-
//# sourceMappingURL=index.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../src/services/peerTransport/index.ts"],"sourcesContent":["import { createHash, createHmac, timingSafeEqual } from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { DatabaseSync } from 'node:sqlite';\n\nimport type { DoomSessionCommunicationEndpoint } from '@agimon-ai/doompi-core/hub-channel';\n\nconst PEER_CONFIG_VERSION = 1;\nconst PEER_CONFIG_FILE = 'peers.json';\nconst PEER_ROUTE = '/api/plugins/session-peer/inbox';\nconst PEER_HEADER = 'x-doompi-session-peer';\nconst TIMESTAMP_HEADER = 'x-doompi-session-timestamp';\nconst SIGNATURE_HEADER = 'x-doompi-session-signature';\nconst MAX_CLOCK_SKEW_MS = 5 * 60_000;\nconst MAX_REPLAY_ENTRIES = 10_000;\nconst REPLAY_DATABASE_FILE = 'peer-replay.sqlite';\n\nexport interface SessionPeer {\n readonly hostId: string;\n readonly url: string;\n readonly secret: string;\n /** Explicit remote durable-message targets. Empty means no delivery grant. */\n readonly allowedSessionIds: readonly string[];\n /** Explicit remote Voice media targets. Empty means no media grant. */\n readonly allowedVoiceSessionIds: readonly string[];\n}\n\nexport interface SessionPeerConfig {\n readonly version: typeof PEER_CONFIG_VERSION;\n readonly hostId: string;\n readonly peers: readonly SessionPeer[];\n}\n\nexport interface RemoteSessionReference {\n readonly hostId: string;\n readonly sessionId: string;\n}\n\nexport interface SessionPeerEnvelope {\n readonly sourceSessionId: string;\n readonly targetSessionId: string;\n readonly type: string;\n readonly payload: unknown;\n}\n\nexport interface SessionPeerReplayGuard {\n admit(request: Request, now?: number): boolean;\n close(): void;\n}\n\nfunction validId(value: unknown): value is string {\n return typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(value);\n}\n\nfunction peerOf(value: unknown): SessionPeer | undefined {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined;\n const record = value as Record<string, unknown>;\n if (!validId(record.hostId) || typeof record.url !== 'string' || typeof record.secret !== 'string') return undefined;\n let url: URL;\n try {\n url = new URL(record.url);\n } catch {\n return undefined;\n }\n if (url.protocol !== 'https:' || url.username !== '' || url.password !== '' || url.search !== '' || url.hash !== '')\n return undefined;\n const allowedSessionIds =\n Array.isArray(record.allowedSessionIds) && record.allowedSessionIds.every(validId)\n ? record.allowedSessionIds\n : undefined;\n const allowedVoiceSessionIds =\n Array.isArray(record.allowedVoiceSessionIds) && record.allowedVoiceSessionIds.every(validId)\n ? record.allowedVoiceSessionIds\n : [];\n if (allowedSessionIds === undefined || record.secret.length < 32) return undefined;\n return {\n hostId: record.hostId,\n url: url.toString(),\n secret: record.secret,\n allowedSessionIds,\n allowedVoiceSessionIds,\n };\n}\n\nexport function sessionPeerConfigPath(homeDirectory: string): string {\n return path.join(homeDirectory, '.pi', '.doom', 'session', PEER_CONFIG_FILE);\n}\n\n/** Reads operator-provisioned peers. Invalid or unreadable config fails closed. */\nexport function readSessionPeerConfig(homeDirectory: string): SessionPeerConfig | undefined {\n let parsed: unknown;\n try {\n const file = sessionPeerConfigPath(homeDirectory);\n if ((fs.statSync(file).mode & 0o077) !== 0) return undefined;\n parsed = JSON.parse(fs.readFileSync(file, 'utf8')) as unknown;\n } catch {\n return undefined;\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return undefined;\n const record = parsed as Record<string, unknown>;\n if (record.version !== PEER_CONFIG_VERSION || !validId(record.hostId) || !Array.isArray(record.peers))\n return undefined;\n const peers = record.peers.map(peerOf);\n if (peers.some((peer) => peer === undefined)) return undefined;\n const resolved = peers as SessionPeer[];\n if (\n new Set(resolved.map((peer) => peer.hostId)).size !== resolved.length ||\n resolved.some((peer) => peer.hostId === record.hostId)\n )\n return undefined;\n return { version: PEER_CONFIG_VERSION, hostId: record.hostId, peers: resolved };\n}\n\n/** `peer/<host-id>/<session-id>` is the only remote target spelling. */\nexport function parseRemoteSessionReference(value: string): RemoteSessionReference | undefined {\n const match = /^peer\\/([^/]+)\\/([^/]+)$/u.exec(value);\n return match && validId(match[1]) && validId(match[2]) ? { hostId: match[1], sessionId: match[2] } : undefined;\n}\n\nexport function peerSessionReference(hostId: string, sessionId: string): string {\n if (!validId(hostId) || !validId(sessionId))\n throw new TypeError('Remote host and session identifiers must be safe opaque IDs.');\n return `peer/${hostId}/${sessionId}`;\n}\n\nfunction signature(secret: string, method: string, pathname: string, timestamp: string, body: string): string {\n const digest = createHash('sha256').update(body).digest('base64url');\n return createHmac('sha256', secret).update(`${method}\\n${pathname}\\n${timestamp}\\n${digest}`).digest('base64url');\n}\n\nexport function peerRequestHeaders(\n sourceHostId: string,\n peer: SessionPeer,\n method: string,\n pathname: string,\n body: string,\n now = Date.now(),\n): Headers {\n if (!validId(sourceHostId)) throw new TypeError('Source host identity must be a safe opaque ID.');\n const timestamp = String(now);\n return new Headers({\n 'content-type': 'application/json',\n [PEER_HEADER]: sourceHostId,\n [TIMESTAMP_HEADER]: timestamp,\n [SIGNATURE_HEADER]: signature(peer.secret, method, pathname, timestamp, body),\n });\n}\n\n/** Persists accepted peer signatures so a valid request cannot be replayed within its clock-skew window. */\nexport function createPeerReplayGuard(homeDirectory: string): SessionPeerReplayGuard {\n const directory = path.dirname(sessionPeerConfigPath(homeDirectory));\n fs.mkdirSync(directory, { recursive: true, mode: 0o700 });\n const database = new DatabaseSync(path.join(directory, REPLAY_DATABASE_FILE));\n database.exec(`\n PRAGMA journal_mode = WAL;\n CREATE TABLE IF NOT EXISTS peer_request_replays (\n signature TEXT PRIMARY KEY,\n expires_at INTEGER NOT NULL\n );\n `);\n const removeExpired = database.prepare('DELETE FROM peer_request_replays WHERE expires_at < ?');\n const insert = database.prepare('INSERT OR IGNORE INTO peer_request_replays (signature, expires_at) VALUES (?, ?)');\n const count = database.prepare('SELECT COUNT(*) AS count FROM peer_request_replays');\n const trim = database.prepare(`\n DELETE FROM peer_request_replays\n WHERE signature IN (\n SELECT signature FROM peer_request_replays ORDER BY expires_at ASC, signature ASC LIMIT ?\n )\n `);\n return {\n admit(request, now = Date.now()) {\n const supplied = request.headers.get(SIGNATURE_HEADER);\n if (!supplied) return false;\n database.exec('BEGIN IMMEDIATE');\n try {\n removeExpired.run(now);\n const accepted = insert.run(supplied, now + MAX_CLOCK_SKEW_MS).changes === 1;\n const total = Number((count.get() as { count: number | bigint }).count);\n if (total > MAX_REPLAY_ENTRIES) trim.run(total - MAX_REPLAY_ENTRIES);\n database.exec('COMMIT');\n return accepted;\n } catch (cause) {\n database.exec('ROLLBACK');\n throw cause;\n }\n },\n close: () => database.close(),\n };\n}\n\nexport function verifyPeerRequest(\n config: SessionPeerConfig,\n request: Request,\n body: string,\n now = Date.now(),\n): SessionPeer | undefined {\n const hostId = request.headers.get(PEER_HEADER);\n const timestamp = request.headers.get(TIMESTAMP_HEADER);\n const supplied = request.headers.get(SIGNATURE_HEADER);\n if (!hostId || !timestamp || !supplied || !/^\\d{1,16}$/u.test(timestamp)) return undefined;\n const sentAt = Number(timestamp);\n if (!Number.isSafeInteger(sentAt) || Math.abs(now - sentAt) > MAX_CLOCK_SKEW_MS) return undefined;\n const peer = config.peers.find((candidate) => candidate.hostId === hostId);\n if (!peer) return undefined;\n const expected = Buffer.from(signature(peer.secret, request.method, new URL(request.url).pathname, timestamp, body));\n const actual = Buffer.from(supplied);\n return actual.byteLength === expected.byteLength && timingSafeEqual(actual, expected) ? peer : undefined;\n}\n\nfunction envelopeOf(value: unknown): SessionPeerEnvelope | undefined {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined;\n const record = value as Record<string, unknown>;\n return validId(record.sourceSessionId) &&\n validId(record.targetSessionId) &&\n typeof record.type === 'string' &&\n record.type !== ''\n ? {\n sourceSessionId: record.sourceSessionId,\n targetSessionId: record.targetSessionId,\n type: record.type,\n payload: record.payload,\n }\n : undefined;\n}\n\nexport function parseSessionPeerEnvelope(value: unknown): SessionPeerEnvelope | undefined {\n return envelopeOf(value);\n}\n\n/** Adds asynchronous tunnel publication to a host-bound local communication endpoint. */\nexport function createPeerCommunication(options: {\n readonly local: DoomSessionCommunicationEndpoint;\n readonly homeDirectory: string;\n readonly fetch?: typeof fetch;\n}): DoomSessionCommunicationEndpoint {\n const config = readSessionPeerConfig(options.homeDirectory);\n const send = options.fetch ?? fetch;\n return {\n sessionId: options.local.sessionId,\n publish(target, type, payload) {\n const remote = parseRemoteSessionReference(target);\n if (!remote) return options.local.publish(target, type, payload);\n const peer = config?.peers.find((candidate) => candidate.hostId === remote.hostId);\n // Pairing is not a grant. Outbound delivery needs the same explicit target\n // allowlist that the receiver enforces for inbound envelopes.\n if (!config || !peer || !peer.allowedSessionIds.includes(remote.sessionId)) return false;\n const body = JSON.stringify({\n sourceSessionId: options.local.sessionId,\n targetSessionId: remote.sessionId,\n type,\n payload,\n });\n const url = new URL(PEER_ROUTE, peer.url);\n void send(url, {\n method: 'POST',\n headers: peerRequestHeaders(config.hostId, peer, 'POST', '/inbox', body),\n body,\n }).catch(() => undefined);\n return true;\n },\n subscribe: (type, listener) => options.local.subscribe(type, listener),\n onPeerReady: (listener) => options.local.onPeerReady(listener),\n close: () => options.local.close(),\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,277 +0,0 @@
|
|
|
1
|
-
const require_type = require("./type.cjs");
|
|
2
|
-
let node_sqlite = require("node:sqlite");
|
|
3
|
-
//#region src/services/sessionDelivery/index.ts
|
|
4
|
-
const DELIVERY_EVENT = "doom/session-delivery/envelope";
|
|
5
|
-
const ACK_EVENT = "doom/session-delivery/ack";
|
|
6
|
-
const DEFAULT_DELIVERY = "prompt";
|
|
7
|
-
const INBOX_STATES = /* @__PURE__ */ new Set([
|
|
8
|
-
"accepted",
|
|
9
|
-
"admitting",
|
|
10
|
-
"admitted",
|
|
11
|
-
"recovery_required"
|
|
12
|
-
]);
|
|
13
|
-
function isRecord(value) {
|
|
14
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15
|
-
}
|
|
16
|
-
function isMode(value) {
|
|
17
|
-
return value === "prompt" || value === "steer" || value === "followUp";
|
|
18
|
-
}
|
|
19
|
-
function metadataOf(value) {
|
|
20
|
-
if (!isRecord(value)) return void 0;
|
|
21
|
-
const metadata = {};
|
|
22
|
-
for (const [key, item] of Object.entries(value)) {
|
|
23
|
-
if (typeof item !== "string") return void 0;
|
|
24
|
-
metadata[key] = item;
|
|
25
|
-
}
|
|
26
|
-
return metadata;
|
|
27
|
-
}
|
|
28
|
-
function envelopeOf(value) {
|
|
29
|
-
if (!isRecord(value)) return void 0;
|
|
30
|
-
const metadata = metadataOf(value.metadata);
|
|
31
|
-
if (typeof value.deliveryId !== "string" || !value.deliveryId || typeof value.kind !== "string" || !value.kind || typeof value.prompt !== "string" || !isMode(value.delivery) || metadata === void 0) return;
|
|
32
|
-
return {
|
|
33
|
-
deliveryId: value.deliveryId,
|
|
34
|
-
kind: value.kind,
|
|
35
|
-
prompt: value.prompt,
|
|
36
|
-
delivery: value.delivery,
|
|
37
|
-
metadata
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
function ackOf(value) {
|
|
41
|
-
if (!isRecord(value)) return void 0;
|
|
42
|
-
if (typeof value.deliveryId !== "string" || typeof value.state !== "string" || !INBOX_STATES.has(value.state)) return;
|
|
43
|
-
return value;
|
|
44
|
-
}
|
|
45
|
-
function normalizedMetadata(metadata) {
|
|
46
|
-
const normalized = {};
|
|
47
|
-
for (const [key, value] of Object.entries(metadata ?? {})) if (value !== void 0) normalized[key] = value;
|
|
48
|
-
return normalized;
|
|
49
|
-
}
|
|
50
|
-
function matchesMetadata(entry, query) {
|
|
51
|
-
return Object.entries(query ?? {}).every(([key, value]) => value === void 0 || entry[key] === value);
|
|
52
|
-
}
|
|
53
|
-
function sameMetadata(left, right) {
|
|
54
|
-
return matchesMetadata(left, right) && matchesMetadata(right, left);
|
|
55
|
-
}
|
|
56
|
-
/** Opens and starts one durable delivery endpoint. */
|
|
57
|
-
function createSessionDeliveryService(options) {
|
|
58
|
-
if (!options.recipientKey.trim() || options.recipientKey !== options.recipientKey.trim()) throw new TypeError("Session delivery requires a trimmed, non-empty recipient key.");
|
|
59
|
-
const database = new node_sqlite.DatabaseSync(options.databasePath);
|
|
60
|
-
database.exec(`
|
|
61
|
-
PRAGMA journal_mode = WAL;
|
|
62
|
-
CREATE TABLE IF NOT EXISTS session_delivery_outbox (
|
|
63
|
-
delivery_id TEXT PRIMARY KEY,
|
|
64
|
-
recipient_key TEXT NOT NULL,
|
|
65
|
-
kind TEXT NOT NULL,
|
|
66
|
-
prompt TEXT NOT NULL,
|
|
67
|
-
delivery_mode TEXT NOT NULL,
|
|
68
|
-
metadata_json TEXT NOT NULL,
|
|
69
|
-
state TEXT NOT NULL,
|
|
70
|
-
recipient_state TEXT,
|
|
71
|
-
created_at INTEGER NOT NULL,
|
|
72
|
-
acknowledged_at INTEGER
|
|
73
|
-
);
|
|
74
|
-
CREATE TABLE IF NOT EXISTS session_delivery_inbox (
|
|
75
|
-
delivery_id TEXT PRIMARY KEY,
|
|
76
|
-
sender_key TEXT NOT NULL,
|
|
77
|
-
recipient_key TEXT NOT NULL,
|
|
78
|
-
kind TEXT NOT NULL,
|
|
79
|
-
prompt TEXT NOT NULL,
|
|
80
|
-
delivery_mode TEXT NOT NULL,
|
|
81
|
-
metadata_json TEXT NOT NULL,
|
|
82
|
-
state TEXT NOT NULL,
|
|
83
|
-
created_at INTEGER NOT NULL,
|
|
84
|
-
updated_at INTEGER NOT NULL,
|
|
85
|
-
consumed_at INTEGER
|
|
86
|
-
);
|
|
87
|
-
`);
|
|
88
|
-
database.prepare("UPDATE session_delivery_inbox SET state = 'recovery_required', updated_at = ? WHERE state = 'admitting'").run(Date.now());
|
|
89
|
-
let closed = false;
|
|
90
|
-
const scheduled = /* @__PURE__ */ new Set();
|
|
91
|
-
let admissionQueue = Promise.resolve();
|
|
92
|
-
const publishAck = (deliveryId, senderKey, state) => {
|
|
93
|
-
options.communication.publish(senderKey, ACK_EVENT, {
|
|
94
|
-
deliveryId,
|
|
95
|
-
state
|
|
96
|
-
});
|
|
97
|
-
};
|
|
98
|
-
const readInboxRow = (deliveryId) => database.prepare("SELECT * FROM session_delivery_inbox WHERE delivery_id = ?").get(deliveryId);
|
|
99
|
-
const scheduleAdmission = (deliveryId) => {
|
|
100
|
-
if (closed || scheduled.has(deliveryId)) return;
|
|
101
|
-
scheduled.add(deliveryId);
|
|
102
|
-
admissionQueue = admissionQueue.then(async () => {
|
|
103
|
-
try {
|
|
104
|
-
if (closed) return;
|
|
105
|
-
const row = readInboxRow(deliveryId);
|
|
106
|
-
if (!row || row.state !== "accepted") return;
|
|
107
|
-
database.prepare("UPDATE session_delivery_inbox SET state = 'admitting', updated_at = ? WHERE delivery_id = ?").run(Date.now(), deliveryId);
|
|
108
|
-
publishAck(deliveryId, row.sender_key, "admitting");
|
|
109
|
-
try {
|
|
110
|
-
await options.admitPrompt(row.prompt, row.delivery_mode);
|
|
111
|
-
} catch {
|
|
112
|
-
database.prepare("UPDATE session_delivery_inbox SET state = 'recovery_required', updated_at = ? WHERE delivery_id = ?").run(Date.now(), deliveryId);
|
|
113
|
-
publishAck(deliveryId, row.sender_key, "recovery_required");
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
database.prepare("UPDATE session_delivery_inbox SET state = 'admitted', updated_at = ? WHERE delivery_id = ?").run(Date.now(), deliveryId);
|
|
117
|
-
publishAck(deliveryId, row.sender_key, "admitted");
|
|
118
|
-
} finally {
|
|
119
|
-
scheduled.delete(deliveryId);
|
|
120
|
-
}
|
|
121
|
-
}).catch(() => {
|
|
122
|
-
scheduled.delete(deliveryId);
|
|
123
|
-
try {
|
|
124
|
-
const row = readInboxRow(deliveryId);
|
|
125
|
-
if (row?.state !== "admitting") return;
|
|
126
|
-
database.prepare("UPDATE session_delivery_inbox SET state = 'recovery_required', updated_at = ? WHERE delivery_id = ?").run(Date.now(), deliveryId);
|
|
127
|
-
try {
|
|
128
|
-
publishAck(deliveryId, row.sender_key, "recovery_required");
|
|
129
|
-
} catch {}
|
|
130
|
-
} catch {}
|
|
131
|
-
});
|
|
132
|
-
};
|
|
133
|
-
const receiveEnvelope = (senderKey, value) => {
|
|
134
|
-
if (closed) return void 0;
|
|
135
|
-
const envelope = envelopeOf(value);
|
|
136
|
-
if (!envelope || !options.authorizePeer(senderKey, envelope.metadata)) return void 0;
|
|
137
|
-
database.prepare(`INSERT OR IGNORE INTO session_delivery_inbox
|
|
138
|
-
(delivery_id, sender_key, recipient_key, kind, prompt, delivery_mode, metadata_json, state, created_at, updated_at)
|
|
139
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, 'accepted', ?, ?)`).run(envelope.deliveryId, senderKey, options.recipientKey, envelope.kind, envelope.prompt, envelope.delivery, JSON.stringify(envelope.metadata), Date.now(), Date.now());
|
|
140
|
-
const current = readInboxRow(envelope.deliveryId);
|
|
141
|
-
if (!current) return void 0;
|
|
142
|
-
if (!(current.sender_key === senderKey && current.recipient_key === options.recipientKey && current.kind === envelope.kind && current.prompt === envelope.prompt && current.delivery_mode === envelope.delivery && sameMetadata(JSON.parse(current.metadata_json), envelope.metadata))) return void 0;
|
|
143
|
-
publishAck(envelope.deliveryId, current.sender_key, current.state);
|
|
144
|
-
if (current.state === "accepted") scheduleAdmission(envelope.deliveryId);
|
|
145
|
-
return current.state;
|
|
146
|
-
};
|
|
147
|
-
const receiveAck = (recipientKey, value) => {
|
|
148
|
-
const ack = ackOf(value);
|
|
149
|
-
if (closed || !ack) return;
|
|
150
|
-
database.prepare(`UPDATE session_delivery_outbox
|
|
151
|
-
SET state = 'acknowledged', recipient_state = ?, acknowledged_at = ?
|
|
152
|
-
WHERE delivery_id = ? AND recipient_key = ?`).run(ack.state, Date.now(), ack.deliveryId, recipientKey);
|
|
153
|
-
};
|
|
154
|
-
const publishOutbox = (deliveryId) => {
|
|
155
|
-
const row = database.prepare("SELECT * FROM session_delivery_outbox WHERE delivery_id = ? AND state = 'queued'").get(deliveryId);
|
|
156
|
-
if (!row) return;
|
|
157
|
-
const metadata = JSON.parse(row.metadata_json);
|
|
158
|
-
if (!options.authorizePeer(row.recipient_key, metadata)) return;
|
|
159
|
-
options.communication.publish(row.recipient_key, DELIVERY_EVENT, {
|
|
160
|
-
deliveryId: row.delivery_id,
|
|
161
|
-
kind: row.kind,
|
|
162
|
-
prompt: row.prompt,
|
|
163
|
-
delivery: row.delivery_mode,
|
|
164
|
-
metadata
|
|
165
|
-
});
|
|
166
|
-
};
|
|
167
|
-
const publishQueued = (recipientKey) => {
|
|
168
|
-
for (const row of database.prepare("SELECT delivery_id FROM session_delivery_outbox WHERE recipient_key = ? AND state = 'queued'").all(recipientKey)) publishOutbox(row.delivery_id);
|
|
169
|
-
};
|
|
170
|
-
const publishAllQueued = () => {
|
|
171
|
-
for (const row of database.prepare("SELECT delivery_id FROM session_delivery_outbox WHERE state = 'queued'").all()) publishOutbox(row.delivery_id);
|
|
172
|
-
};
|
|
173
|
-
const releaseEnvelope = options.communication.subscribe(DELIVERY_EVENT, receiveEnvelope);
|
|
174
|
-
const releaseAck = options.communication.subscribe(ACK_EVENT, receiveAck);
|
|
175
|
-
const releaseReady = options.communication.onPeerReady(publishQueued);
|
|
176
|
-
for (const row of database.prepare("SELECT delivery_id, sender_key FROM session_delivery_inbox WHERE state = 'recovery_required'").all()) publishAck(row.delivery_id, row.sender_key, "recovery_required");
|
|
177
|
-
for (const row of database.prepare("SELECT delivery_id FROM session_delivery_inbox WHERE state = 'accepted'").all()) scheduleAdmission(row.delivery_id);
|
|
178
|
-
publishAllQueued();
|
|
179
|
-
const retryTimer = setInterval(publishAllQueued, 1e3);
|
|
180
|
-
retryTimer.unref();
|
|
181
|
-
return Object.freeze({
|
|
182
|
-
recipientKey: options.recipientKey,
|
|
183
|
-
async deliver(request) {
|
|
184
|
-
if (closed) throw new Error("Session delivery service is closed.");
|
|
185
|
-
const deliveryId = request.deliveryId ?? crypto.randomUUID();
|
|
186
|
-
const mode = request.delivery ?? DEFAULT_DELIVERY;
|
|
187
|
-
if (!deliveryId.trim() || !request.recipientKey.trim() || !request.kind.trim() || !isMode(mode)) throw new TypeError("Delivery id, recipient key, kind, and mode must be valid.");
|
|
188
|
-
if (!options.authorizePeer(request.recipientKey, request.metadata)) throw new Error(`Session '${request.recipientKey}' is not an authorized communication peer.`);
|
|
189
|
-
const metadata = normalizedMetadata(request.metadata);
|
|
190
|
-
database.prepare(`INSERT OR IGNORE INTO session_delivery_outbox
|
|
191
|
-
(delivery_id, recipient_key, kind, prompt, delivery_mode, metadata_json, state, created_at)
|
|
192
|
-
VALUES (?, ?, ?, ?, ?, ?, 'queued', ?)`).run(deliveryId, request.recipientKey, request.kind, request.prompt, mode, JSON.stringify(metadata), Date.now());
|
|
193
|
-
const current = database.prepare("SELECT * FROM session_delivery_outbox WHERE delivery_id = ?").get(deliveryId);
|
|
194
|
-
if (!current || current.recipient_key !== request.recipientKey || current.kind !== request.kind || current.prompt !== request.prompt || current.delivery_mode !== mode || !sameMetadata(JSON.parse(current.metadata_json), metadata)) throw new TypeError(`Delivery id '${deliveryId}' is already bound to another envelope.`);
|
|
195
|
-
publishOutbox(deliveryId);
|
|
196
|
-
return { deliveryId };
|
|
197
|
-
},
|
|
198
|
-
async waitForAdmission(deliveryId, timeoutMs = 5e3) {
|
|
199
|
-
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
200
|
-
let state;
|
|
201
|
-
do {
|
|
202
|
-
state = database.prepare("SELECT recipient_state FROM session_delivery_outbox WHERE delivery_id = ?").get(deliveryId)?.recipient_state ?? void 0;
|
|
203
|
-
if (state === "admitted" || state === "recovery_required") return state;
|
|
204
|
-
if (Date.now() >= deadline) return state;
|
|
205
|
-
await new Promise((resolve) => setTimeout(resolve, Math.min(10, deadline - Date.now())));
|
|
206
|
-
} while (!closed);
|
|
207
|
-
return state;
|
|
208
|
-
},
|
|
209
|
-
receive(senderKey, kind, payload) {
|
|
210
|
-
if (kind === DELIVERY_EVENT) return receiveEnvelope(senderKey, payload);
|
|
211
|
-
if (kind === ACK_EVENT) {
|
|
212
|
-
receiveAck(senderKey, payload);
|
|
213
|
-
return "accepted";
|
|
214
|
-
}
|
|
215
|
-
},
|
|
216
|
-
inbox(query = {}) {
|
|
217
|
-
return database.prepare("SELECT * FROM session_delivery_inbox ORDER BY created_at, delivery_id").all().map((row) => {
|
|
218
|
-
const metadata = JSON.parse(row.metadata_json);
|
|
219
|
-
return {
|
|
220
|
-
deliveryId: row.delivery_id,
|
|
221
|
-
senderKey: row.sender_key,
|
|
222
|
-
recipientKey: row.recipient_key,
|
|
223
|
-
kind: row.kind,
|
|
224
|
-
prompt: row.prompt,
|
|
225
|
-
delivery: row.delivery_mode,
|
|
226
|
-
metadata,
|
|
227
|
-
state: row.state,
|
|
228
|
-
consumed: row.consumed_at !== null
|
|
229
|
-
};
|
|
230
|
-
}).filter((entry) => (query.kind === void 0 || entry.kind === query.kind) && (query.state === void 0 || entry.state === query.state) && (query.consumed === void 0 || entry.consumed === query.consumed) && matchesMetadata(entry.metadata, query.metadata));
|
|
231
|
-
},
|
|
232
|
-
consume(deliveryId) {
|
|
233
|
-
return database.prepare("UPDATE session_delivery_inbox SET consumed_at = ? WHERE delivery_id = ? AND state IN ('admitted', 'recovery_required') AND consumed_at IS NULL").run(Date.now(), deliveryId).changes === 1;
|
|
234
|
-
},
|
|
235
|
-
outbox(deliveryId) {
|
|
236
|
-
const row = database.prepare("SELECT delivery_id, recipient_key, kind, metadata_json, state, recipient_state FROM session_delivery_outbox WHERE delivery_id = ?").get(deliveryId);
|
|
237
|
-
if (!row) return void 0;
|
|
238
|
-
return {
|
|
239
|
-
deliveryId: row.delivery_id,
|
|
240
|
-
recipientKey: row.recipient_key,
|
|
241
|
-
kind: row.kind,
|
|
242
|
-
metadata: JSON.parse(row.metadata_json),
|
|
243
|
-
state: row.state,
|
|
244
|
-
...row.recipient_state === null ? {} : { recipientState: row.recipient_state }
|
|
245
|
-
};
|
|
246
|
-
},
|
|
247
|
-
async close() {
|
|
248
|
-
if (closed) return;
|
|
249
|
-
closed = true;
|
|
250
|
-
clearInterval(retryTimer);
|
|
251
|
-
releaseEnvelope();
|
|
252
|
-
releaseAck();
|
|
253
|
-
releaseReady();
|
|
254
|
-
try {
|
|
255
|
-
await admissionQueue;
|
|
256
|
-
} finally {
|
|
257
|
-
database.close();
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
/** Installs a provider-owned endpoint in the current Cordis context. */
|
|
263
|
-
function provideSessionDeliveryService(context, options) {
|
|
264
|
-
const service = createSessionDeliveryService(options);
|
|
265
|
-
const unprovide = context.provide(require_type.DOOM_SESSION_DELIVERY_SERVICE, service);
|
|
266
|
-
return async () => {
|
|
267
|
-
unprovide();
|
|
268
|
-
await service.close();
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
//#endregion
|
|
272
|
-
exports.DOOM_SESSION_DELIVERY_SERVICE = require_type.DOOM_SESSION_DELIVERY_SERVICE;
|
|
273
|
-
exports.createSessionDeliveryService = createSessionDeliveryService;
|
|
274
|
-
exports.provideSessionDeliveryService = provideSessionDeliveryService;
|
|
275
|
-
exports.readDoomSessionDelivery = require_type.readDoomSessionDelivery;
|
|
276
|
-
|
|
277
|
-
//# sourceMappingURL=index.cjs.map
|