@agimon-ai/doompi-core 0.0.1-alpha.76 → 0.0.1-alpha.78

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.
Files changed (205) hide show
  1. package/dist/headless-hub.cjs +107 -7
  2. package/dist/headless-hub.cjs.map +1 -1
  3. package/dist/headless-hub.d.cts +10 -0
  4. package/dist/headless-hub.d.cts.map +1 -1
  5. package/dist/headless-hub.d.mts +10 -0
  6. package/dist/headless-hub.d.mts.map +1 -1
  7. package/dist/headless-hub.mjs +107 -7
  8. package/dist/headless-hub.mjs.map +1 -1
  9. package/dist/headless-server.cjs +63 -6
  10. package/dist/headless-server.cjs.map +1 -1
  11. package/dist/headless-server.d.cts +2 -0
  12. package/dist/headless-server.d.cts.map +1 -1
  13. package/dist/headless-server.d.mts +2 -0
  14. package/dist/headless-server.d.mts.map +1 -1
  15. package/dist/headless-server.mjs +64 -7
  16. package/dist/headless-server.mjs.map +1 -1
  17. package/dist/headless-session-host.d.cts +5 -0
  18. package/dist/headless-session-host.d.cts.map +1 -1
  19. package/dist/headless-session-host.d.mts +5 -0
  20. package/dist/headless-session-host.d.mts.map +1 -1
  21. package/dist/headless.d.cts +2 -2
  22. package/dist/headless.d.mts +2 -2
  23. package/dist/history-import.cjs +1 -1
  24. package/dist/history-import.mjs +1 -1
  25. package/dist/history-ownership.cjs +1 -1
  26. package/dist/history-ownership.mjs +1 -1
  27. package/dist/history.cjs +2 -0
  28. package/dist/history.d.cts +2 -1
  29. package/dist/history.d.mts +2 -1
  30. package/dist/history.mjs +2 -1
  31. package/dist/hub-channel.d.cts +2 -2
  32. package/dist/hub-channel.d.mts +2 -2
  33. package/dist/jsonl-session-repo.cjs +1 -1
  34. package/dist/jsonl-session-repo.mjs +1 -1
  35. package/dist/mcp-facet.cjs +11 -0
  36. package/dist/mcp-facet.d.cts +4 -0
  37. package/dist/mcp-facet.d.mts +4 -0
  38. package/dist/mcp-facet.mjs +4 -0
  39. package/dist/mcp-schema-cost.cjs +2 -2
  40. package/dist/mcp-schema-cost.mjs +1 -1
  41. package/dist/package-api-server.cjs +13 -1
  42. package/dist/package-api-server.cjs.map +1 -1
  43. package/dist/package-api-server.mjs +13 -1
  44. package/dist/package-api-server.mjs.map +1 -1
  45. package/dist/remote-runtime.cjs +11 -3
  46. package/dist/remote-runtime.cjs.map +1 -1
  47. package/dist/remote-runtime.d.cts.map +1 -1
  48. package/dist/remote-runtime.d.mts.map +1 -1
  49. package/dist/remote-runtime.mjs +12 -4
  50. package/dist/remote-runtime.mjs.map +1 -1
  51. package/dist/runtime-cordis-host.cjs.map +1 -1
  52. package/dist/runtime-cordis-host.d.cts +1 -1
  53. package/dist/runtime-cordis-host.d.cts.map +1 -1
  54. package/dist/runtime-cordis-host.d.mts +1 -1
  55. package/dist/runtime-cordis-host.d.mts.map +1 -1
  56. package/dist/runtime-cordis-host.mjs.map +1 -1
  57. package/dist/runtime-identity.cjs +1 -1
  58. package/dist/runtime-identity.mjs +1 -1
  59. package/dist/runtime-json.cjs +1 -1
  60. package/dist/runtime-json.mjs +1 -1
  61. package/dist/runtime-pi-extension.cjs +6 -2
  62. package/dist/runtime-pi-extension.cjs.map +1 -1
  63. package/dist/runtime-pi-extension.d.cts.map +1 -1
  64. package/dist/runtime-pi-extension.d.mts.map +1 -1
  65. package/dist/runtime-pi-extension.mjs +6 -2
  66. package/dist/runtime-pi-extension.mjs.map +1 -1
  67. package/dist/schemas/builtinApiContracts.cjs +1 -1
  68. package/dist/schemas/builtinApiContracts.cjs.map +1 -1
  69. package/dist/schemas/builtinApiContracts.mjs +1 -1
  70. package/dist/schemas/builtinApiContracts.mjs.map +1 -1
  71. package/dist/schemas/headless.cjs.map +1 -1
  72. package/dist/schemas/headless.d.cts +10 -0
  73. package/dist/schemas/headless.d.cts.map +1 -1
  74. package/dist/schemas/headless.d.mts +10 -0
  75. package/dist/schemas/headless.d.mts.map +1 -1
  76. package/dist/schemas/headless.mjs.map +1 -1
  77. package/dist/schemas/httpApiContracts.cjs +27 -3
  78. package/dist/schemas/httpApiContracts.cjs.map +1 -1
  79. package/dist/schemas/httpApiContracts.mjs +25 -4
  80. package/dist/schemas/httpApiContracts.mjs.map +1 -1
  81. package/dist/schemas/hubChannel.d.cts +12 -0
  82. package/dist/schemas/hubChannel.d.cts.map +1 -1
  83. package/dist/schemas/hubChannel.d.mts +12 -0
  84. package/dist/schemas/hubChannel.d.mts.map +1 -1
  85. package/dist/schemas/mcpBundle.cjs +67 -0
  86. package/dist/schemas/mcpBundle.cjs.map +1 -0
  87. package/dist/schemas/mcpBundle.d.cts +21 -0
  88. package/dist/schemas/mcpBundle.d.cts.map +1 -0
  89. package/dist/schemas/mcpBundle.d.mts +21 -0
  90. package/dist/schemas/mcpBundle.d.mts.map +1 -0
  91. package/dist/schemas/mcpBundle.mjs +65 -0
  92. package/dist/schemas/mcpBundle.mjs.map +1 -0
  93. package/dist/schemas/mcpFacet.cjs +19 -0
  94. package/dist/schemas/mcpFacet.cjs.map +1 -0
  95. package/dist/schemas/mcpFacet.d.cts +66 -0
  96. package/dist/schemas/mcpFacet.d.cts.map +1 -0
  97. package/dist/schemas/mcpFacet.d.mts +66 -0
  98. package/dist/schemas/mcpFacet.d.mts.map +1 -0
  99. package/dist/schemas/mcpFacet.mjs +17 -0
  100. package/dist/schemas/mcpFacet.mjs.map +1 -0
  101. package/dist/schemas/packageApi.cjs.map +1 -1
  102. package/dist/schemas/packageApi.d.cts +3 -1
  103. package/dist/schemas/packageApi.d.cts.map +1 -1
  104. package/dist/schemas/packageApi.d.mts +3 -1
  105. package/dist/schemas/packageApi.d.mts.map +1 -1
  106. package/dist/schemas/packageApi.mjs.map +1 -1
  107. package/dist/schemas/sessionApiContracts.cjs +3 -0
  108. package/dist/schemas/sessionApiContracts.cjs.map +1 -1
  109. package/dist/schemas/sessionApiContracts.d.cts.map +1 -1
  110. package/dist/schemas/sessionApiContracts.d.mts.map +1 -1
  111. package/dist/schemas/sessionApiContracts.mjs +3 -0
  112. package/dist/schemas/sessionApiContracts.mjs.map +1 -1
  113. package/dist/server/headlessProtocol.cjs +26 -0
  114. package/dist/server/headlessProtocol.cjs.map +1 -1
  115. package/dist/server/headlessProtocol.mjs +26 -0
  116. package/dist/server/headlessProtocol.mjs.map +1 -1
  117. package/dist/server/mcpBundleLoader.cjs +56 -0
  118. package/dist/server/mcpBundleLoader.cjs.map +1 -0
  119. package/dist/server/mcpBundleLoader.d.cts +27 -0
  120. package/dist/server/mcpBundleLoader.d.cts.map +1 -0
  121. package/dist/server/mcpBundleLoader.d.mts +27 -0
  122. package/dist/server/mcpBundleLoader.d.mts.map +1 -0
  123. package/dist/server/mcpBundleLoader.mjs +52 -0
  124. package/dist/server/mcpBundleLoader.mjs.map +1 -0
  125. package/dist/server/sessionMcpHandler.cjs +75 -16
  126. package/dist/server/sessionMcpHandler.cjs.map +1 -1
  127. package/dist/server/sessionMcpHandler.d.cts.map +1 -1
  128. package/dist/server/sessionMcpHandler.d.mts.map +1 -1
  129. package/dist/server/sessionMcpHandler.mjs +76 -17
  130. package/dist/server/sessionMcpHandler.mjs.map +1 -1
  131. package/dist/server/sessionMcpRoutes.cjs +14 -7
  132. package/dist/server/sessionMcpRoutes.cjs.map +1 -1
  133. package/dist/server/sessionMcpRoutes.mjs +14 -7
  134. package/dist/server/sessionMcpRoutes.mjs.map +1 -1
  135. package/dist/services/historyCreationFileSystem/index.cjs +3 -2
  136. package/dist/services/historyCreationFileSystem/index.cjs.map +1 -1
  137. package/dist/services/historyCreationFileSystem/index.mjs +3 -2
  138. package/dist/services/historyCreationFileSystem/index.mjs.map +1 -1
  139. package/dist/services/piExtensionHost/index.cjs +19 -7
  140. package/dist/services/piExtensionHost/index.cjs.map +1 -1
  141. package/dist/services/piExtensionHost/index.mjs +19 -7
  142. package/dist/services/piExtensionHost/index.mjs.map +1 -1
  143. package/dist/services/pluginLifecycle/index.cjs +1 -1
  144. package/dist/services/pluginLifecycle/index.cjs.map +1 -1
  145. package/dist/services/pluginLifecycle/index.mjs +1 -1
  146. package/dist/services/pluginLifecycle/index.mjs.map +1 -1
  147. package/dist/services/webauthnPolicy/index.cjs +1 -1
  148. package/dist/services/webauthnPolicy/index.cjs.map +1 -1
  149. package/dist/services/webauthnPolicy/index.mjs +1 -1
  150. package/dist/services/webauthnPolicy/index.mjs.map +1 -1
  151. package/dist/services/workspaceRegistry/index.cjs +90 -0
  152. package/dist/services/workspaceRegistry/index.cjs.map +1 -0
  153. package/dist/services/workspaceRegistry/index.d.cts +18 -0
  154. package/dist/services/workspaceRegistry/index.d.cts.map +1 -0
  155. package/dist/services/workspaceRegistry/index.d.mts +18 -0
  156. package/dist/services/workspaceRegistry/index.d.mts.map +1 -0
  157. package/dist/services/workspaceRegistry/index.mjs +87 -0
  158. package/dist/services/workspaceRegistry/index.mjs.map +1 -0
  159. package/dist/sqlite-session-storage.cjs +1 -1
  160. package/dist/sqlite-session-storage.mjs +1 -1
  161. package/dist/sync-location.cjs +2 -2
  162. package/dist/sync-location.mjs +1 -1
  163. package/dist/sync-registration.cjs +19 -5
  164. package/dist/sync-registration.cjs.map +1 -1
  165. package/dist/sync-registration.d.cts +3 -0
  166. package/dist/sync-registration.d.cts.map +1 -1
  167. package/dist/sync-registration.d.mts +3 -0
  168. package/dist/sync-registration.d.mts.map +1 -1
  169. package/dist/sync-registration.mjs +18 -4
  170. package/dist/sync-registration.mjs.map +1 -1
  171. package/dist/systems/child/adapters/headlessChildSessionService.cjs +1 -1
  172. package/dist/systems/child/adapters/headlessChildSessionService.mjs +1 -1
  173. package/dist/systems/main/adapters/headlessHost.cjs +3 -1
  174. package/dist/systems/main/adapters/headlessHost.cjs.map +1 -1
  175. package/dist/systems/main/adapters/headlessHost.d.cts.map +1 -1
  176. package/dist/systems/main/adapters/headlessHost.d.mts.map +1 -1
  177. package/dist/systems/main/adapters/headlessHost.mjs +3 -1
  178. package/dist/systems/main/adapters/headlessHost.mjs.map +1 -1
  179. package/dist/systems/main/adapters/headlessSessionHost.cjs +248 -89
  180. package/dist/systems/main/adapters/headlessSessionHost.cjs.map +1 -1
  181. package/dist/systems/main/adapters/headlessSessionHost.d.cts.map +1 -1
  182. package/dist/systems/main/adapters/headlessSessionHost.d.mts.map +1 -1
  183. package/dist/systems/main/adapters/headlessSessionHost.mjs +247 -89
  184. package/dist/systems/main/adapters/headlessSessionHost.mjs.map +1 -1
  185. package/dist/terminal-pi-child-session-service.cjs +1 -1
  186. package/dist/terminal-pi-child-session-service.mjs +1 -1
  187. package/dist/testing/pi/testHost.cjs +6 -0
  188. package/dist/testing/pi/testHost.cjs.map +1 -1
  189. package/dist/testing/pi/testHost.mjs +6 -0
  190. package/dist/testing/pi/testHost.mjs.map +1 -1
  191. package/dist/types/server/sessionToolSurface.d.cts +5 -2
  192. package/dist/types/server/sessionToolSurface.d.cts.map +1 -1
  193. package/dist/types/server/sessionToolSurface.d.mts +5 -2
  194. package/dist/types/server/sessionToolSurface.d.mts.map +1 -1
  195. package/dist/v3-export.cjs +1 -1
  196. package/dist/v3-export.mjs +1 -1
  197. package/dist/web/types/webPlugin.d.cts +8 -0
  198. package/dist/web/types/webPlugin.d.cts.map +1 -1
  199. package/dist/web/types/webPlugin.d.mts +8 -0
  200. package/dist/web/types/webPlugin.d.mts.map +1 -1
  201. package/dist/web-compositions.cjs +2 -2
  202. package/dist/web-compositions.mjs +1 -1
  203. package/dist/web.d.cts +2 -2
  204. package/dist/web.d.mts +2 -2
  205. package/package.json +19 -14
@@ -56,6 +56,14 @@ async function serveSessionApis(options) {
56
56
  request: async () => Response.json({ error: "No package APIs are mounted." }, { status: 404 }),
57
57
  close: () => Promise.resolve()
58
58
  };
59
+ const owningSessionService = options.sessionService;
60
+ const communication = owningSessionService?.bindCommunication?.(options.sessionId);
61
+ const exposedSessionService = owningSessionService === void 0 ? void 0 : {
62
+ create: (request) => owningSessionService.create(request),
63
+ close: (sessionId) => owningSessionService.close(sessionId),
64
+ isLive: (sessionId) => owningSessionService.isLive(sessionId),
65
+ ...owningSessionService.canCommunicate === void 0 ? {} : { canCommunicate: (sourceSessionId, targetSessionId) => owningSessionService.canCommunicate?.(sourceSessionId, targetSessionId) === true }
66
+ };
59
67
  const context = {
60
68
  scope: "session",
61
69
  sessionId: options.sessionId,
@@ -67,7 +75,8 @@ async function serveSessionApis(options) {
67
75
  directEvents: options.directEvents,
68
76
  ...options.internalToken === void 0 ? {} : { internalToken: options.internalToken },
69
77
  ...options.hubToken === void 0 ? {} : { hubToken: options.hubToken },
70
- ...options.sessionService === void 0 ? {} : { sessionService: options.sessionService },
78
+ ...exposedSessionService === void 0 ? {} : { sessionService: exposedSessionService },
79
+ ...communication === void 0 ? {} : { sessionCommunication: communication },
71
80
  onNotice: options.onNotice
72
81
  };
73
82
  const host = require_index.createDoomServerHost({
@@ -92,11 +101,13 @@ async function serveSessionApis(options) {
92
101
  throw error;
93
102
  }
94
103
  } catch (error) {
104
+ communication?.close();
95
105
  host.dispose();
96
106
  throw error;
97
107
  }
98
108
  if (host.mounted().length === 0 && !options.prepareFacets) {
99
109
  await installed.dispose();
110
+ communication?.close();
100
111
  host.dispose();
101
112
  return {
102
113
  request: async () => Response.json({ error: "No package APIs are mounted." }, { status: 404 }),
@@ -149,6 +160,7 @@ async function serveSessionApis(options) {
149
160
  close: () => {
150
161
  closePromise ??= (async () => {
151
162
  await installed.dispose();
163
+ communication?.close();
152
164
  host.dispose();
153
165
  })();
154
166
  return closePromise;
@@ -1 +1 @@
1
- {"version":3,"file":"package-api-server.cjs","names":["createDoomServerHost","installServerFacets","DOOM_API_ROUTE_PREFIX","validatedTraceContext"],"sources":["../src/server/packageApiServer.ts"],"sourcesContent":["import type { DoomTraceContext } from '@agimon-ai/doompi-telemetry';\n\nimport type { DoomDirectEventBus, DoomHubSessionService } from '../exports/hubChannel';\nimport { DOOM_API_ROUTE_PREFIX, type DoomApi, type DoomApiContext } from '../exports/packageApi';\nimport type { DoomPluginRegistry } from '../exports/pluginProtocol';\nimport { createDoomServerHost, type CreateDoomServerHostOptions, type DoomServerFacet } from '../exports/serverFacet';\nimport {\n installServerFacets,\n type LoadedServerFacet,\n type InstalledServerFacets,\n type InstallServerFacetsOptions,\n} from '../exports/serverFacet';\nimport { observe, type ServerTelemetry } from '../services/serverTelemetry';\nimport { validatedTraceContext } from '../services/traceContext';\n\n/**\n * A body that keeps streaming well after its headers went out is worth its own span. A fast\n * one is not: the request span already carries the duration and outcome, so emitting a second\n * span per call made package API traffic 99.65% of all spans recorded in a two hour sample.\n */\nexport const COMPLETION_SPAN_MIN_DURATION_MS = 1000;\n\nexport interface PackageApiServerOptions {\n sessionId: string;\n workspaceId?: string;\n workspaceRoot?: string;\n homeDirectory?: string;\n mountChannel?: CreateDoomServerHostOptions['mountChannel'];\n cwd: string;\n /** Admitted environment for APIs serving this session. */\n readonly environment: Readonly<Record<string, string | undefined>>;\n /** Lifecycle-owned direct events shared with the hub. */\n readonly directEvents: DoomDirectEventBus;\n internalToken?: string;\n hubToken?: string;\n /** Shared exact-scope dispatch table, so session facets can mount plugin methods. */\n pluginRegistry?: DoomPluginRegistry;\n sessionService?: DoomHubSessionService;\n apis: readonly DoomApi[];\n /** Server facets to install; each registers its own APIs through the host. */\n facets?: readonly (DoomServerFacet | LoadedServerFacet)[];\n prepareFacets?: InstallServerFacetsOptions['prepare'];\n activateFacets?: (installed: InstalledServerFacets) => Promise<void>;\n canDispatch?: () => boolean;\n telemetry?: ServerTelemetry;\n onNotice: (message: string) => void;\n}\n\nexport interface PackageApiServer {\n /** Dispatches an already-authenticated request without crossing a transport boundary. */\n readonly request: (request: Request) => Promise<Response>;\n close(): Promise<void>;\n}\n\nfunction describeError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction responseWithCompletion(response: Response, complete: () => void): Response {\n if (response.body === null) {\n complete();\n return response;\n }\n const reader = response.body.getReader();\n let completed = false;\n const finish = (): void => {\n if (completed) return;\n completed = true;\n complete();\n };\n const body = new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n const result = await reader.read();\n if (result.done) {\n finish();\n controller.close();\n } else controller.enqueue(result.value);\n } catch (error) {\n finish();\n controller.error(error);\n }\n },\n async cancel(reason) {\n finish();\n await reader.cancel(reason);\n },\n });\n return new Response(body, response);\n}\n\n/** Installs session facets and dispatches authenticated package API calls in-process. */\nexport async function serveSessionApis(options: PackageApiServerOptions): Promise<PackageApiServer> {\n if (options.environment === undefined)\n throw new Error('Session package API server requires an admitted environment.');\n if (options.directEvents === undefined) throw new Error('Session package API server requires direct events.');\n const facets = options.facets ?? [];\n if (options.apis.length === 0 && facets.length === 0 && !options.prepareFacets) {\n return {\n request: async () => Response.json({ error: 'No package APIs are mounted.' }, { status: 404 }),\n close: () => Promise.resolve(),\n };\n }\n\n const context: DoomApiContext = {\n scope: 'session',\n sessionId: options.sessionId,\n workspaceId: options.workspaceId,\n workspaceRoot: options.workspaceRoot,\n homeDirectory: options.homeDirectory,\n cwd: options.cwd,\n environment: options.environment,\n directEvents: options.directEvents,\n ...(options.internalToken === undefined ? {} : { internalToken: options.internalToken }),\n ...(options.hubToken === undefined ? {} : { hubToken: options.hubToken }),\n ...(options.sessionService === undefined ? {} : { sessionService: options.sessionService }),\n onNotice: options.onNotice,\n };\n const host = createDoomServerHost({\n scope: 'session',\n context,\n mountChannel: options.mountChannel,\n ...(options.pluginRegistry === undefined ? {} : { pluginRegistry: options.pluginRegistry }),\n });\n for (const api of options.apis) host.registerApi(api);\n let installed: InstalledServerFacets;\n try {\n installed = await installServerFacets({ host, facets, onNotice: options.onNotice, prepare: options.prepareFacets });\n try {\n await options.activateFacets?.(installed);\n } catch (error) {\n await installed.dispose();\n throw error;\n }\n } catch (error) {\n host.dispose();\n throw error;\n }\n if (host.mounted().length === 0 && !options.prepareFacets) {\n await installed.dispose();\n host.dispose();\n return {\n request: async () => Response.json({ error: 'No package APIs are mounted.' }, { status: 404 }),\n close: () => Promise.resolve(),\n };\n }\n\n const dispatch = async (request: Request): Promise<Response> => {\n if (options.canDispatch?.() === false)\n return Response.json({ error: 'Session selection is not ready.' }, { status: 503 });\n const url = new URL(request.url);\n if (!url.pathname.startsWith(`${DOOM_API_ROUTE_PREFIX}/`))\n return Response.json({ error: 'Not found.' }, { status: 404 });\n const rest = url.pathname.slice(DOOM_API_ROUTE_PREFIX.length + 1);\n const slash = rest.indexOf('/');\n const basePath = slash === -1 ? rest : rest.slice(0, slash);\n const handler = host.handlerFor(basePath);\n if (handler === undefined)\n return Response.json({ error: `No API '${basePath}' in this session.` }, { status: 404 });\n url.pathname = slash === -1 ? '/' : rest.slice(slash);\n const startedAt = performance.now();\n const parent = validatedTraceContext(request.headers.get('traceparent'));\n let childContext: DoomTraceContext | undefined;\n let response: Response;\n try {\n const invoke = async (context?: DoomTraceContext): Promise<Response> => {\n if (options.canDispatch?.() === false || host.handlerFor(basePath) !== handler) {\n return Response.json({ error: 'Session selection changed before dispatch.' }, { status: 503 });\n }\n childContext = context;\n return handler.fetch(new Request(url, request));\n };\n response = options.telemetry\n ? await options.telemetry.runInSpan(\n 'doompi_server.package_api.request',\n { api: basePath, method: request.method },\n invoke,\n parent,\n )\n : await invoke();\n } catch (error) {\n options.onNotice(`package API '${basePath}' failed on ${url.pathname} (${describeError(error)})`);\n response = Response.json({ error: `The '${basePath}' API failed.` }, { status: 500 });\n }\n return responseWithCompletion(response, () => {\n if (!options.telemetry) return;\n const durationMs = Math.round(performance.now() - startedAt);\n // Keep this span only where it says something the request span cannot: a failing status,\n // or a body that took real time to flush after the headers were sent.\n if (response.status < 400 && durationMs < COMPLETION_SPAN_MIN_DURATION_MS) return;\n observe(\n options.telemetry.runInSpan(\n 'doompi_server.package_api.complete',\n {\n api: basePath,\n method: request.method,\n status_code: response.status,\n duration_ms: durationMs,\n },\n async () => undefined,\n childContext ?? parent,\n ),\n options.onNotice,\n );\n });\n };\n let closePromise: Promise<void> | undefined;\n\n return {\n request: dispatch,\n close: () => {\n closePromise ??= (async () => {\n await installed.dispose();\n host.dispose();\n })();\n return closePromise;\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;AAoBA,MAAa,kCAAkC;AAkC/C,SAAS,cAAc,OAAwB;CAC7C,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC9D;AAEA,SAAS,uBAAuB,UAAoB,UAAgC;CAClF,IAAI,SAAS,SAAS,MAAM;EAC1B,SAAS;EACT,OAAO;CACT;CACA,MAAM,SAAS,SAAS,KAAK,UAAU;CACvC,IAAI,YAAY;CAChB,MAAM,eAAqB;EACzB,IAAI,WAAW;EACf,YAAY;EACZ,SAAS;CACX;CACA,MAAM,OAAO,IAAI,eAA2B;EAC1C,MAAM,KAAK,YAAY;GACrB,IAAI;IACF,MAAM,SAAS,MAAM,OAAO,KAAK;IACjC,IAAI,OAAO,MAAM;KACf,OAAO;KACP,WAAW,MAAM;IACnB,OAAO,WAAW,QAAQ,OAAO,KAAK;GACxC,SAAS,OAAO;IACd,OAAO;IACP,WAAW,MAAM,KAAK;GACxB;EACF;EACA,MAAM,OAAO,QAAQ;GACnB,OAAO;GACP,MAAM,OAAO,OAAO,MAAM;EAC5B;CACF,CAAC;CACD,OAAO,IAAI,SAAS,MAAM,QAAQ;AACpC;;AAGA,eAAsB,iBAAiB,SAA6D;CAClG,IAAI,QAAQ,gBAAgB,KAAA,GAC1B,MAAM,IAAI,MAAM,8DAA8D;CAChF,IAAI,QAAQ,iBAAiB,KAAA,GAAW,MAAM,IAAI,MAAM,oDAAoD;CAC5G,MAAM,SAAS,QAAQ,UAAU,CAAC;CAClC,IAAI,QAAQ,KAAK,WAAW,KAAK,OAAO,WAAW,KAAK,CAAC,QAAQ,eAC/D,OAAO;EACL,SAAS,YAAY,SAAS,KAAK,EAAE,OAAO,+BAA+B,GAAG,EAAE,QAAQ,IAAI,CAAC;EAC7F,aAAa,QAAQ,QAAQ;CAC/B;CAGF,MAAM,UAA0B;EAC9B,OAAO;EACP,WAAW,QAAQ;EACnB,aAAa,QAAQ;EACrB,eAAe,QAAQ;EACvB,eAAe,QAAQ;EACvB,KAAK,QAAQ;EACb,aAAa,QAAQ;EACrB,cAAc,QAAQ;EACtB,GAAI,QAAQ,kBAAkB,KAAA,IAAY,CAAC,IAAI,EAAE,eAAe,QAAQ,cAAc;EACtF,GAAI,QAAQ,aAAa,KAAA,IAAY,CAAC,IAAI,EAAE,UAAU,QAAQ,SAAS;EACvE,GAAI,QAAQ,mBAAmB,KAAA,IAAY,CAAC,IAAI,EAAE,gBAAgB,QAAQ,eAAe;EACzF,UAAU,QAAQ;CACpB;CACA,MAAM,OAAOA,cAAAA,qBAAqB;EAChC,OAAO;EACP;EACA,cAAc,QAAQ;EACtB,GAAI,QAAQ,mBAAmB,KAAA,IAAY,CAAC,IAAI,EAAE,gBAAgB,QAAQ,eAAe;CAC3F,CAAC;CACD,KAAK,MAAM,OAAO,QAAQ,MAAM,KAAK,YAAY,GAAG;CACpD,IAAI;CACJ,IAAI;EACF,YAAY,MAAMC,0BAAAA,oBAAoB;GAAE;GAAM;GAAQ,UAAU,QAAQ;GAAU,SAAS,QAAQ;EAAc,CAAC;EAClH,IAAI;GACF,MAAM,QAAQ,iBAAiB,SAAS;EAC1C,SAAS,OAAO;GACd,MAAM,UAAU,QAAQ;GACxB,MAAM;EACR;CACF,SAAS,OAAO;EACd,KAAK,QAAQ;EACb,MAAM;CACR;CACA,IAAI,KAAK,QAAQ,CAAC,CAAC,WAAW,KAAK,CAAC,QAAQ,eAAe;EACzD,MAAM,UAAU,QAAQ;EACxB,KAAK,QAAQ;EACb,OAAO;GACL,SAAS,YAAY,SAAS,KAAK,EAAE,OAAO,+BAA+B,GAAG,EAAE,QAAQ,IAAI,CAAC;GAC7F,aAAa,QAAQ,QAAQ;EAC/B;CACF;CAEA,MAAM,WAAW,OAAO,YAAwC;EAC9D,IAAI,QAAQ,cAAc,MAAM,OAC9B,OAAO,SAAS,KAAK,EAAE,OAAO,kCAAkC,GAAG,EAAE,QAAQ,IAAI,CAAC;EACpF,MAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;EAC/B,IAAI,CAAC,IAAI,SAAS,WAAW,eAA2B,GACtD,OAAO,SAAS,KAAK,EAAE,OAAO,aAAa,GAAG,EAAE,QAAQ,IAAI,CAAC;EAC/D,MAAM,OAAO,IAAI,SAAS,MAAMC,mBAAAA,sBAAsB,SAAS,CAAC;EAChE,MAAM,QAAQ,KAAK,QAAQ,GAAG;EAC9B,MAAM,WAAW,UAAU,KAAK,OAAO,KAAK,MAAM,GAAG,KAAK;EAC1D,MAAM,UAAU,KAAK,WAAW,QAAQ;EACxC,IAAI,YAAY,KAAA,GACd,OAAO,SAAS,KAAK,EAAE,OAAO,WAAW,SAAS,oBAAoB,GAAG,EAAE,QAAQ,IAAI,CAAC;EAC1F,IAAI,WAAW,UAAU,KAAK,MAAM,KAAK,MAAM,KAAK;EACpD,MAAM,YAAY,YAAY,IAAI;EAClC,MAAM,SAASC,gBAAAA,sBAAsB,QAAQ,QAAQ,IAAI,aAAa,CAAC;EACvE,IAAI;EACJ,IAAI;EACJ,IAAI;GACF,MAAM,SAAS,OAAO,YAAkD;IACtE,IAAI,QAAQ,cAAc,MAAM,SAAS,KAAK,WAAW,QAAQ,MAAM,SACrE,OAAO,SAAS,KAAK,EAAE,OAAO,6CAA6C,GAAG,EAAE,QAAQ,IAAI,CAAC;IAE/F,eAAe;IACf,OAAO,QAAQ,MAAM,IAAI,QAAQ,KAAK,OAAO,CAAC;GAChD;GACA,WAAW,QAAQ,YACf,MAAM,QAAQ,UAAU,UACtB,qCACA;IAAE,KAAK;IAAU,QAAQ,QAAQ;GAAO,GACxC,QACA,MACF,IACA,MAAM,OAAO;EACnB,SAAS,OAAO;GACd,QAAQ,SAAS,gBAAgB,SAAS,cAAc,IAAI,SAAS,IAAI,cAAc,KAAK,EAAE,EAAE;GAChG,WAAW,SAAS,KAAK,EAAE,OAAO,QAAQ,SAAS,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;EACtF;EACA,OAAO,uBAAuB,gBAAgB;GAC5C,IAAI,CAAC,QAAQ,WAAW;GACxB,MAAM,aAAa,KAAK,MAAM,YAAY,IAAI,IAAI,SAAS;GAG3D,IAAI,SAAS,SAAS,OAAO,aAAA,KAA8C;GAC3E,yBAAA,QACE,QAAQ,UAAU,UAChB,sCACA;IACE,KAAK;IACL,QAAQ,QAAQ;IAChB,aAAa,SAAS;IACtB,aAAa;GACf,GACA,YAAY,KAAA,GACZ,gBAAgB,MAClB,GACA,QAAQ,QACV;EACF,CAAC;CACH;CACA,IAAI;CAEJ,OAAO;EACL,SAAS;EACT,aAAa;GACX,kBAAkB,YAAY;IAC5B,MAAM,UAAU,QAAQ;IACxB,KAAK,QAAQ;GACf,EAAA,CAAG;GACH,OAAO;EACT;CACF;AACF"}
1
+ {"version":3,"file":"package-api-server.cjs","names":["createDoomServerHost","installServerFacets","DOOM_API_ROUTE_PREFIX","validatedTraceContext"],"sources":["../src/server/packageApiServer.ts"],"sourcesContent":["import type { DoomTraceContext } from '@agimon-ai/doompi-telemetry';\n\nimport type { DoomDirectEventBus, DoomHubSessionService } from '../exports/hubChannel';\nimport { DOOM_API_ROUTE_PREFIX, type DoomApi, type DoomApiContext } from '../exports/packageApi';\nimport type { DoomPluginRegistry } from '../exports/pluginProtocol';\nimport { createDoomServerHost, type CreateDoomServerHostOptions, type DoomServerFacet } from '../exports/serverFacet';\nimport {\n installServerFacets,\n type LoadedServerFacet,\n type InstalledServerFacets,\n type InstallServerFacetsOptions,\n} from '../exports/serverFacet';\nimport { observe, type ServerTelemetry } from '../services/serverTelemetry';\nimport { validatedTraceContext } from '../services/traceContext';\n\n/**\n * A body that keeps streaming well after its headers went out is worth its own span. A fast\n * one is not: the request span already carries the duration and outcome, so emitting a second\n * span per call made package API traffic 99.65% of all spans recorded in a two hour sample.\n */\nexport const COMPLETION_SPAN_MIN_DURATION_MS = 1000;\n\nexport interface PackageApiServerOptions {\n sessionId: string;\n workspaceId?: string;\n workspaceRoot?: string;\n homeDirectory?: string;\n mountChannel?: CreateDoomServerHostOptions['mountChannel'];\n cwd: string;\n /** Admitted environment for APIs serving this session. */\n readonly environment: Readonly<Record<string, string | undefined>>;\n /** Lifecycle-owned direct events shared with the hub. */\n readonly directEvents: DoomDirectEventBus;\n internalToken?: string;\n hubToken?: string;\n /** Shared exact-scope dispatch table, so session facets can mount plugin methods. */\n pluginRegistry?: DoomPluginRegistry;\n sessionService?: DoomHubSessionService;\n apis: readonly DoomApi[];\n /** Server facets to install; each registers its own APIs through the host. */\n facets?: readonly (DoomServerFacet | LoadedServerFacet)[];\n prepareFacets?: InstallServerFacetsOptions['prepare'];\n activateFacets?: (installed: InstalledServerFacets) => Promise<void>;\n canDispatch?: () => boolean;\n telemetry?: ServerTelemetry;\n onNotice: (message: string) => void;\n}\n\nexport interface PackageApiServer {\n /** Dispatches an already-authenticated request without crossing a transport boundary. */\n readonly request: (request: Request) => Promise<Response>;\n close(): Promise<void>;\n}\n\nfunction describeError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction responseWithCompletion(response: Response, complete: () => void): Response {\n if (response.body === null) {\n complete();\n return response;\n }\n const reader = response.body.getReader();\n let completed = false;\n const finish = (): void => {\n if (completed) return;\n completed = true;\n complete();\n };\n const body = new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n const result = await reader.read();\n if (result.done) {\n finish();\n controller.close();\n } else controller.enqueue(result.value);\n } catch (error) {\n finish();\n controller.error(error);\n }\n },\n async cancel(reason) {\n finish();\n await reader.cancel(reason);\n },\n });\n return new Response(body, response);\n}\n\n/** Installs session facets and dispatches authenticated package API calls in-process. */\nexport async function serveSessionApis(options: PackageApiServerOptions): Promise<PackageApiServer> {\n if (options.environment === undefined)\n throw new Error('Session package API server requires an admitted environment.');\n if (options.directEvents === undefined) throw new Error('Session package API server requires direct events.');\n const facets = options.facets ?? [];\n if (options.apis.length === 0 && facets.length === 0 && !options.prepareFacets) {\n return {\n request: async () => Response.json({ error: 'No package APIs are mounted.' }, { status: 404 }),\n close: () => Promise.resolve(),\n };\n }\n\n const owningSessionService = options.sessionService;\n const communication = owningSessionService?.bindCommunication?.(options.sessionId);\n const exposedSessionService: DoomHubSessionService | undefined =\n owningSessionService === undefined\n ? undefined\n : {\n create: (request) => owningSessionService.create(request),\n close: (sessionId) => owningSessionService.close(sessionId),\n isLive: (sessionId) => owningSessionService.isLive(sessionId),\n ...(owningSessionService.canCommunicate === undefined\n ? {}\n : {\n canCommunicate: (sourceSessionId: string, targetSessionId: string) =>\n owningSessionService.canCommunicate?.(sourceSessionId, targetSessionId) === true,\n }),\n };\n const context: DoomApiContext = {\n scope: 'session',\n sessionId: options.sessionId,\n workspaceId: options.workspaceId,\n workspaceRoot: options.workspaceRoot,\n homeDirectory: options.homeDirectory,\n cwd: options.cwd,\n environment: options.environment,\n directEvents: options.directEvents,\n ...(options.internalToken === undefined ? {} : { internalToken: options.internalToken }),\n ...(options.hubToken === undefined ? {} : { hubToken: options.hubToken }),\n ...(exposedSessionService === undefined ? {} : { sessionService: exposedSessionService }),\n ...(communication === undefined ? {} : { sessionCommunication: communication }),\n onNotice: options.onNotice,\n };\n const host = createDoomServerHost({\n scope: 'session',\n context,\n mountChannel: options.mountChannel,\n ...(options.pluginRegistry === undefined ? {} : { pluginRegistry: options.pluginRegistry }),\n });\n for (const api of options.apis) host.registerApi(api);\n let installed: InstalledServerFacets;\n try {\n installed = await installServerFacets({ host, facets, onNotice: options.onNotice, prepare: options.prepareFacets });\n try {\n await options.activateFacets?.(installed);\n } catch (error) {\n await installed.dispose();\n throw error;\n }\n } catch (error) {\n communication?.close();\n host.dispose();\n throw error;\n }\n if (host.mounted().length === 0 && !options.prepareFacets) {\n await installed.dispose();\n communication?.close();\n host.dispose();\n return {\n request: async () => Response.json({ error: 'No package APIs are mounted.' }, { status: 404 }),\n close: () => Promise.resolve(),\n };\n }\n\n const dispatch = async (request: Request): Promise<Response> => {\n if (options.canDispatch?.() === false)\n return Response.json({ error: 'Session selection is not ready.' }, { status: 503 });\n const url = new URL(request.url);\n if (!url.pathname.startsWith(`${DOOM_API_ROUTE_PREFIX}/`))\n return Response.json({ error: 'Not found.' }, { status: 404 });\n const rest = url.pathname.slice(DOOM_API_ROUTE_PREFIX.length + 1);\n const slash = rest.indexOf('/');\n const basePath = slash === -1 ? rest : rest.slice(0, slash);\n const handler = host.handlerFor(basePath);\n if (handler === undefined)\n return Response.json({ error: `No API '${basePath}' in this session.` }, { status: 404 });\n url.pathname = slash === -1 ? '/' : rest.slice(slash);\n const startedAt = performance.now();\n const parent = validatedTraceContext(request.headers.get('traceparent'));\n let childContext: DoomTraceContext | undefined;\n let response: Response;\n try {\n const invoke = async (context?: DoomTraceContext): Promise<Response> => {\n if (options.canDispatch?.() === false || host.handlerFor(basePath) !== handler) {\n return Response.json({ error: 'Session selection changed before dispatch.' }, { status: 503 });\n }\n childContext = context;\n return handler.fetch(new Request(url, request));\n };\n response = options.telemetry\n ? await options.telemetry.runInSpan(\n 'doompi_server.package_api.request',\n { api: basePath, method: request.method },\n invoke,\n parent,\n )\n : await invoke();\n } catch (error) {\n options.onNotice(`package API '${basePath}' failed on ${url.pathname} (${describeError(error)})`);\n response = Response.json({ error: `The '${basePath}' API failed.` }, { status: 500 });\n }\n return responseWithCompletion(response, () => {\n if (!options.telemetry) return;\n const durationMs = Math.round(performance.now() - startedAt);\n // Keep this span only where it says something the request span cannot: a failing status,\n // or a body that took real time to flush after the headers were sent.\n if (response.status < 400 && durationMs < COMPLETION_SPAN_MIN_DURATION_MS) return;\n observe(\n options.telemetry.runInSpan(\n 'doompi_server.package_api.complete',\n {\n api: basePath,\n method: request.method,\n status_code: response.status,\n duration_ms: durationMs,\n },\n async () => undefined,\n childContext ?? parent,\n ),\n options.onNotice,\n );\n });\n };\n let closePromise: Promise<void> | undefined;\n\n return {\n request: dispatch,\n close: () => {\n closePromise ??= (async () => {\n await installed.dispose();\n communication?.close();\n host.dispose();\n })();\n return closePromise;\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;AAoBA,MAAa,kCAAkC;AAkC/C,SAAS,cAAc,OAAwB;CAC7C,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC9D;AAEA,SAAS,uBAAuB,UAAoB,UAAgC;CAClF,IAAI,SAAS,SAAS,MAAM;EAC1B,SAAS;EACT,OAAO;CACT;CACA,MAAM,SAAS,SAAS,KAAK,UAAU;CACvC,IAAI,YAAY;CAChB,MAAM,eAAqB;EACzB,IAAI,WAAW;EACf,YAAY;EACZ,SAAS;CACX;CACA,MAAM,OAAO,IAAI,eAA2B;EAC1C,MAAM,KAAK,YAAY;GACrB,IAAI;IACF,MAAM,SAAS,MAAM,OAAO,KAAK;IACjC,IAAI,OAAO,MAAM;KACf,OAAO;KACP,WAAW,MAAM;IACnB,OAAO,WAAW,QAAQ,OAAO,KAAK;GACxC,SAAS,OAAO;IACd,OAAO;IACP,WAAW,MAAM,KAAK;GACxB;EACF;EACA,MAAM,OAAO,QAAQ;GACnB,OAAO;GACP,MAAM,OAAO,OAAO,MAAM;EAC5B;CACF,CAAC;CACD,OAAO,IAAI,SAAS,MAAM,QAAQ;AACpC;;AAGA,eAAsB,iBAAiB,SAA6D;CAClG,IAAI,QAAQ,gBAAgB,KAAA,GAC1B,MAAM,IAAI,MAAM,8DAA8D;CAChF,IAAI,QAAQ,iBAAiB,KAAA,GAAW,MAAM,IAAI,MAAM,oDAAoD;CAC5G,MAAM,SAAS,QAAQ,UAAU,CAAC;CAClC,IAAI,QAAQ,KAAK,WAAW,KAAK,OAAO,WAAW,KAAK,CAAC,QAAQ,eAC/D,OAAO;EACL,SAAS,YAAY,SAAS,KAAK,EAAE,OAAO,+BAA+B,GAAG,EAAE,QAAQ,IAAI,CAAC;EAC7F,aAAa,QAAQ,QAAQ;CAC/B;CAGF,MAAM,uBAAuB,QAAQ;CACrC,MAAM,gBAAgB,sBAAsB,oBAAoB,QAAQ,SAAS;CACjF,MAAM,wBACJ,yBAAyB,KAAA,IACrB,KAAA,IACA;EACE,SAAS,YAAY,qBAAqB,OAAO,OAAO;EACxD,QAAQ,cAAc,qBAAqB,MAAM,SAAS;EAC1D,SAAS,cAAc,qBAAqB,OAAO,SAAS;EAC5D,GAAI,qBAAqB,mBAAmB,KAAA,IACxC,CAAC,IACD,EACE,iBAAiB,iBAAyB,oBACxC,qBAAqB,iBAAiB,iBAAiB,eAAe,MAAM,KAChF;CACN;CACN,MAAM,UAA0B;EAC9B,OAAO;EACP,WAAW,QAAQ;EACnB,aAAa,QAAQ;EACrB,eAAe,QAAQ;EACvB,eAAe,QAAQ;EACvB,KAAK,QAAQ;EACb,aAAa,QAAQ;EACrB,cAAc,QAAQ;EACtB,GAAI,QAAQ,kBAAkB,KAAA,IAAY,CAAC,IAAI,EAAE,eAAe,QAAQ,cAAc;EACtF,GAAI,QAAQ,aAAa,KAAA,IAAY,CAAC,IAAI,EAAE,UAAU,QAAQ,SAAS;EACvE,GAAI,0BAA0B,KAAA,IAAY,CAAC,IAAI,EAAE,gBAAgB,sBAAsB;EACvF,GAAI,kBAAkB,KAAA,IAAY,CAAC,IAAI,EAAE,sBAAsB,cAAc;EAC7E,UAAU,QAAQ;CACpB;CACA,MAAM,OAAOA,cAAAA,qBAAqB;EAChC,OAAO;EACP;EACA,cAAc,QAAQ;EACtB,GAAI,QAAQ,mBAAmB,KAAA,IAAY,CAAC,IAAI,EAAE,gBAAgB,QAAQ,eAAe;CAC3F,CAAC;CACD,KAAK,MAAM,OAAO,QAAQ,MAAM,KAAK,YAAY,GAAG;CACpD,IAAI;CACJ,IAAI;EACF,YAAY,MAAMC,0BAAAA,oBAAoB;GAAE;GAAM;GAAQ,UAAU,QAAQ;GAAU,SAAS,QAAQ;EAAc,CAAC;EAClH,IAAI;GACF,MAAM,QAAQ,iBAAiB,SAAS;EAC1C,SAAS,OAAO;GACd,MAAM,UAAU,QAAQ;GACxB,MAAM;EACR;CACF,SAAS,OAAO;EACd,eAAe,MAAM;EACrB,KAAK,QAAQ;EACb,MAAM;CACR;CACA,IAAI,KAAK,QAAQ,CAAC,CAAC,WAAW,KAAK,CAAC,QAAQ,eAAe;EACzD,MAAM,UAAU,QAAQ;EACxB,eAAe,MAAM;EACrB,KAAK,QAAQ;EACb,OAAO;GACL,SAAS,YAAY,SAAS,KAAK,EAAE,OAAO,+BAA+B,GAAG,EAAE,QAAQ,IAAI,CAAC;GAC7F,aAAa,QAAQ,QAAQ;EAC/B;CACF;CAEA,MAAM,WAAW,OAAO,YAAwC;EAC9D,IAAI,QAAQ,cAAc,MAAM,OAC9B,OAAO,SAAS,KAAK,EAAE,OAAO,kCAAkC,GAAG,EAAE,QAAQ,IAAI,CAAC;EACpF,MAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;EAC/B,IAAI,CAAC,IAAI,SAAS,WAAW,eAA2B,GACtD,OAAO,SAAS,KAAK,EAAE,OAAO,aAAa,GAAG,EAAE,QAAQ,IAAI,CAAC;EAC/D,MAAM,OAAO,IAAI,SAAS,MAAMC,mBAAAA,sBAAsB,SAAS,CAAC;EAChE,MAAM,QAAQ,KAAK,QAAQ,GAAG;EAC9B,MAAM,WAAW,UAAU,KAAK,OAAO,KAAK,MAAM,GAAG,KAAK;EAC1D,MAAM,UAAU,KAAK,WAAW,QAAQ;EACxC,IAAI,YAAY,KAAA,GACd,OAAO,SAAS,KAAK,EAAE,OAAO,WAAW,SAAS,oBAAoB,GAAG,EAAE,QAAQ,IAAI,CAAC;EAC1F,IAAI,WAAW,UAAU,KAAK,MAAM,KAAK,MAAM,KAAK;EACpD,MAAM,YAAY,YAAY,IAAI;EAClC,MAAM,SAASC,gBAAAA,sBAAsB,QAAQ,QAAQ,IAAI,aAAa,CAAC;EACvE,IAAI;EACJ,IAAI;EACJ,IAAI;GACF,MAAM,SAAS,OAAO,YAAkD;IACtE,IAAI,QAAQ,cAAc,MAAM,SAAS,KAAK,WAAW,QAAQ,MAAM,SACrE,OAAO,SAAS,KAAK,EAAE,OAAO,6CAA6C,GAAG,EAAE,QAAQ,IAAI,CAAC;IAE/F,eAAe;IACf,OAAO,QAAQ,MAAM,IAAI,QAAQ,KAAK,OAAO,CAAC;GAChD;GACA,WAAW,QAAQ,YACf,MAAM,QAAQ,UAAU,UACtB,qCACA;IAAE,KAAK;IAAU,QAAQ,QAAQ;GAAO,GACxC,QACA,MACF,IACA,MAAM,OAAO;EACnB,SAAS,OAAO;GACd,QAAQ,SAAS,gBAAgB,SAAS,cAAc,IAAI,SAAS,IAAI,cAAc,KAAK,EAAE,EAAE;GAChG,WAAW,SAAS,KAAK,EAAE,OAAO,QAAQ,SAAS,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;EACtF;EACA,OAAO,uBAAuB,gBAAgB;GAC5C,IAAI,CAAC,QAAQ,WAAW;GACxB,MAAM,aAAa,KAAK,MAAM,YAAY,IAAI,IAAI,SAAS;GAG3D,IAAI,SAAS,SAAS,OAAO,aAAA,KAA8C;GAC3E,yBAAA,QACE,QAAQ,UAAU,UAChB,sCACA;IACE,KAAK;IACL,QAAQ,QAAQ;IAChB,aAAa,SAAS;IACtB,aAAa;GACf,GACA,YAAY,KAAA,GACZ,gBAAgB,MAClB,GACA,QAAQ,QACV;EACF,CAAC;CACH;CACA,IAAI;CAEJ,OAAO;EACL,SAAS;EACT,aAAa;GACX,kBAAkB,YAAY;IAC5B,MAAM,UAAU,QAAQ;IACxB,eAAe,MAAM;IACrB,KAAK,QAAQ;GACf,EAAA,CAAG;GACH,OAAO;EACT;CACF;AACF"}
@@ -55,6 +55,14 @@ async function serveSessionApis(options) {
55
55
  request: async () => Response.json({ error: "No package APIs are mounted." }, { status: 404 }),
56
56
  close: () => Promise.resolve()
57
57
  };
58
+ const owningSessionService = options.sessionService;
59
+ const communication = owningSessionService?.bindCommunication?.(options.sessionId);
60
+ const exposedSessionService = owningSessionService === void 0 ? void 0 : {
61
+ create: (request) => owningSessionService.create(request),
62
+ close: (sessionId) => owningSessionService.close(sessionId),
63
+ isLive: (sessionId) => owningSessionService.isLive(sessionId),
64
+ ...owningSessionService.canCommunicate === void 0 ? {} : { canCommunicate: (sourceSessionId, targetSessionId) => owningSessionService.canCommunicate?.(sourceSessionId, targetSessionId) === true }
65
+ };
58
66
  const context = {
59
67
  scope: "session",
60
68
  sessionId: options.sessionId,
@@ -66,7 +74,8 @@ async function serveSessionApis(options) {
66
74
  directEvents: options.directEvents,
67
75
  ...options.internalToken === void 0 ? {} : { internalToken: options.internalToken },
68
76
  ...options.hubToken === void 0 ? {} : { hubToken: options.hubToken },
69
- ...options.sessionService === void 0 ? {} : { sessionService: options.sessionService },
77
+ ...exposedSessionService === void 0 ? {} : { sessionService: exposedSessionService },
78
+ ...communication === void 0 ? {} : { sessionCommunication: communication },
70
79
  onNotice: options.onNotice
71
80
  };
72
81
  const host = createDoomServerHost({
@@ -91,11 +100,13 @@ async function serveSessionApis(options) {
91
100
  throw error;
92
101
  }
93
102
  } catch (error) {
103
+ communication?.close();
94
104
  host.dispose();
95
105
  throw error;
96
106
  }
97
107
  if (host.mounted().length === 0 && !options.prepareFacets) {
98
108
  await installed.dispose();
109
+ communication?.close();
99
110
  host.dispose();
100
111
  return {
101
112
  request: async () => Response.json({ error: "No package APIs are mounted." }, { status: 404 }),
@@ -148,6 +159,7 @@ async function serveSessionApis(options) {
148
159
  close: () => {
149
160
  closePromise ??= (async () => {
150
161
  await installed.dispose();
162
+ communication?.close();
151
163
  host.dispose();
152
164
  })();
153
165
  return closePromise;
@@ -1 +1 @@
1
- {"version":3,"file":"package-api-server.mjs","names":[],"sources":["../src/server/packageApiServer.ts"],"sourcesContent":["import type { DoomTraceContext } from '@agimon-ai/doompi-telemetry';\n\nimport type { DoomDirectEventBus, DoomHubSessionService } from '../exports/hubChannel';\nimport { DOOM_API_ROUTE_PREFIX, type DoomApi, type DoomApiContext } from '../exports/packageApi';\nimport type { DoomPluginRegistry } from '../exports/pluginProtocol';\nimport { createDoomServerHost, type CreateDoomServerHostOptions, type DoomServerFacet } from '../exports/serverFacet';\nimport {\n installServerFacets,\n type LoadedServerFacet,\n type InstalledServerFacets,\n type InstallServerFacetsOptions,\n} from '../exports/serverFacet';\nimport { observe, type ServerTelemetry } from '../services/serverTelemetry';\nimport { validatedTraceContext } from '../services/traceContext';\n\n/**\n * A body that keeps streaming well after its headers went out is worth its own span. A fast\n * one is not: the request span already carries the duration and outcome, so emitting a second\n * span per call made package API traffic 99.65% of all spans recorded in a two hour sample.\n */\nexport const COMPLETION_SPAN_MIN_DURATION_MS = 1000;\n\nexport interface PackageApiServerOptions {\n sessionId: string;\n workspaceId?: string;\n workspaceRoot?: string;\n homeDirectory?: string;\n mountChannel?: CreateDoomServerHostOptions['mountChannel'];\n cwd: string;\n /** Admitted environment for APIs serving this session. */\n readonly environment: Readonly<Record<string, string | undefined>>;\n /** Lifecycle-owned direct events shared with the hub. */\n readonly directEvents: DoomDirectEventBus;\n internalToken?: string;\n hubToken?: string;\n /** Shared exact-scope dispatch table, so session facets can mount plugin methods. */\n pluginRegistry?: DoomPluginRegistry;\n sessionService?: DoomHubSessionService;\n apis: readonly DoomApi[];\n /** Server facets to install; each registers its own APIs through the host. */\n facets?: readonly (DoomServerFacet | LoadedServerFacet)[];\n prepareFacets?: InstallServerFacetsOptions['prepare'];\n activateFacets?: (installed: InstalledServerFacets) => Promise<void>;\n canDispatch?: () => boolean;\n telemetry?: ServerTelemetry;\n onNotice: (message: string) => void;\n}\n\nexport interface PackageApiServer {\n /** Dispatches an already-authenticated request without crossing a transport boundary. */\n readonly request: (request: Request) => Promise<Response>;\n close(): Promise<void>;\n}\n\nfunction describeError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction responseWithCompletion(response: Response, complete: () => void): Response {\n if (response.body === null) {\n complete();\n return response;\n }\n const reader = response.body.getReader();\n let completed = false;\n const finish = (): void => {\n if (completed) return;\n completed = true;\n complete();\n };\n const body = new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n const result = await reader.read();\n if (result.done) {\n finish();\n controller.close();\n } else controller.enqueue(result.value);\n } catch (error) {\n finish();\n controller.error(error);\n }\n },\n async cancel(reason) {\n finish();\n await reader.cancel(reason);\n },\n });\n return new Response(body, response);\n}\n\n/** Installs session facets and dispatches authenticated package API calls in-process. */\nexport async function serveSessionApis(options: PackageApiServerOptions): Promise<PackageApiServer> {\n if (options.environment === undefined)\n throw new Error('Session package API server requires an admitted environment.');\n if (options.directEvents === undefined) throw new Error('Session package API server requires direct events.');\n const facets = options.facets ?? [];\n if (options.apis.length === 0 && facets.length === 0 && !options.prepareFacets) {\n return {\n request: async () => Response.json({ error: 'No package APIs are mounted.' }, { status: 404 }),\n close: () => Promise.resolve(),\n };\n }\n\n const context: DoomApiContext = {\n scope: 'session',\n sessionId: options.sessionId,\n workspaceId: options.workspaceId,\n workspaceRoot: options.workspaceRoot,\n homeDirectory: options.homeDirectory,\n cwd: options.cwd,\n environment: options.environment,\n directEvents: options.directEvents,\n ...(options.internalToken === undefined ? {} : { internalToken: options.internalToken }),\n ...(options.hubToken === undefined ? {} : { hubToken: options.hubToken }),\n ...(options.sessionService === undefined ? {} : { sessionService: options.sessionService }),\n onNotice: options.onNotice,\n };\n const host = createDoomServerHost({\n scope: 'session',\n context,\n mountChannel: options.mountChannel,\n ...(options.pluginRegistry === undefined ? {} : { pluginRegistry: options.pluginRegistry }),\n });\n for (const api of options.apis) host.registerApi(api);\n let installed: InstalledServerFacets;\n try {\n installed = await installServerFacets({ host, facets, onNotice: options.onNotice, prepare: options.prepareFacets });\n try {\n await options.activateFacets?.(installed);\n } catch (error) {\n await installed.dispose();\n throw error;\n }\n } catch (error) {\n host.dispose();\n throw error;\n }\n if (host.mounted().length === 0 && !options.prepareFacets) {\n await installed.dispose();\n host.dispose();\n return {\n request: async () => Response.json({ error: 'No package APIs are mounted.' }, { status: 404 }),\n close: () => Promise.resolve(),\n };\n }\n\n const dispatch = async (request: Request): Promise<Response> => {\n if (options.canDispatch?.() === false)\n return Response.json({ error: 'Session selection is not ready.' }, { status: 503 });\n const url = new URL(request.url);\n if (!url.pathname.startsWith(`${DOOM_API_ROUTE_PREFIX}/`))\n return Response.json({ error: 'Not found.' }, { status: 404 });\n const rest = url.pathname.slice(DOOM_API_ROUTE_PREFIX.length + 1);\n const slash = rest.indexOf('/');\n const basePath = slash === -1 ? rest : rest.slice(0, slash);\n const handler = host.handlerFor(basePath);\n if (handler === undefined)\n return Response.json({ error: `No API '${basePath}' in this session.` }, { status: 404 });\n url.pathname = slash === -1 ? '/' : rest.slice(slash);\n const startedAt = performance.now();\n const parent = validatedTraceContext(request.headers.get('traceparent'));\n let childContext: DoomTraceContext | undefined;\n let response: Response;\n try {\n const invoke = async (context?: DoomTraceContext): Promise<Response> => {\n if (options.canDispatch?.() === false || host.handlerFor(basePath) !== handler) {\n return Response.json({ error: 'Session selection changed before dispatch.' }, { status: 503 });\n }\n childContext = context;\n return handler.fetch(new Request(url, request));\n };\n response = options.telemetry\n ? await options.telemetry.runInSpan(\n 'doompi_server.package_api.request',\n { api: basePath, method: request.method },\n invoke,\n parent,\n )\n : await invoke();\n } catch (error) {\n options.onNotice(`package API '${basePath}' failed on ${url.pathname} (${describeError(error)})`);\n response = Response.json({ error: `The '${basePath}' API failed.` }, { status: 500 });\n }\n return responseWithCompletion(response, () => {\n if (!options.telemetry) return;\n const durationMs = Math.round(performance.now() - startedAt);\n // Keep this span only where it says something the request span cannot: a failing status,\n // or a body that took real time to flush after the headers were sent.\n if (response.status < 400 && durationMs < COMPLETION_SPAN_MIN_DURATION_MS) return;\n observe(\n options.telemetry.runInSpan(\n 'doompi_server.package_api.complete',\n {\n api: basePath,\n method: request.method,\n status_code: response.status,\n duration_ms: durationMs,\n },\n async () => undefined,\n childContext ?? parent,\n ),\n options.onNotice,\n );\n });\n };\n let closePromise: Promise<void> | undefined;\n\n return {\n request: dispatch,\n close: () => {\n closePromise ??= (async () => {\n await installed.dispose();\n host.dispose();\n })();\n return closePromise;\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"package-api-server.mjs","names":[],"sources":["../src/server/packageApiServer.ts"],"sourcesContent":["import type { DoomTraceContext } from '@agimon-ai/doompi-telemetry';\n\nimport type { DoomDirectEventBus, DoomHubSessionService } from '../exports/hubChannel';\nimport { DOOM_API_ROUTE_PREFIX, type DoomApi, type DoomApiContext } from '../exports/packageApi';\nimport type { DoomPluginRegistry } from '../exports/pluginProtocol';\nimport { createDoomServerHost, type CreateDoomServerHostOptions, type DoomServerFacet } from '../exports/serverFacet';\nimport {\n installServerFacets,\n type LoadedServerFacet,\n type InstalledServerFacets,\n type InstallServerFacetsOptions,\n} from '../exports/serverFacet';\nimport { observe, type ServerTelemetry } from '../services/serverTelemetry';\nimport { validatedTraceContext } from '../services/traceContext';\n\n/**\n * A body that keeps streaming well after its headers went out is worth its own span. A fast\n * one is not: the request span already carries the duration and outcome, so emitting a second\n * span per call made package API traffic 99.65% of all spans recorded in a two hour sample.\n */\nexport const COMPLETION_SPAN_MIN_DURATION_MS = 1000;\n\nexport interface PackageApiServerOptions {\n sessionId: string;\n workspaceId?: string;\n workspaceRoot?: string;\n homeDirectory?: string;\n mountChannel?: CreateDoomServerHostOptions['mountChannel'];\n cwd: string;\n /** Admitted environment for APIs serving this session. */\n readonly environment: Readonly<Record<string, string | undefined>>;\n /** Lifecycle-owned direct events shared with the hub. */\n readonly directEvents: DoomDirectEventBus;\n internalToken?: string;\n hubToken?: string;\n /** Shared exact-scope dispatch table, so session facets can mount plugin methods. */\n pluginRegistry?: DoomPluginRegistry;\n sessionService?: DoomHubSessionService;\n apis: readonly DoomApi[];\n /** Server facets to install; each registers its own APIs through the host. */\n facets?: readonly (DoomServerFacet | LoadedServerFacet)[];\n prepareFacets?: InstallServerFacetsOptions['prepare'];\n activateFacets?: (installed: InstalledServerFacets) => Promise<void>;\n canDispatch?: () => boolean;\n telemetry?: ServerTelemetry;\n onNotice: (message: string) => void;\n}\n\nexport interface PackageApiServer {\n /** Dispatches an already-authenticated request without crossing a transport boundary. */\n readonly request: (request: Request) => Promise<Response>;\n close(): Promise<void>;\n}\n\nfunction describeError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction responseWithCompletion(response: Response, complete: () => void): Response {\n if (response.body === null) {\n complete();\n return response;\n }\n const reader = response.body.getReader();\n let completed = false;\n const finish = (): void => {\n if (completed) return;\n completed = true;\n complete();\n };\n const body = new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n const result = await reader.read();\n if (result.done) {\n finish();\n controller.close();\n } else controller.enqueue(result.value);\n } catch (error) {\n finish();\n controller.error(error);\n }\n },\n async cancel(reason) {\n finish();\n await reader.cancel(reason);\n },\n });\n return new Response(body, response);\n}\n\n/** Installs session facets and dispatches authenticated package API calls in-process. */\nexport async function serveSessionApis(options: PackageApiServerOptions): Promise<PackageApiServer> {\n if (options.environment === undefined)\n throw new Error('Session package API server requires an admitted environment.');\n if (options.directEvents === undefined) throw new Error('Session package API server requires direct events.');\n const facets = options.facets ?? [];\n if (options.apis.length === 0 && facets.length === 0 && !options.prepareFacets) {\n return {\n request: async () => Response.json({ error: 'No package APIs are mounted.' }, { status: 404 }),\n close: () => Promise.resolve(),\n };\n }\n\n const owningSessionService = options.sessionService;\n const communication = owningSessionService?.bindCommunication?.(options.sessionId);\n const exposedSessionService: DoomHubSessionService | undefined =\n owningSessionService === undefined\n ? undefined\n : {\n create: (request) => owningSessionService.create(request),\n close: (sessionId) => owningSessionService.close(sessionId),\n isLive: (sessionId) => owningSessionService.isLive(sessionId),\n ...(owningSessionService.canCommunicate === undefined\n ? {}\n : {\n canCommunicate: (sourceSessionId: string, targetSessionId: string) =>\n owningSessionService.canCommunicate?.(sourceSessionId, targetSessionId) === true,\n }),\n };\n const context: DoomApiContext = {\n scope: 'session',\n sessionId: options.sessionId,\n workspaceId: options.workspaceId,\n workspaceRoot: options.workspaceRoot,\n homeDirectory: options.homeDirectory,\n cwd: options.cwd,\n environment: options.environment,\n directEvents: options.directEvents,\n ...(options.internalToken === undefined ? {} : { internalToken: options.internalToken }),\n ...(options.hubToken === undefined ? {} : { hubToken: options.hubToken }),\n ...(exposedSessionService === undefined ? {} : { sessionService: exposedSessionService }),\n ...(communication === undefined ? {} : { sessionCommunication: communication }),\n onNotice: options.onNotice,\n };\n const host = createDoomServerHost({\n scope: 'session',\n context,\n mountChannel: options.mountChannel,\n ...(options.pluginRegistry === undefined ? {} : { pluginRegistry: options.pluginRegistry }),\n });\n for (const api of options.apis) host.registerApi(api);\n let installed: InstalledServerFacets;\n try {\n installed = await installServerFacets({ host, facets, onNotice: options.onNotice, prepare: options.prepareFacets });\n try {\n await options.activateFacets?.(installed);\n } catch (error) {\n await installed.dispose();\n throw error;\n }\n } catch (error) {\n communication?.close();\n host.dispose();\n throw error;\n }\n if (host.mounted().length === 0 && !options.prepareFacets) {\n await installed.dispose();\n communication?.close();\n host.dispose();\n return {\n request: async () => Response.json({ error: 'No package APIs are mounted.' }, { status: 404 }),\n close: () => Promise.resolve(),\n };\n }\n\n const dispatch = async (request: Request): Promise<Response> => {\n if (options.canDispatch?.() === false)\n return Response.json({ error: 'Session selection is not ready.' }, { status: 503 });\n const url = new URL(request.url);\n if (!url.pathname.startsWith(`${DOOM_API_ROUTE_PREFIX}/`))\n return Response.json({ error: 'Not found.' }, { status: 404 });\n const rest = url.pathname.slice(DOOM_API_ROUTE_PREFIX.length + 1);\n const slash = rest.indexOf('/');\n const basePath = slash === -1 ? rest : rest.slice(0, slash);\n const handler = host.handlerFor(basePath);\n if (handler === undefined)\n return Response.json({ error: `No API '${basePath}' in this session.` }, { status: 404 });\n url.pathname = slash === -1 ? '/' : rest.slice(slash);\n const startedAt = performance.now();\n const parent = validatedTraceContext(request.headers.get('traceparent'));\n let childContext: DoomTraceContext | undefined;\n let response: Response;\n try {\n const invoke = async (context?: DoomTraceContext): Promise<Response> => {\n if (options.canDispatch?.() === false || host.handlerFor(basePath) !== handler) {\n return Response.json({ error: 'Session selection changed before dispatch.' }, { status: 503 });\n }\n childContext = context;\n return handler.fetch(new Request(url, request));\n };\n response = options.telemetry\n ? await options.telemetry.runInSpan(\n 'doompi_server.package_api.request',\n { api: basePath, method: request.method },\n invoke,\n parent,\n )\n : await invoke();\n } catch (error) {\n options.onNotice(`package API '${basePath}' failed on ${url.pathname} (${describeError(error)})`);\n response = Response.json({ error: `The '${basePath}' API failed.` }, { status: 500 });\n }\n return responseWithCompletion(response, () => {\n if (!options.telemetry) return;\n const durationMs = Math.round(performance.now() - startedAt);\n // Keep this span only where it says something the request span cannot: a failing status,\n // or a body that took real time to flush after the headers were sent.\n if (response.status < 400 && durationMs < COMPLETION_SPAN_MIN_DURATION_MS) return;\n observe(\n options.telemetry.runInSpan(\n 'doompi_server.package_api.complete',\n {\n api: basePath,\n method: request.method,\n status_code: response.status,\n duration_ms: durationMs,\n },\n async () => undefined,\n childContext ?? parent,\n ),\n options.onNotice,\n );\n });\n };\n let closePromise: Promise<void> | undefined;\n\n return {\n request: dispatch,\n close: () => {\n closePromise ??= (async () => {\n await installed.dispose();\n communication?.close();\n host.dispose();\n })();\n return closePromise;\n },\n };\n}\n"],"mappings":"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"}
@@ -33,6 +33,9 @@ const SEALED_HTTP_METHODS = /* @__PURE__ */ new Set([
33
33
  "PATCH",
34
34
  "DELETE"
35
35
  ]);
36
+ /** HMAC-authenticated peer routes are the only non-device tunnel APIs. */
37
+ const SESSION_PEER_INBOX_ROUTE = "/api/plugins/session-peer/inbox";
38
+ const VOICE_PEER_ROUTES = /* @__PURE__ */ new Set(["/api/plugins/voice/peer", "/api/plugins/voice/peer-ownership"]);
36
39
  const FORBIDDEN_HEADERS = /* @__PURE__ */ new Set([
37
40
  "authorization",
38
41
  "connection",
@@ -95,9 +98,11 @@ function createRemoteRuntime(options) {
95
98
  if (context.env.listener === "local") return next();
96
99
  const path = context.req.path;
97
100
  const publicSessionMcp = require_sessionMcpRoutes.isPublicSessionMcpRoute(context.req.method, path);
101
+ const publicSessionPeer = context.req.method === "POST" && path === SESSION_PEER_INBOX_ROUTE;
102
+ const publicVoicePeer = context.req.method === "POST" && VOICE_PEER_ROUTES.has(path);
98
103
  const verdict = require_index.originVerdict({
99
104
  listener: "tunnel",
100
- method: publicSessionMcp ? "GET" : context.req.method,
105
+ method: publicSessionMcp || publicSessionPeer || publicVoicePeer ? "GET" : context.req.method,
101
106
  isUpgrade: false,
102
107
  origin: context.req.header("origin"),
103
108
  host: context.req.header("host"),
@@ -105,7 +110,7 @@ function createRemoteRuntime(options) {
105
110
  tunnel: remote.tunnelPolicy()
106
111
  });
107
112
  if (verdict !== "allow") return context.json({ error: `Tunnel request refused: ${verdict}.` }, 403);
108
- if (require_index.isPublicPairingRoute(context.req.method, path) || publicSessionMcp) return next();
113
+ if (require_index.isPublicPairingRoute(context.req.method, path) || publicSessionMcp || publicSessionPeer || publicVoicePeer) return next();
109
114
  const device = remote.authorize((0, hono_cookie.getCookie)(context, require_remote.DEVICE_COOKIE, "host"));
110
115
  if (device === void 0) return context.json({ error: "This device is not paired." }, 401);
111
116
  if (context.env.sealedDeviceId === device) return next();
@@ -344,6 +349,9 @@ function createRemoteRuntime(options) {
344
349
  if (typeof ceremony?.ceremonyId !== "string" || !await remote.passkeys().finishStepUp(ceremony.ceremonyId, `device:${device}`, action, ceremony.response)) stepUpDenied = true;
345
350
  }
346
351
  headers.delete(require_remote.STEP_UP_HEADER);
352
+ headers.set(require_packageApi.DOOM_API_CALLER_LOCALITY_HEADER, "remote");
353
+ headers.set(require_packageApi.DOOM_API_CALLER_DEVICE_ID_HEADER, device);
354
+ headers.set(require_packageApi.DOOM_API_CALLER_STEP_UP_HEADER, action && remote.stepUpRequired(action) ? "verified" : "not-required");
347
355
  let response;
348
356
  if (stepUpDenied) response = context.json({
349
357
  error: "This action needs a fresh passkey gesture.",
@@ -376,7 +384,7 @@ function createRemoteRuntime(options) {
376
384
  return sealed.ok ? context.json(sealed.envelope) : context.json({ error: `The sealed response failed: ${sealed.failure}.` }, 503);
377
385
  });
378
386
  app.on(["POST", "DELETE"], "*", async (context) => {
379
- if (!require_sessionMcpRoutes.isPublicSessionMcpRoute(context.req.method, context.req.path)) return context.notFound();
387
+ if (!require_sessionMcpRoutes.isPublicSessionMcpRoute(context.req.method, context.req.path) && !(context.req.method === "POST" && context.req.path === SESSION_PEER_INBOX_ROUTE) && !(context.req.method === "POST" && VOICE_PEER_ROUTES.has(context.req.path))) return context.notFound();
380
388
  return options.forward(context.req.raw);
381
389
  });
382
390
  app.on(["GET", "HEAD"], "*", async (context) => {
@@ -1 +1 @@
1
- {"version":3,"file":"remote-runtime.cjs","names":["REMOTE_HTTP_ROUTE","REMOTE_CHANNEL_ROUTE","pipeline","Readable","createRemoteAccessStore","path","Hono","isPublicSessionMcpRoute","originVerdict","localOriginPolicy","isPublicPairingRoute","getCookie","DEVICE_COOKIE","createRemoteAccess","createTunnelLauncher","WebSocketServer","createServer","parseDoomSocketPath","WebSocket","stepUpActionFor","STEP_UP_HEADER","isSessionMcpHostRoute"],"sources":["../src/server/remoteRuntime.ts"],"sourcesContent":["import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';\nimport path from 'node:path';\nimport { Readable } from 'node:stream';\nimport { pipeline } from 'node:stream/promises';\n\nimport { Hono } from 'hono';\nimport { getCookie } from 'hono/cookie';\nimport WebSocket, { WebSocketServer, type RawData } from 'ws';\n\nimport { DEVICE_COOKIE, REMOTE_CHANNEL_ROUTE, REMOTE_HTTP_ROUTE, STEP_UP_HEADER } from '../constants/remote';\nimport { parseDoomSocketPath } from '../schemas/packageApi';\nimport { createRemoteAccess, type RemoteAccess } from '../services/remoteAccess';\nimport { createRemoteAccessStore } from '../services/remoteAccessStore';\nimport {\n isPublicPairingRoute,\n originVerdict,\n type GuardListener,\n localOriginPolicy,\n} from '../services/remoteGuardPolicy';\nimport { createTunnelLauncher, reapStaleTunnel } from '../services/tunnelProcess';\nimport { stepUpActionFor } from '../services/webauthnPolicy';\nimport { type TunnelLauncher } from '../types/remote';\nimport { registerRemoteRoutes } from './remoteRoutes';\nimport { isPublicSessionMcpRoute, isSessionMcpHostRoute } from './sessionMcpRoutes';\n\nconst MAX_REQUEST_BYTES = 8 * 1024 * 1024;\nconst TUNNEL_HEADERS_TIMEOUT_MS = 5_000;\nconst TUNNEL_REQUEST_TIMEOUT_MS = 10_000;\nconst TUNNEL_CONNECTION_LIMIT = 64;\nconst TUNNEL_REQUESTS_PER_SOCKET = 100;\nconst SEALED_HTTP_VERSION = 1;\nconst SEALED_HTTP_METHODS = new Set(['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE']);\nconst FORBIDDEN_HEADERS = new Set([\n 'authorization',\n 'connection',\n 'content-length',\n 'cookie',\n 'forwarded',\n 'host',\n 'origin',\n 'transfer-encoding',\n 'upgrade',\n 'x-doompi-token',\n 'x-forwarded-for',\n 'x-forwarded-host',\n 'x-forwarded-proto',\n]);\n\ninterface SealedHttpRequest {\n v: number;\n method: string;\n target: string;\n headers: Array<[string, string]>;\n body?: string;\n}\n\nfunction sealedRequest(value: unknown): SealedHttpRequest | undefined {\n if (typeof value !== 'object' || value === null) return undefined;\n const request = value as Partial<SealedHttpRequest>;\n if (\n request.v !== SEALED_HTTP_VERSION ||\n typeof request.method !== 'string' ||\n !SEALED_HTTP_METHODS.has(request.method) ||\n typeof request.target !== 'string' ||\n !request.target.startsWith('/') ||\n request.target.startsWith('//') ||\n request.target.includes('#') ||\n !Array.isArray(request.headers) ||\n !request.headers.every(\n (header) => Array.isArray(header) && header.length === 2 && header.every((part) => typeof part === 'string'),\n ) ||\n (request.body !== undefined && typeof request.body !== 'string')\n )\n return undefined;\n const parsed = new URL(request.target, 'http://doompi.local');\n if (parsed.pathname === REMOTE_HTTP_ROUTE || parsed.pathname === REMOTE_CHANNEL_ROUTE) return undefined;\n return request as SealedHttpRequest;\n}\n\nfunction decodedBody(value: string | undefined): Buffer | undefined {\n if (value === undefined) return Buffer.alloc(0);\n if (!/^(?:[A-Za-z\\d+/]{4})*(?:[A-Za-z\\d+/]{2}==|[A-Za-z\\d+/]{3}=)?$/u.test(value)) return undefined;\n const body = Buffer.from(value, 'base64');\n return body.toString('base64') === value ? body : undefined;\n}\n\ninterface ListenerBindings {\n listener: GuardListener;\n incoming?: IncomingMessage;\n sealedDeviceId?: string;\n}\n\nexport interface RemoteRuntimeOptions {\n homeDirectory: string;\n registrationToken: string;\n bundleTrust(): { publicKey: string; revision: number } | undefined;\n onNotice(message: string): void;\n /** The listener to which a sealed, authenticated request is forwarded. */\n forward(request: Request): Promise<Response>;\n connectProtocol(pathname: string): WebSocket;\n launchTunnel?: TunnelLauncher;\n}\n\nexport interface RemoteRuntime {\n readonly remote: RemoteAccess;\n fetchLocal(request: Request): Promise<Response>;\n close(): Promise<void>;\n}\n\nasync function readBody(request: IncomingMessage): Promise<Buffer> {\n const chunks: Buffer[] = [];\n let size = 0;\n for await (const chunk of request) {\n const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);\n size += bytes.byteLength;\n if (size > MAX_REQUEST_BYTES) throw new Error('The tunnel request body is too large.');\n chunks.push(bytes);\n }\n return Buffer.concat(chunks);\n}\n\nasync function writeResponse(response: ServerResponse, result: Response): Promise<void> {\n response.writeHead(result.status, Object.fromEntries(result.headers.entries()));\n if (result.body === null) {\n response.end();\n return;\n }\n await pipeline(Readable.fromWeb(result.body as import('node:stream/web').ReadableStream), response);\n}\n\n/** One global control plane and a second socket dedicated to public tunnel traffic. */\nexport function createRemoteRuntime(options: RemoteRuntimeOptions): RemoteRuntime {\n const store = createRemoteAccessStore({\n stateDir: path.join(options.homeDirectory, '.doompi', 'web'),\n onNotice: (message) => options.onNotice(message),\n });\n reapStaleTunnel(store.directory, (message) => options.onNotice(message));\n const app = new Hono<{ Bindings: ListenerBindings }>();\n let remote: RemoteAccess;\n let frontendOrigin: string | undefined;\n app.use('*', async (context, next) => {\n if (context.env.listener === 'local') return next();\n const path = context.req.path;\n const publicSessionMcp = isPublicSessionMcpRoute(context.req.method, path);\n const verdict = originVerdict({\n listener: 'tunnel',\n // Exact OAuth and MCP routes authenticate at the application layer. Treat\n // an absent Origin like a server-to-server read while retaining Host and\n // supplied-Origin validation.\n method: publicSessionMcp ? 'GET' : context.req.method,\n isUpgrade: false,\n origin: context.req.header('origin'),\n host: context.req.header('host'),\n local: localOriginPolicy(0),\n tunnel: remote.tunnelPolicy(),\n });\n if (verdict !== 'allow') return context.json({ error: `Tunnel request refused: ${verdict}.` }, 403);\n if (isPublicPairingRoute(context.req.method, path) || publicSessionMcp) return next();\n const device = remote.authorize(getCookie(context, DEVICE_COOKIE, 'host'));\n if (device === undefined) return context.json({ error: 'This device is not paired.' }, 401);\n if (context.env.sealedDeviceId === device) return next();\n if (context.req.method === 'POST' && (path === REMOTE_CHANNEL_ROUTE || path === REMOTE_HTTP_ROUTE)) return next();\n if ((context.req.method === 'GET' || context.req.method === 'HEAD') && !path.startsWith('/api/')) return next();\n return context.json({ error: 'Remote HTTP requests must use the sealed gateway.' }, 401);\n });\n\n remote = createRemoteAccess({\n store,\n launchTunnel:\n options.launchTunnel ??\n createTunnelLauncher({\n stateDir: store.directory,\n onNotice: (message) => options.onNotice(message),\n onExit: (message) => {\n options.onNotice(`remote access: ${message}`);\n void remote.disable();\n },\n }),\n bindListener: async () => {\n const webSockets = new WebSocketServer({ noServer: true, maxPayload: MAX_REQUEST_BYTES });\n const server = createServer((incoming, outgoing) => {\n void (async () => {\n const abort = new AbortController();\n const disconnected = (): void => abort.abort();\n incoming.once('aborted', disconnected);\n outgoing.once('close', disconnected);\n try {\n const url = new URL(incoming.url ?? '/', `http://${incoming.headers.host ?? 'localhost'}`);\n const headers = new Headers();\n for (const [name, value] of Object.entries(incoming.headers))\n if (value !== undefined) headers.set(name, Array.isArray(value) ? value.join(', ') : value);\n const body = incoming.method === 'GET' || incoming.method === 'HEAD' ? undefined : await readBody(incoming);\n const request = new Request(url, {\n method: incoming.method,\n headers,\n signal: abort.signal,\n ...(body === undefined ? {} : { body }),\n });\n await writeResponse(outgoing, await app.fetch(request, { listener: 'tunnel', incoming }));\n } finally {\n incoming.off('aborted', disconnected);\n outgoing.off('close', disconnected);\n }\n })().catch((error: unknown) => {\n options.onNotice(`tunnel request failed: ${String(error)}`);\n if (!outgoing.headersSent) {\n outgoing.writeHead(500, { 'content-type': 'application/json' });\n outgoing.end(JSON.stringify({ error: 'The tunnel request failed.' }));\n } else outgoing.destroy();\n });\n });\n server.on('upgrade', (incoming, socket, head) => {\n const url = new URL(incoming.url ?? '/', `http://${incoming.headers.host ?? 'localhost'}`);\n const verdict = originVerdict({\n listener: 'tunnel',\n method: incoming.method ?? 'GET',\n isUpgrade: true,\n origin: incoming.headers.origin,\n host: incoming.headers.host,\n local: localOriginPolicy(0),\n tunnel: remote.tunnelPolicy(),\n });\n const cookies = new Map(\n (incoming.headers.cookie ?? '').split(';').map((part) => {\n const at = part.indexOf('=');\n return [part.slice(0, at).trim(), part.slice(at + 1).trim()];\n }),\n );\n const device = remote.authorize(cookies.get(`__Host-${DEVICE_COOKIE}`));\n const channel = device === undefined ? undefined : remote.channelFor(device, 'protocol');\n if (!parseDoomSocketPath(url.pathname) || verdict !== 'allow' || !device || !channel) {\n socket.write('HTTP/1.1 403 Forbidden\\r\\n\\r\\n');\n socket.destroy();\n return;\n }\n webSockets.handleUpgrade(incoming, socket, head, (client) => {\n let upstream: WebSocket;\n try {\n upstream = options.connectProtocol(url.pathname);\n } catch (error) {\n options.onNotice(`remote protocol could not connect: ${String(error)}`);\n client.close(1011, 'protocol unavailable');\n return;\n }\n const pending: Buffer[] = [];\n let pendingBytes = 0;\n let closed = false;\n const untrack = remote.trackSocket(device, (code, reason) => client.close(code, reason));\n const close = (): void => {\n if (closed) return;\n closed = true;\n untrack();\n client.close();\n upstream.close();\n };\n client.on('message', (raw: RawData) => {\n if (closed) return;\n const bytes = Array.isArray(raw) ? Buffer.concat(raw) : Buffer.from(raw as Buffer);\n let envelope: unknown;\n try {\n envelope = JSON.parse(bytes.toString('utf8')) as unknown;\n } catch {\n close();\n return;\n }\n const opened = channel.open(envelope);\n if (!opened.ok) {\n close();\n return;\n }\n const plain = Buffer.from(opened.plaintext);\n if (upstream.readyState === WebSocket.OPEN) upstream.send(plain);\n else {\n pendingBytes += plain.byteLength;\n if (pendingBytes > MAX_REQUEST_BYTES) close();\n else pending.push(plain);\n }\n });\n upstream.on('open', () => {\n for (const bytes of pending) upstream.send(bytes);\n pending.length = 0;\n pendingBytes = 0;\n });\n upstream.on('message', (raw: RawData) => {\n if (closed) return;\n const plain = Array.isArray(raw) ? Buffer.concat(raw) : Buffer.from(raw as Buffer);\n const sealed = channel.seal(plain);\n if (!sealed.ok) {\n close();\n return;\n }\n client.send(Buffer.from(JSON.stringify(sealed.envelope)));\n });\n client.on('close', close);\n client.on('error', close);\n upstream.on('close', close);\n upstream.on('error', (error) => {\n options.onNotice(`remote protocol failed: ${error.message}`);\n close();\n });\n });\n });\n server.headersTimeout = TUNNEL_HEADERS_TIMEOUT_MS;\n server.requestTimeout = TUNNEL_REQUEST_TIMEOUT_MS;\n server.maxConnections = TUNNEL_CONNECTION_LIMIT;\n server.maxRequestsPerSocket = TUNNEL_REQUESTS_PER_SOCKET;\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject);\n server.listen(0, '127.0.0.1', resolve);\n });\n const address = server.address();\n if (!address || typeof address === 'string') throw new Error('The tunnel listener has no TCP address.');\n return {\n port: address.port,\n close: async () => {\n for (const client of webSockets.clients) client.terminate();\n webSockets.close();\n server.closeAllConnections();\n await new Promise<void>((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));\n },\n };\n },\n bundleTrust: () => options.bundleTrust(),\n onNotice: (message) => options.onNotice(message),\n });\n registerRemoteRoutes(app as unknown as Hono, {\n remote,\n listenerOf: (context) => (context.env as ListenerBindings).listener,\n });\n app.post('/api/remote/frontend', async (context) => {\n if (context.env.listener !== 'local')\n return context.json({ error: 'This action is only available on the host.' }, 403);\n if (context.req.header('x-doompi-web-registration') !== options.registrationToken)\n return context.json({ error: 'The web presentation server was not authenticated.' }, 403);\n let body: unknown;\n try {\n body = await context.req.json();\n } catch {\n return context.json({ error: 'A loopback web origin is required.' }, 400);\n }\n const origin = typeof body === 'object' && body !== null && 'origin' in body ? body.origin : undefined;\n if (typeof origin !== 'string') return context.json({ error: 'A loopback web origin is required.' }, 400);\n let parsed: URL;\n try {\n parsed = new URL(origin);\n } catch {\n return context.json({ error: 'The web origin is invalid.' }, 400);\n }\n if (\n parsed.protocol !== 'http:' ||\n !['127.0.0.1', 'localhost', '[::1]'].includes(parsed.hostname) ||\n parsed.pathname !== '/' ||\n parsed.search !== '' ||\n parsed.hash !== '' ||\n !parsed.port\n )\n return context.json({ error: 'The web origin must be a loopback HTTP listener.' }, 400);\n frontendOrigin = parsed.origin;\n return context.json({ ok: true });\n });\n app.post(REMOTE_HTTP_ROUTE, async (context) => {\n const device = remote.authorize(getCookie(context, DEVICE_COOKIE, 'host'));\n const channel = device === undefined ? undefined : remote.channelFor(device, 'http');\n if (!device || !channel) return context.json({ error: 'A sealed paired device is required.' }, 401);\n let envelope: unknown;\n try {\n envelope = await context.req.json();\n } catch {\n return context.json({ error: 'The sealed request envelope was malformed.' }, 400);\n }\n const opened = channel.open(envelope);\n if (!opened.ok) return context.json({ error: `The sealed request was refused: ${opened.failure}.` }, 400);\n let decoded: unknown;\n try {\n decoded = JSON.parse(new TextDecoder().decode(opened.plaintext)) as unknown;\n } catch {\n return context.json({ error: 'The sealed HTTP request was malformed.' }, 400);\n }\n const inner = sealedRequest(decoded);\n const body = decodedBody(inner?.body);\n if (!inner || !body) return context.json({ error: 'The sealed HTTP request was invalid.' }, 400);\n const target = new URL(inner.target, 'http://doompi.local');\n const headers = new Headers();\n for (const [name, value] of inner.headers) {\n const normalized = name.toLowerCase();\n if (FORBIDDEN_HEADERS.has(normalized) || normalized.startsWith('sec-')) continue;\n headers.append(name, value);\n }\n const action = stepUpActionFor(inner.method, target.pathname);\n let stepUpDenied = false;\n if (action && remote.stepUpRequired(action)) {\n const assertion = headers.get(STEP_UP_HEADER);\n let credential: unknown;\n try {\n credential = assertion ? JSON.parse(Buffer.from(assertion, 'base64url').toString('utf8')) : undefined;\n } catch {\n credential = undefined;\n }\n const ceremony = credential as { ceremonyId?: unknown; response?: unknown } | undefined;\n if (\n typeof ceremony?.ceremonyId !== 'string' ||\n !(await remote.passkeys().finishStepUp(ceremony.ceremonyId, `device:${device}`, action, ceremony.response))\n )\n stepUpDenied = true;\n }\n headers.delete(STEP_UP_HEADER);\n let response: Response;\n if (stepUpDenied) {\n response = context.json({ error: 'This action needs a fresh passkey gesture.', action }, 401);\n } else if (isSessionMcpHostRoute(target.pathname)) {\n response = context.json({ error: 'Session MCP management is only available on the host.' }, 403);\n } else {\n const internal = new Request(target, {\n method: inner.method,\n headers,\n ...(inner.method === 'GET' || inner.method === 'HEAD' ? {} : { body }),\n });\n if (target.pathname === '/api/remote' || target.pathname.startsWith('/api/remote/')) {\n headers.set('cookie', context.req.header('cookie') ?? '');\n headers.set('host', context.req.header('host') ?? '');\n headers.set('origin', context.req.header('origin') ?? '');\n response = await app.fetch(new Request(internal, { headers }), {\n listener: 'tunnel',\n sealedDeviceId: device,\n });\n } else response = await options.forward(internal);\n }\n const responseBody = Buffer.from(await response.arrayBuffer());\n const sealed = channel.seal(\n new TextEncoder().encode(\n JSON.stringify({\n v: SEALED_HTTP_VERSION,\n status: response.status,\n headers: [...response.headers.entries()],\n body: responseBody.toString('base64'),\n }),\n ),\n );\n return sealed.ok\n ? context.json(sealed.envelope)\n : context.json({ error: `The sealed response failed: ${sealed.failure}.` }, 503);\n });\n app.on(['POST', 'DELETE'], '*', async (context) => {\n if (!isPublicSessionMcpRoute(context.req.method, context.req.path)) return context.notFound();\n return options.forward(context.req.raw);\n });\n app.on(['GET', 'HEAD'], '*', async (context) => {\n if (isPublicSessionMcpRoute(context.req.method, context.req.path)) return options.forward(context.req.raw);\n if (context.req.path.startsWith('/api/')) return context.json({ error: 'Not found.' }, 404);\n if (context.req.path === '/' && remote.authorize(getCookie(context, DEVICE_COOKIE, 'host')) === undefined)\n return context.redirect('/pair');\n if (context.req.path === '/bundle-manifest.json' || context.req.path.startsWith('/bundle-assets/')) {\n const from = new URL(context.req.url);\n return await options.forward(\n new Request(new URL(`${from.pathname}${from.search}`, 'http://doompi.local'), {\n method: context.req.method,\n }),\n );\n }\n if (!frontendOrigin) return context.json({ error: 'The web presentation server is unavailable.' }, 503);\n const from = new URL(context.req.url);\n const target = new URL(`${from.pathname}${from.search}`, frontendOrigin);\n const result = await fetch(target, { method: context.req.method, redirect: 'manual' });\n const headers = new Headers(result.headers);\n headers.delete('set-cookie');\n return new Response(result.body, { status: result.status, headers });\n });\n\n return {\n remote,\n fetchLocal: async (request) => app.fetch(request, { listener: 'local' }),\n close: async () => remote.close(),\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"remote-runtime.cjs","names":["REMOTE_HTTP_ROUTE","REMOTE_CHANNEL_ROUTE","pipeline","Readable","createRemoteAccessStore","path","Hono","isPublicSessionMcpRoute","originVerdict","localOriginPolicy","isPublicPairingRoute","getCookie","DEVICE_COOKIE","createRemoteAccess","createTunnelLauncher","WebSocketServer","createServer","parseDoomSocketPath","WebSocket","stepUpActionFor","STEP_UP_HEADER","DOOM_API_CALLER_LOCALITY_HEADER","DOOM_API_CALLER_DEVICE_ID_HEADER","DOOM_API_CALLER_STEP_UP_HEADER","isSessionMcpHostRoute"],"sources":["../src/server/remoteRuntime.ts"],"sourcesContent":["import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';\nimport path from 'node:path';\nimport { Readable } from 'node:stream';\nimport { pipeline } from 'node:stream/promises';\n\nimport { Hono } from 'hono';\nimport { getCookie } from 'hono/cookie';\nimport WebSocket, { WebSocketServer, type RawData } from 'ws';\n\nimport { DEVICE_COOKIE, REMOTE_CHANNEL_ROUTE, REMOTE_HTTP_ROUTE, STEP_UP_HEADER } from '../constants/remote';\nimport {\n DOOM_API_CALLER_DEVICE_ID_HEADER,\n DOOM_API_CALLER_LOCALITY_HEADER,\n DOOM_API_CALLER_STEP_UP_HEADER,\n parseDoomSocketPath,\n} from '../schemas/packageApi';\nimport { createRemoteAccess, type RemoteAccess } from '../services/remoteAccess';\nimport { createRemoteAccessStore } from '../services/remoteAccessStore';\nimport {\n isPublicPairingRoute,\n originVerdict,\n type GuardListener,\n localOriginPolicy,\n} from '../services/remoteGuardPolicy';\nimport { createTunnelLauncher, reapStaleTunnel } from '../services/tunnelProcess';\nimport { stepUpActionFor } from '../services/webauthnPolicy';\nimport { type TunnelLauncher } from '../types/remote';\nimport { registerRemoteRoutes } from './remoteRoutes';\nimport { isPublicSessionMcpRoute, isSessionMcpHostRoute } from './sessionMcpRoutes';\n\nconst MAX_REQUEST_BYTES = 8 * 1024 * 1024;\nconst TUNNEL_HEADERS_TIMEOUT_MS = 5_000;\nconst TUNNEL_REQUEST_TIMEOUT_MS = 10_000;\nconst TUNNEL_CONNECTION_LIMIT = 64;\nconst TUNNEL_REQUESTS_PER_SOCKET = 100;\nconst SEALED_HTTP_VERSION = 1;\nconst SEALED_HTTP_METHODS = new Set(['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE']);\n/** HMAC-authenticated peer routes are the only non-device tunnel APIs. */\nconst SESSION_PEER_INBOX_ROUTE = '/api/plugins/session-peer/inbox';\nconst VOICE_PEER_ROUTES = new Set(['/api/plugins/voice/peer', '/api/plugins/voice/peer-ownership']);\nconst FORBIDDEN_HEADERS = new Set([\n 'authorization',\n 'connection',\n 'content-length',\n 'cookie',\n 'forwarded',\n 'host',\n 'origin',\n 'transfer-encoding',\n 'upgrade',\n 'x-doompi-token',\n 'x-forwarded-for',\n 'x-forwarded-host',\n 'x-forwarded-proto',\n]);\n\ninterface SealedHttpRequest {\n v: number;\n method: string;\n target: string;\n headers: Array<[string, string]>;\n body?: string;\n}\n\nfunction sealedRequest(value: unknown): SealedHttpRequest | undefined {\n if (typeof value !== 'object' || value === null) return undefined;\n const request = value as Partial<SealedHttpRequest>;\n if (\n request.v !== SEALED_HTTP_VERSION ||\n typeof request.method !== 'string' ||\n !SEALED_HTTP_METHODS.has(request.method) ||\n typeof request.target !== 'string' ||\n !request.target.startsWith('/') ||\n request.target.startsWith('//') ||\n request.target.includes('#') ||\n !Array.isArray(request.headers) ||\n !request.headers.every(\n (header) => Array.isArray(header) && header.length === 2 && header.every((part) => typeof part === 'string'),\n ) ||\n (request.body !== undefined && typeof request.body !== 'string')\n )\n return undefined;\n const parsed = new URL(request.target, 'http://doompi.local');\n if (parsed.pathname === REMOTE_HTTP_ROUTE || parsed.pathname === REMOTE_CHANNEL_ROUTE) return undefined;\n return request as SealedHttpRequest;\n}\n\nfunction decodedBody(value: string | undefined): Buffer | undefined {\n if (value === undefined) return Buffer.alloc(0);\n if (!/^(?:[A-Za-z\\d+/]{4})*(?:[A-Za-z\\d+/]{2}==|[A-Za-z\\d+/]{3}=)?$/u.test(value)) return undefined;\n const body = Buffer.from(value, 'base64');\n return body.toString('base64') === value ? body : undefined;\n}\n\ninterface ListenerBindings {\n listener: GuardListener;\n incoming?: IncomingMessage;\n sealedDeviceId?: string;\n}\n\nexport interface RemoteRuntimeOptions {\n homeDirectory: string;\n registrationToken: string;\n bundleTrust(): { publicKey: string; revision: number } | undefined;\n onNotice(message: string): void;\n /** The listener to which a sealed, authenticated request is forwarded. */\n forward(request: Request): Promise<Response>;\n connectProtocol(pathname: string): WebSocket;\n launchTunnel?: TunnelLauncher;\n}\n\nexport interface RemoteRuntime {\n readonly remote: RemoteAccess;\n fetchLocal(request: Request): Promise<Response>;\n close(): Promise<void>;\n}\n\nasync function readBody(request: IncomingMessage): Promise<Buffer> {\n const chunks: Buffer[] = [];\n let size = 0;\n for await (const chunk of request) {\n const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);\n size += bytes.byteLength;\n if (size > MAX_REQUEST_BYTES) throw new Error('The tunnel request body is too large.');\n chunks.push(bytes);\n }\n return Buffer.concat(chunks);\n}\n\nasync function writeResponse(response: ServerResponse, result: Response): Promise<void> {\n response.writeHead(result.status, Object.fromEntries(result.headers.entries()));\n if (result.body === null) {\n response.end();\n return;\n }\n await pipeline(Readable.fromWeb(result.body as import('node:stream/web').ReadableStream), response);\n}\n\n/** One global control plane and a second socket dedicated to public tunnel traffic. */\nexport function createRemoteRuntime(options: RemoteRuntimeOptions): RemoteRuntime {\n const store = createRemoteAccessStore({\n stateDir: path.join(options.homeDirectory, '.doompi', 'web'),\n onNotice: (message) => options.onNotice(message),\n });\n reapStaleTunnel(store.directory, (message) => options.onNotice(message));\n const app = new Hono<{ Bindings: ListenerBindings }>();\n let remote: RemoteAccess;\n let frontendOrigin: string | undefined;\n app.use('*', async (context, next) => {\n if (context.env.listener === 'local') return next();\n const path = context.req.path;\n const publicSessionMcp = isPublicSessionMcpRoute(context.req.method, path);\n const publicSessionPeer = context.req.method === 'POST' && path === SESSION_PEER_INBOX_ROUTE;\n const publicVoicePeer = context.req.method === 'POST' && VOICE_PEER_ROUTES.has(path);\n const verdict = originVerdict({\n listener: 'tunnel',\n // Exact OAuth, MCP, and HMAC-authenticated peer routes authenticate at the\n // application layer. Treat an absent Origin like a server-to-server read.\n method: publicSessionMcp || publicSessionPeer || publicVoicePeer ? 'GET' : context.req.method,\n isUpgrade: false,\n origin: context.req.header('origin'),\n host: context.req.header('host'),\n local: localOriginPolicy(0),\n tunnel: remote.tunnelPolicy(),\n });\n if (verdict !== 'allow') return context.json({ error: `Tunnel request refused: ${verdict}.` }, 403);\n if (isPublicPairingRoute(context.req.method, path) || publicSessionMcp || publicSessionPeer || publicVoicePeer)\n return next();\n const device = remote.authorize(getCookie(context, DEVICE_COOKIE, 'host'));\n if (device === undefined) return context.json({ error: 'This device is not paired.' }, 401);\n if (context.env.sealedDeviceId === device) return next();\n if (context.req.method === 'POST' && (path === REMOTE_CHANNEL_ROUTE || path === REMOTE_HTTP_ROUTE)) return next();\n if ((context.req.method === 'GET' || context.req.method === 'HEAD') && !path.startsWith('/api/')) return next();\n return context.json({ error: 'Remote HTTP requests must use the sealed gateway.' }, 401);\n });\n\n remote = createRemoteAccess({\n store,\n launchTunnel:\n options.launchTunnel ??\n createTunnelLauncher({\n stateDir: store.directory,\n onNotice: (message) => options.onNotice(message),\n onExit: (message) => {\n options.onNotice(`remote access: ${message}`);\n void remote.disable();\n },\n }),\n bindListener: async () => {\n const webSockets = new WebSocketServer({ noServer: true, maxPayload: MAX_REQUEST_BYTES });\n const server = createServer((incoming, outgoing) => {\n void (async () => {\n const abort = new AbortController();\n const disconnected = (): void => abort.abort();\n incoming.once('aborted', disconnected);\n outgoing.once('close', disconnected);\n try {\n const url = new URL(incoming.url ?? '/', `http://${incoming.headers.host ?? 'localhost'}`);\n const headers = new Headers();\n for (const [name, value] of Object.entries(incoming.headers))\n if (value !== undefined) headers.set(name, Array.isArray(value) ? value.join(', ') : value);\n const body = incoming.method === 'GET' || incoming.method === 'HEAD' ? undefined : await readBody(incoming);\n const request = new Request(url, {\n method: incoming.method,\n headers,\n signal: abort.signal,\n ...(body === undefined ? {} : { body }),\n });\n await writeResponse(outgoing, await app.fetch(request, { listener: 'tunnel', incoming }));\n } finally {\n incoming.off('aborted', disconnected);\n outgoing.off('close', disconnected);\n }\n })().catch((error: unknown) => {\n options.onNotice(`tunnel request failed: ${String(error)}`);\n if (!outgoing.headersSent) {\n outgoing.writeHead(500, { 'content-type': 'application/json' });\n outgoing.end(JSON.stringify({ error: 'The tunnel request failed.' }));\n } else outgoing.destroy();\n });\n });\n server.on('upgrade', (incoming, socket, head) => {\n const url = new URL(incoming.url ?? '/', `http://${incoming.headers.host ?? 'localhost'}`);\n const verdict = originVerdict({\n listener: 'tunnel',\n method: incoming.method ?? 'GET',\n isUpgrade: true,\n origin: incoming.headers.origin,\n host: incoming.headers.host,\n local: localOriginPolicy(0),\n tunnel: remote.tunnelPolicy(),\n });\n const cookies = new Map(\n (incoming.headers.cookie ?? '').split(';').map((part) => {\n const at = part.indexOf('=');\n return [part.slice(0, at).trim(), part.slice(at + 1).trim()];\n }),\n );\n const device = remote.authorize(cookies.get(`__Host-${DEVICE_COOKIE}`));\n const channel = device === undefined ? undefined : remote.channelFor(device, 'protocol');\n if (!parseDoomSocketPath(url.pathname) || verdict !== 'allow' || !device || !channel) {\n socket.write('HTTP/1.1 403 Forbidden\\r\\n\\r\\n');\n socket.destroy();\n return;\n }\n webSockets.handleUpgrade(incoming, socket, head, (client) => {\n let upstream: WebSocket;\n try {\n upstream = options.connectProtocol(url.pathname);\n } catch (error) {\n options.onNotice(`remote protocol could not connect: ${String(error)}`);\n client.close(1011, 'protocol unavailable');\n return;\n }\n const pending: Buffer[] = [];\n let pendingBytes = 0;\n let closed = false;\n const untrack = remote.trackSocket(device, (code, reason) => client.close(code, reason));\n const close = (): void => {\n if (closed) return;\n closed = true;\n untrack();\n client.close();\n upstream.close();\n };\n client.on('message', (raw: RawData) => {\n if (closed) return;\n const bytes = Array.isArray(raw) ? Buffer.concat(raw) : Buffer.from(raw as Buffer);\n let envelope: unknown;\n try {\n envelope = JSON.parse(bytes.toString('utf8')) as unknown;\n } catch {\n close();\n return;\n }\n const opened = channel.open(envelope);\n if (!opened.ok) {\n close();\n return;\n }\n const plain = Buffer.from(opened.plaintext);\n if (upstream.readyState === WebSocket.OPEN) upstream.send(plain);\n else {\n pendingBytes += plain.byteLength;\n if (pendingBytes > MAX_REQUEST_BYTES) close();\n else pending.push(plain);\n }\n });\n upstream.on('open', () => {\n for (const bytes of pending) upstream.send(bytes);\n pending.length = 0;\n pendingBytes = 0;\n });\n upstream.on('message', (raw: RawData) => {\n if (closed) return;\n const plain = Array.isArray(raw) ? Buffer.concat(raw) : Buffer.from(raw as Buffer);\n const sealed = channel.seal(plain);\n if (!sealed.ok) {\n close();\n return;\n }\n client.send(Buffer.from(JSON.stringify(sealed.envelope)));\n });\n client.on('close', close);\n client.on('error', close);\n upstream.on('close', close);\n upstream.on('error', (error) => {\n options.onNotice(`remote protocol failed: ${error.message}`);\n close();\n });\n });\n });\n server.headersTimeout = TUNNEL_HEADERS_TIMEOUT_MS;\n server.requestTimeout = TUNNEL_REQUEST_TIMEOUT_MS;\n server.maxConnections = TUNNEL_CONNECTION_LIMIT;\n server.maxRequestsPerSocket = TUNNEL_REQUESTS_PER_SOCKET;\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject);\n server.listen(0, '127.0.0.1', resolve);\n });\n const address = server.address();\n if (!address || typeof address === 'string') throw new Error('The tunnel listener has no TCP address.');\n return {\n port: address.port,\n close: async () => {\n for (const client of webSockets.clients) client.terminate();\n webSockets.close();\n server.closeAllConnections();\n await new Promise<void>((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));\n },\n };\n },\n bundleTrust: () => options.bundleTrust(),\n onNotice: (message) => options.onNotice(message),\n });\n registerRemoteRoutes(app as unknown as Hono, {\n remote,\n listenerOf: (context) => (context.env as ListenerBindings).listener,\n });\n app.post('/api/remote/frontend', async (context) => {\n if (context.env.listener !== 'local')\n return context.json({ error: 'This action is only available on the host.' }, 403);\n if (context.req.header('x-doompi-web-registration') !== options.registrationToken)\n return context.json({ error: 'The web presentation server was not authenticated.' }, 403);\n let body: unknown;\n try {\n body = await context.req.json();\n } catch {\n return context.json({ error: 'A loopback web origin is required.' }, 400);\n }\n const origin = typeof body === 'object' && body !== null && 'origin' in body ? body.origin : undefined;\n if (typeof origin !== 'string') return context.json({ error: 'A loopback web origin is required.' }, 400);\n let parsed: URL;\n try {\n parsed = new URL(origin);\n } catch {\n return context.json({ error: 'The web origin is invalid.' }, 400);\n }\n if (\n parsed.protocol !== 'http:' ||\n !['127.0.0.1', 'localhost', '[::1]'].includes(parsed.hostname) ||\n parsed.pathname !== '/' ||\n parsed.search !== '' ||\n parsed.hash !== '' ||\n !parsed.port\n )\n return context.json({ error: 'The web origin must be a loopback HTTP listener.' }, 400);\n frontendOrigin = parsed.origin;\n return context.json({ ok: true });\n });\n app.post(REMOTE_HTTP_ROUTE, async (context) => {\n const device = remote.authorize(getCookie(context, DEVICE_COOKIE, 'host'));\n const channel = device === undefined ? undefined : remote.channelFor(device, 'http');\n if (!device || !channel) return context.json({ error: 'A sealed paired device is required.' }, 401);\n let envelope: unknown;\n try {\n envelope = await context.req.json();\n } catch {\n return context.json({ error: 'The sealed request envelope was malformed.' }, 400);\n }\n const opened = channel.open(envelope);\n if (!opened.ok) return context.json({ error: `The sealed request was refused: ${opened.failure}.` }, 400);\n let decoded: unknown;\n try {\n decoded = JSON.parse(new TextDecoder().decode(opened.plaintext)) as unknown;\n } catch {\n return context.json({ error: 'The sealed HTTP request was malformed.' }, 400);\n }\n const inner = sealedRequest(decoded);\n const body = decodedBody(inner?.body);\n if (!inner || !body) return context.json({ error: 'The sealed HTTP request was invalid.' }, 400);\n const target = new URL(inner.target, 'http://doompi.local');\n const headers = new Headers();\n for (const [name, value] of inner.headers) {\n const normalized = name.toLowerCase();\n if (FORBIDDEN_HEADERS.has(normalized) || normalized.startsWith('sec-')) continue;\n headers.append(name, value);\n }\n const action = stepUpActionFor(inner.method, target.pathname);\n let stepUpDenied = false;\n if (action && remote.stepUpRequired(action)) {\n const assertion = headers.get(STEP_UP_HEADER);\n let credential: unknown;\n try {\n credential = assertion ? JSON.parse(Buffer.from(assertion, 'base64url').toString('utf8')) : undefined;\n } catch {\n credential = undefined;\n }\n const ceremony = credential as { ceremonyId?: unknown; response?: unknown } | undefined;\n if (\n typeof ceremony?.ceremonyId !== 'string' ||\n !(await remote.passkeys().finishStepUp(ceremony.ceremonyId, `device:${device}`, action, ceremony.response))\n )\n stepUpDenied = true;\n }\n headers.delete(STEP_UP_HEADER);\n // Caller stamps are assigned after the sealed request is opened. They never\n // originate from browser-controlled sealed headers.\n headers.set(DOOM_API_CALLER_LOCALITY_HEADER, 'remote');\n headers.set(DOOM_API_CALLER_DEVICE_ID_HEADER, device);\n headers.set(DOOM_API_CALLER_STEP_UP_HEADER, action && remote.stepUpRequired(action) ? 'verified' : 'not-required');\n let response: Response;\n if (stepUpDenied) {\n response = context.json({ error: 'This action needs a fresh passkey gesture.', action }, 401);\n } else if (isSessionMcpHostRoute(target.pathname)) {\n response = context.json({ error: 'Session MCP management is only available on the host.' }, 403);\n } else {\n const internal = new Request(target, {\n method: inner.method,\n headers,\n ...(inner.method === 'GET' || inner.method === 'HEAD' ? {} : { body }),\n });\n if (target.pathname === '/api/remote' || target.pathname.startsWith('/api/remote/')) {\n headers.set('cookie', context.req.header('cookie') ?? '');\n headers.set('host', context.req.header('host') ?? '');\n headers.set('origin', context.req.header('origin') ?? '');\n response = await app.fetch(new Request(internal, { headers }), {\n listener: 'tunnel',\n sealedDeviceId: device,\n });\n } else response = await options.forward(internal);\n }\n const responseBody = Buffer.from(await response.arrayBuffer());\n const sealed = channel.seal(\n new TextEncoder().encode(\n JSON.stringify({\n v: SEALED_HTTP_VERSION,\n status: response.status,\n headers: [...response.headers.entries()],\n body: responseBody.toString('base64'),\n }),\n ),\n );\n return sealed.ok\n ? context.json(sealed.envelope)\n : context.json({ error: `The sealed response failed: ${sealed.failure}.` }, 503);\n });\n app.on(['POST', 'DELETE'], '*', async (context) => {\n if (\n !isPublicSessionMcpRoute(context.req.method, context.req.path) &&\n !(context.req.method === 'POST' && context.req.path === SESSION_PEER_INBOX_ROUTE) &&\n !(context.req.method === 'POST' && VOICE_PEER_ROUTES.has(context.req.path))\n )\n return context.notFound();\n return options.forward(context.req.raw);\n });\n app.on(['GET', 'HEAD'], '*', async (context) => {\n if (isPublicSessionMcpRoute(context.req.method, context.req.path)) return options.forward(context.req.raw);\n if (context.req.path.startsWith('/api/')) return context.json({ error: 'Not found.' }, 404);\n if (context.req.path === '/' && remote.authorize(getCookie(context, DEVICE_COOKIE, 'host')) === undefined)\n return context.redirect('/pair');\n if (context.req.path === '/bundle-manifest.json' || context.req.path.startsWith('/bundle-assets/')) {\n const from = new URL(context.req.url);\n return await options.forward(\n new Request(new URL(`${from.pathname}${from.search}`, 'http://doompi.local'), {\n method: context.req.method,\n }),\n );\n }\n if (!frontendOrigin) return context.json({ error: 'The web presentation server is unavailable.' }, 503);\n const from = new URL(context.req.url);\n const target = new URL(`${from.pathname}${from.search}`, frontendOrigin);\n const result = await fetch(target, { method: context.req.method, redirect: 'manual' });\n const headers = new Headers(result.headers);\n headers.delete('set-cookie');\n return new Response(result.body, { status: result.status, headers });\n });\n\n return {\n remote,\n fetchLocal: async (request) => app.fetch(request, { listener: 'local' }),\n close: async () => remote.close(),\n };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"remote-runtime.d.cts","names":[],"sources":["../src/server/remoteRuntime.ts"],"mappings":";;;;iBA4FiB;EACf;EACA;EACA;IAAiB;IAAmB;;EACpC,SAAS;;EAET,QAAQ,SAAS,UAAU,QAAQ;EACnC,gBAAgB,mBAAmB;EACnC,eAAe;;iBAGA;WACN,QAAQ;EACjB,WAAW,SAAS,UAAU,QAAQ;EACtC,SAAS;;;wBAyBK,oBAAoB,SAAS,uBAAuB"}
1
+ {"version":3,"file":"remote-runtime.d.cts","names":[],"sources":["../src/server/remoteRuntime.ts"],"mappings":";;;;iBAoGiB;EACf;EACA;EACA;IAAiB;IAAmB;;EACpC,SAAS;;EAET,QAAQ,SAAS,UAAU,QAAQ;EACnC,gBAAgB,mBAAmB;EACnC,eAAe;;iBAGA;WACN,QAAQ;EACjB,WAAW,SAAS,UAAU,QAAQ;EACtC,SAAS;;;wBAyBK,oBAAoB,SAAS,uBAAuB"}
@@ -1 +1 @@
1
- {"version":3,"file":"remote-runtime.d.mts","names":[],"sources":["../src/server/remoteRuntime.ts"],"mappings":";;;;iBA4FiB;EACf;EACA;EACA;IAAiB;IAAmB;;EACpC,SAAS;;EAET,QAAQ,SAAS,UAAU,QAAQ;EACnC,gBAAgB,mBAAmB;EACnC,eAAe;;iBAGA;WACN,QAAQ;EACjB,WAAW,SAAS,UAAU,QAAQ;EACtC,SAAS;;;wBAyBK,oBAAoB,SAAS,uBAAuB"}
1
+ {"version":3,"file":"remote-runtime.d.mts","names":[],"sources":["../src/server/remoteRuntime.ts"],"mappings":";;;;iBAoGiB;EACf;EACA;EACA;IAAiB;IAAmB;;EACpC,SAAS;;EAET,QAAQ,SAAS,UAAU,QAAQ;EACnC,gBAAgB,mBAAmB;EACnC,eAAe;;iBAGA;WACN,QAAQ;EACjB,WAAW,SAAS,UAAU,QAAQ;EACtC,SAAS;;;wBAyBK,oBAAoB,SAAS,uBAAuB"}
@@ -1,4 +1,4 @@
1
- import { parseDoomSocketPath } from "./schemas/packageApi.mjs";
1
+ import { DOOM_API_CALLER_DEVICE_ID_HEADER, DOOM_API_CALLER_LOCALITY_HEADER, DOOM_API_CALLER_STEP_UP_HEADER, parseDoomSocketPath } from "./schemas/packageApi.mjs";
2
2
  import { isPublicSessionMcpRoute, isSessionMcpHostRoute } from "./server/sessionMcpRoutes.mjs";
3
3
  import { DEVICE_COOKIE, REMOTE_CHANNEL_ROUTE, REMOTE_HTTP_ROUTE, STEP_UP_HEADER } from "./constants/remote.mjs";
4
4
  import { isPublicPairingRoute, localOriginPolicy, originVerdict } from "./services/remoteGuardPolicy/index.mjs";
@@ -29,6 +29,9 @@ const SEALED_HTTP_METHODS = /* @__PURE__ */ new Set([
29
29
  "PATCH",
30
30
  "DELETE"
31
31
  ]);
32
+ /** HMAC-authenticated peer routes are the only non-device tunnel APIs. */
33
+ const SESSION_PEER_INBOX_ROUTE = "/api/plugins/session-peer/inbox";
34
+ const VOICE_PEER_ROUTES = /* @__PURE__ */ new Set(["/api/plugins/voice/peer", "/api/plugins/voice/peer-ownership"]);
32
35
  const FORBIDDEN_HEADERS = /* @__PURE__ */ new Set([
33
36
  "authorization",
34
37
  "connection",
@@ -91,9 +94,11 @@ function createRemoteRuntime(options) {
91
94
  if (context.env.listener === "local") return next();
92
95
  const path = context.req.path;
93
96
  const publicSessionMcp = isPublicSessionMcpRoute(context.req.method, path);
97
+ const publicSessionPeer = context.req.method === "POST" && path === SESSION_PEER_INBOX_ROUTE;
98
+ const publicVoicePeer = context.req.method === "POST" && VOICE_PEER_ROUTES.has(path);
94
99
  const verdict = originVerdict({
95
100
  listener: "tunnel",
96
- method: publicSessionMcp ? "GET" : context.req.method,
101
+ method: publicSessionMcp || publicSessionPeer || publicVoicePeer ? "GET" : context.req.method,
97
102
  isUpgrade: false,
98
103
  origin: context.req.header("origin"),
99
104
  host: context.req.header("host"),
@@ -101,7 +106,7 @@ function createRemoteRuntime(options) {
101
106
  tunnel: remote.tunnelPolicy()
102
107
  });
103
108
  if (verdict !== "allow") return context.json({ error: `Tunnel request refused: ${verdict}.` }, 403);
104
- if (isPublicPairingRoute(context.req.method, path) || publicSessionMcp) return next();
109
+ if (isPublicPairingRoute(context.req.method, path) || publicSessionMcp || publicSessionPeer || publicVoicePeer) return next();
105
110
  const device = remote.authorize(getCookie(context, DEVICE_COOKIE, "host"));
106
111
  if (device === void 0) return context.json({ error: "This device is not paired." }, 401);
107
112
  if (context.env.sealedDeviceId === device) return next();
@@ -340,6 +345,9 @@ function createRemoteRuntime(options) {
340
345
  if (typeof ceremony?.ceremonyId !== "string" || !await remote.passkeys().finishStepUp(ceremony.ceremonyId, `device:${device}`, action, ceremony.response)) stepUpDenied = true;
341
346
  }
342
347
  headers.delete(STEP_UP_HEADER);
348
+ headers.set(DOOM_API_CALLER_LOCALITY_HEADER, "remote");
349
+ headers.set(DOOM_API_CALLER_DEVICE_ID_HEADER, device);
350
+ headers.set(DOOM_API_CALLER_STEP_UP_HEADER, action && remote.stepUpRequired(action) ? "verified" : "not-required");
343
351
  let response;
344
352
  if (stepUpDenied) response = context.json({
345
353
  error: "This action needs a fresh passkey gesture.",
@@ -372,7 +380,7 @@ function createRemoteRuntime(options) {
372
380
  return sealed.ok ? context.json(sealed.envelope) : context.json({ error: `The sealed response failed: ${sealed.failure}.` }, 503);
373
381
  });
374
382
  app.on(["POST", "DELETE"], "*", async (context) => {
375
- if (!isPublicSessionMcpRoute(context.req.method, context.req.path)) return context.notFound();
383
+ if (!isPublicSessionMcpRoute(context.req.method, context.req.path) && !(context.req.method === "POST" && context.req.path === SESSION_PEER_INBOX_ROUTE) && !(context.req.method === "POST" && VOICE_PEER_ROUTES.has(context.req.path))) return context.notFound();
376
384
  return options.forward(context.req.raw);
377
385
  });
378
386
  app.on(["GET", "HEAD"], "*", async (context) => {