@agimon-ai/doompi-web 0.0.1-alpha.1 → 0.0.1-alpha.3

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 (105) hide show
  1. package/dist/index.d.cts +2 -2
  2. package/dist/index.d.mts +2 -2
  3. package/dist/src/adapters/httpServer.cjs +1 -1
  4. package/dist/src/adapters/httpServer.cjs.map +1 -1
  5. package/dist/src/adapters/httpServer.d.cts +2 -0
  6. package/dist/src/adapters/httpServer.d.cts.map +1 -1
  7. package/dist/src/adapters/httpServer.d.mts +2 -0
  8. package/dist/src/adapters/httpServer.d.mts.map +1 -1
  9. package/dist/src/adapters/httpServer.mjs +1 -1
  10. package/dist/src/adapters/httpServer.mjs.map +1 -1
  11. package/dist/src/adapters/packageApiProxy.cjs +1 -1
  12. package/dist/src/adapters/packageApiProxy.cjs.map +1 -1
  13. package/dist/src/adapters/packageApiProxy.mjs +1 -1
  14. package/dist/src/adapters/packageApiProxy.mjs.map +1 -1
  15. package/dist/src/adapters/registryWatcher.d.cts +1 -0
  16. package/dist/src/adapters/registryWatcher.d.mts +1 -0
  17. package/dist/src/adapters/remoteRoutes.cjs +1 -1
  18. package/dist/src/adapters/remoteRoutes.mjs +1 -1
  19. package/dist/src/adapters/serverSpawner.cjs +1 -1
  20. package/dist/src/adapters/serverSpawner.cjs.map +1 -1
  21. package/dist/src/adapters/serverSpawner.d.cts +1 -0
  22. package/dist/src/adapters/serverSpawner.d.mts +1 -0
  23. package/dist/src/adapters/serverSpawner.mjs +1 -1
  24. package/dist/src/adapters/serverSpawner.mjs.map +1 -1
  25. package/dist/src/adapters/sessionHub.cjs +1 -1
  26. package/dist/src/adapters/sessionHub.cjs.map +1 -1
  27. package/dist/src/adapters/sessionHub.d.cts +6 -0
  28. package/dist/src/adapters/sessionHub.d.mts +6 -0
  29. package/dist/src/adapters/sessionHub.mjs +1 -1
  30. package/dist/src/adapters/sessionHub.mjs.map +1 -1
  31. package/dist/src/adapters/settingsRoutes.cjs +1 -1
  32. package/dist/src/adapters/settingsRoutes.cjs.map +1 -1
  33. package/dist/src/adapters/settingsRoutes.mjs +1 -1
  34. package/dist/src/adapters/settingsRoutes.mjs.map +1 -1
  35. package/dist/src/adapters/syncGuard.cjs +1 -1
  36. package/dist/src/adapters/syncGuard.cjs.map +1 -1
  37. package/dist/src/adapters/syncGuard.mjs +1 -1
  38. package/dist/src/adapters/syncGuard.mjs.map +1 -1
  39. package/dist/src/services/webauthnPolicy.cjs +1 -1
  40. package/dist/src/services/webauthnPolicy.cjs.map +1 -1
  41. package/dist/src/services/webauthnPolicy.mjs +1 -1
  42. package/dist/src/services/webauthnPolicy.mjs.map +1 -1
  43. package/dist/src/types/settings.cjs +1 -1
  44. package/dist/src/types/settings.cjs.map +1 -1
  45. package/dist/src/types/settings.d.cts +1 -0
  46. package/dist/src/types/settings.d.mts +1 -0
  47. package/dist/src/types/settings.mjs +1 -1
  48. package/dist/src/types/settings.mjs.map +1 -1
  49. package/dist/web/assets/{CodeEditorView-D-TJJJaq.js → CodeEditorView-CP55qjoR.js} +3 -3
  50. package/dist/web/assets/{CodeEditorView-D-TJJJaq.js.map → CodeEditorView-CP55qjoR.js.map} +1 -1
  51. package/dist/web/assets/{dist-DASJVcWF.js → dist-B0inTUnL.js} +2 -2
  52. package/dist/web/assets/{dist-DASJVcWF.js.map → dist-B0inTUnL.js.map} +1 -1
  53. package/dist/web/assets/{dist-CvW-klGY.js → dist-Bqd73gXk.js} +2 -2
  54. package/dist/web/assets/{dist-CvW-klGY.js.map → dist-Bqd73gXk.js.map} +1 -1
  55. package/dist/web/assets/{dist-nYGDKnBj.js → dist-C7QLpOTK.js} +2 -2
  56. package/dist/web/assets/{dist-nYGDKnBj.js.map → dist-C7QLpOTK.js.map} +1 -1
  57. package/dist/web/assets/{dist-DPmeQCk2.js → dist-CElJaC8s.js} +2 -2
  58. package/dist/web/assets/{dist-DPmeQCk2.js.map → dist-CElJaC8s.js.map} +1 -1
  59. package/dist/web/assets/{dist-DHhgwtts.js → dist-Cwzviw_U.js} +2 -2
  60. package/dist/web/assets/{dist-DHhgwtts.js.map → dist-Cwzviw_U.js.map} +1 -1
  61. package/dist/web/assets/{dist-BIM3Wiup.js → dist-DgVFo7X3.js} +2 -2
  62. package/dist/web/assets/{dist-BIM3Wiup.js.map → dist-DgVFo7X3.js.map} +1 -1
  63. package/dist/web/assets/{dist-Q1Vv-_7E.js → dist-GONoz8to.js} +2 -2
  64. package/dist/web/assets/{dist-Q1Vv-_7E.js.map → dist-GONoz8to.js.map} +1 -1
  65. package/dist/web/assets/{dist-BpSem-7r.js → dist-Gdc8iniF.js} +2 -2
  66. package/dist/web/assets/{dist-BpSem-7r.js.map → dist-Gdc8iniF.js.map} +1 -1
  67. package/dist/web/assets/{dist-CuZIwX62.js → dist-TyTwU387.js} +2 -2
  68. package/dist/web/assets/{dist-CuZIwX62.js.map → dist-TyTwU387.js.map} +1 -1
  69. package/dist/web/assets/{dist-BNh-qcPn.js → dist-eZCftY6T.js} +1 -1
  70. package/dist/web/assets/index-BqJnbtrI.css +2 -0
  71. package/dist/web/assets/index-Ck8t7U8J.js +98 -0
  72. package/dist/web/assets/{index-dUt-mAKq.js.map → index-Ck8t7U8J.js.map} +1 -1
  73. package/dist/web/index.html +2 -2
  74. package/package.json +14 -14
  75. package/src/adapters/httpServer.ts +107 -15
  76. package/src/adapters/packageApiProxy.ts +2 -0
  77. package/src/adapters/serverSpawner.ts +38 -2
  78. package/src/adapters/sessionHub.ts +63 -4
  79. package/src/adapters/settingsRoutes.ts +187 -0
  80. package/src/adapters/syncGuard.ts +27 -7
  81. package/src/services/webauthnPolicy.ts +16 -7
  82. package/src/types/settings.ts +50 -8
  83. package/src/web/app/Providers.tsx +2 -2
  84. package/src/web/app/sessionRuntime.ts +2 -1
  85. package/src/web/features/activity/ActivityDock.tsx +76 -56
  86. package/src/web/features/session/Timeline.tsx +16 -1
  87. package/src/web/features/sessions/NewSessionDialog.tsx +1 -1
  88. package/src/web/features/settings/ContributedSettings.tsx +20 -10
  89. package/src/web/features/settings/PluginSettings.tsx +6 -2
  90. package/src/web/features/settings/ProviderSettings.tsx +11 -4
  91. package/src/web/features/settings/RepositorySettings.tsx +365 -0
  92. package/src/web/features/settings/RepositoryWorkspace.tsx +150 -0
  93. package/src/web/features/settings/SettingsMenu.tsx +97 -26
  94. package/src/web/features/settings/TunnelSettings.tsx +6 -3
  95. package/src/web/lib/composition.ts +8 -3
  96. package/src/web/lib/pluginRegistry.ts +18 -0
  97. package/src/web/lib/protocolTimeline.ts +3 -1
  98. package/src/web/lib/sessionModel.ts +55 -14
  99. package/src/web/lib/settingsApi.ts +38 -0
  100. package/src/web/lib/settingsSections.ts +46 -10
  101. package/src/web/lib/transport.ts +12 -0
  102. package/src/web/routes/SettingsPage.tsx +63 -16
  103. package/src/web/stores/sessionStore.ts +10 -2
  104. package/dist/web/assets/index-D8NYSvmx.css +0 -2
  105. package/dist/web/assets/index-dUt-mAKq.js +0 -97
package/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { BridgeState, BridgeStatusFrame, SessionFrame } from "./src/types/session.cjs";
2
2
  import { SessionAttachment, WebServer, WebServerOptions } from "./src/types/bridge.cjs";
3
- import { serveWeb } from "./src/adapters/httpServer.cjs";
4
- import { ServeOptions, parseServeOptions } from "./src/services/serveOptions.cjs";
5
3
  import { HUB_PROTOCOL_VERSION, HUB_ROLE, SESSIONS_API_ROUTE, SessionPhase, SessionSummary } from "./src/types/hub.cjs";
6
4
  import { SESSION_RECORD_VERSION, SessionRecord } from "./src/types/registry.cjs";
5
+ import { serveWeb } from "./src/adapters/httpServer.cjs";
6
+ import { ServeOptions, parseServeOptions } from "./src/services/serveOptions.cjs";
7
7
  export { type BridgeState, type BridgeStatusFrame, HUB_PROTOCOL_VERSION, HUB_ROLE, SESSIONS_API_ROUTE, SESSION_RECORD_VERSION, type ServeOptions, type SessionAttachment, type SessionFrame, type SessionPhase, type SessionRecord, type SessionSummary, type WebServer, type WebServerOptions, parseServeOptions, serveWeb };
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { BridgeState, BridgeStatusFrame, SessionFrame } from "./src/types/session.mjs";
2
2
  import { SessionAttachment, WebServer, WebServerOptions } from "./src/types/bridge.mjs";
3
- import { serveWeb } from "./src/adapters/httpServer.mjs";
4
- import { ServeOptions, parseServeOptions } from "./src/services/serveOptions.mjs";
5
3
  import { HUB_PROTOCOL_VERSION, HUB_ROLE, SESSIONS_API_ROUTE, SessionPhase, SessionSummary } from "./src/types/hub.mjs";
6
4
  import { SESSION_RECORD_VERSION, SessionRecord } from "./src/types/registry.mjs";
5
+ import { serveWeb } from "./src/adapters/httpServer.mjs";
6
+ import { ServeOptions, parseServeOptions } from "./src/services/serveOptions.mjs";
7
7
  export { type BridgeState, type BridgeStatusFrame, HUB_PROTOCOL_VERSION, HUB_ROLE, SESSIONS_API_ROUTE, SESSION_RECORD_VERSION, type ServeOptions, type SessionAttachment, type SessionFrame, type SessionPhase, type SessionRecord, type SessionSummary, type WebServer, type WebServerOptions, parseServeOptions, serveWeb };
@@ -1,2 +1,2 @@
1
- const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("./piHubService.cjs"),n=require("./syncGuard.cjs"),r=require("./piWebSocketListener.cjs"),i=require("../types/media.cjs"),a=require("../services/fileMedia.cjs"),o=require("../services/staticAssets.cjs"),s=require("../types/hub.cjs"),c=require("../types/notification.cjs");require("../types/session.cjs");const l=require("./gitStatus.cjs"),u=require("./registryWatcher.cjs"),d=require("./serverSpawner.cjs"),f=require("./sessionHub.cjs"),p=require("../types/remoteAccess.cjs"),m=require("../services/remoteGuardPolicy.cjs"),h=require("../services/sessionMigration.cjs"),ee=require("./authRoutes.cjs"),te=require("./settingsRoutes.cjs"),ne=require("./providerAuth.cjs"),re=require("./remoteGuard.cjs"),ie=require("./remoteAccess.cjs"),g=require("./remoteAccessStore.cjs"),_=require("./remoteRoutes.cjs"),v=require("./tunnelProcess.cjs"),y=require("./directoryListing.cjs"),b=require("./sessionFiles.cjs"),x=require("./packageApiProxy.cjs"),ae=require("./threadJournals.cjs"),oe=require("./webHubPluginLoader.cjs"),S=require("./webTelemetry.cjs");let C=require("node:fs");C=e.__toESM(C,1);let w=require("node:path");w=e.__toESM(w,1);let T=require("node:url"),E=require("node:os");E=e.__toESM(E,1);let D=require("@hono/node-server"),se=require("@hono/node-ws"),O=require("@earendil-works/pi-server"),ce=require("hono"),k=require("hono/cookie"),A=require("hono/body-limit"),j=require("@agimon-ai/doompi-extension-contracts/package-api"),le=require("@agimon-ai/doompi-extension-contracts/package-api-loader"),M=require("@agimon-ai/doompi-extension-contracts/sandbox-harness");const N=`index.html`,P=[`.doompi`,`web`,`current`,`web`],F=8388608,I=new Set([`GET`,`HEAD`,`POST`,`PUT`,`PATCH`,`DELETE`]),L=new Set([`connection`,`content-length`,`cookie`,`forwarded`,`host`,`origin`,`transfer-encoding`,`upgrade`,`x-forwarded-for`,`x-forwarded-host`,`x-forwarded-proto`]);function ue(e){if(typeof e!=`object`||!e)return;let t=e;if(t.v!==1||typeof t.method!=`string`||!I.has(t.method)||typeof t.target!=`string`||!t.target.startsWith(`/`)||t.target.startsWith(`//`)||t.target.includes(`#`)||!Array.isArray(t.headers)||!t.headers.every(e=>Array.isArray(e)&&e.length===2&&e.every(e=>typeof e==`string`))||t.body!==void 0&&typeof t.body!=`string`)return;let n=new URL(t.target,`http://sealed.doompi.invalid`);if(n.pathname!==p.REMOTE_HTTP_ROUTE&&n.pathname!==p.REMOTE_CHANNEL_ROUTE)return t}function de(e,t){try{let n=new Headers;for(let[t,r]of e.headers){let e=t.toLowerCase();L.has(e)||e.startsWith(`sec-`)||n.append(t,r)}for(let e of[`cookie`,`host`,`origin`]){let r=t.req.header(e);r!==void 0&&n.set(e,r)}return n}catch{return}}function fe(e){if(e===void 0)return Buffer.alloc(0);if(!/^(?:[A-Za-z\d+/]{4})*(?:[A-Za-z\d+/]{2}==|[A-Za-z\d+/]{3}=)?$/.test(e))return;let t=Buffer.from(e,`base64`);return t.toString(`base64`)===e?t:void 0}function R(){let e=w.default.dirname((0,T.fileURLToPath)(require("url").pathToFileURL(__filename).href));for(;;){if(C.default.existsSync(w.default.join(e,`package.json`)))return w.default.join(e,`dist`,`web`);let t=w.default.dirname(e);if(t===e)throw Error(`doompi-web could not locate its own package root.`);e=t}}function z(e,t){if(e!==void 0)return e;let n=process.env.DOOMPI_WEB_DIST;if(n!==void 0&&n!==``)return n;let r=w.default.join(E.default.homedir(),...P);return C.default.existsSync(w.default.join(r,N))?(t(`serving the synced cockpit bundle from ${r}`),r):R()}function B(e){try{return C.default.statSync(e).isFile()?C.default.readFileSync(e):void 0}catch{return}}function V(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function H(e){return e instanceof Error?e.message:String(e)}function U(e,t){return`${e}\n${t}`}function pe(e,t,n,r){return f.createSessionHub({source:u.watchRegistry(e.registryDir,t),spawner:d.createServerSpawner({registryDir:e.registryDir,command:e.spawnCommand,onNotice:t}),readGit:l.readGitStatus,channels:n,telemetry:r,onNotice:t})}function me(e,t,n){let r=[];for(let i of t){let t=`${j.DOOM_API_ROUTE_PREFIX}/${i.basePath}`,a;try{a=i.start({scope:`hub`,onNotice:n})}catch(e){n(`hub API '${i.basePath}' did not start (${H(e)}); its routes stay unmounted`);continue}r.push(a),e.all(`${t}/*`,async e=>{if(e.req.query(`session`)!==void 0)return e.notFound();let r=new URL(e.req.url);r.pathname=r.pathname.slice(t.length)||`/`;try{return await a.fetch(new Request(r,e.req.raw))}catch(t){return n(`hub API '${i.basePath}' failed on ${r.pathname} (${H(t)})`),e.json({error:`The '${i.basePath}' API failed.`},500)}})}return r}function he(e,t,n,r){e.all(`${j.DOOM_API_ROUTE_PREFIX}/*`,async e=>{let i=e.req.query(s.API_SESSION_QUERY_PARAM);if(i===void 0)return e.notFound();let a=t.snapshot().find(e=>e.id===i);if(a===void 0)return e.json({error:`No session ${i}.`},404);if(a.apiSocketPath===void 0)return e.json({error:`Session ${i} serves no package API.`},404);let o=new URL(e.req.url);o.searchParams.delete(s.API_SESSION_QUERY_PARAM);let c=S.readTraceContext(e.req.raw.headers);try{return await r.runInSpan(`web.package_proxy`,{"operation.name":`web.package_proxy`,"http.method":e.req.method},async t=>await x.proxyToSocket({socketPath:a.apiSocketPath,path:`${o.pathname}${o.search}`,method:e.req.method,headers:e.req.raw.headers,body:e.req.raw.body,trace:S.forwardedTraceContext(t,c)}),c)}catch(t){return n(`session ${i} API is unreachable (${H(t)})`),e.json({error:`Session ${i} is not answering.`},502)}})}async function W(e){let l=e.host??`127.0.0.1`,u=e.onNotice??(()=>{}),d=S.createWebTelemetry(),f=z(e.assetsDir,u),x=pe(e,u,await oe.loadHubChannels(f,u),d),C=ae.createThreadJournals({resolve:(e,t)=>x.threadJournal(e,t),onNotice:u}),T=n.createSyncGuard({repoRoot:process.cwd(),onNotice:u});await T.ensureSynced();let E=new Set,j=(e,t)=>{for(let n of E)t&&!n.local||n.post(e)},P=new ce.Hono,I=(0,se.createNodeWebSocket)({app:P}),L,R=g.createRemoteAccessStore({stateDir:e.remoteStateDir,onNotice:u});v.reapStaleTunnel(R.directory,u);let H=e.onHandover===void 0?void 0:t=>{let n=h.planSessionMigration(x.snapshot().map(e=>({id:e.id,cwd:e.cwd,...e.name===void 0?{}:{name:e.name}})),t.sandbox.workspaces);for(let e of h.describeStranded(n.stranded))u(e);for(let e of n.migrate){let t=x.stop(e.id);t.ok||u(`could not stop ${e.name??e.id} before the move: ${t.error}`)}e.onHandover?.({settings:t,sessions:n.migrate})},W=ie.createRemoteAccess({store:R,launchTunnel:e.remoteAccess?.launchTunnel??v.createTunnelLauncher({stateDir:R.directory,...e.cloudflaredPath===void 0?{}:{cloudflaredPath:e.cloudflaredPath},onNotice:u,onExit:e=>{u(`remote access: ${e}`),W.disable()}}),bindListener:async()=>await new Promise((e,t)=>{let n=(0,D.serve)({fetch:P.fetch,port:0,hostname:`127.0.0.1`,serverOptions:{maxHeaderSize:16384,headersTimeout:5e3,requestTimeout:1e4}},t=>{I.injectWebSocket(n),e({port:t.port,close:async()=>await new Promise(e=>{n.closeAllConnections?.(),n.close(()=>e())})})}),r=n;r.maxConnections=64,r.maxRequestsPerSocket=100,n.once(`error`,t)}),onNotice:u,...H===void 0?{}:{requestHandover:H},contained:(0,M.insideSandbox)(process.env),broadcastLocal:e=>j(e,!0),broadcastAll:e=>j(e,!1),...e.remoteAccess?.now===void 0?{}:{now:e.remoteAccess.now}}),G=re.createRemoteGuard({loopbackPort:()=>L,tunnelPolicy:()=>W.tunnelPolicy(),authorize:e=>W.authorize((0,k.getCookie)(e,p.DEVICE_COOKIE,`host`)),trustedDevice:e=>e.env?.sealedDeviceId,channelReady:(e,t)=>W.channelFor(e,t)!==void 0,stepUp:{required:e=>W.stepUpRequired(e),verify:async(e,t,n)=>{if(typeof n!=`object`||!n)return!1;let r=n.ceremonyId,i=n.response;if(typeof r!=`string`)return!1;let a=e.env?.sealedDeviceId??W.authorize((0,k.getCookie)(e,`doompi_device`,`host`)),o=a===void 0?`local`:`device:${a}`;return await W.passkeys().finishStepUp(r,o,t,i)}},extraOrigins:m.allowedOriginsFromEnv(process.env.DOOMPI_WEB_ALLOW_ORIGIN)});P.use(`*`,G.middleware),P.use(`*`,async(e,t)=>{let n=new URL(e.req.url).pathname;if(n===p.REMOTE_HTTP_ROUTE)return t();let r=S.requestOperation(n);if(r===void 0)return t();let i=performance.now();return await d.runInSpan(r,{"operation.name":r,"http.method":e.req.method},async()=>{await t(),await d.recordEvent(`web.api.request`,{"operation.name":r,"http.method":e.req.method,"http.status_code":e.res.status,duration_ms:Math.max(0,Math.round(performance.now()-i))})},S.readTraceContext(e.req.raw.headers))}),P.use(`*`,async(e,t)=>{if(G.listenerOf(e)===`local`)return t();let n=Number(e.req.header(`content-length`));return Number.isFinite(n)&&n>F?e.json({error:`The tunnel request body is too large.`},413):t()}),_.registerRemoteRoutes(P,{remote:W,listenerOf:e=>G.listenerOf(e)});let ge=(0,A.bodyLimit)({maxSize:F,onError:e=>e.json({error:`The tunnel request body is too large.`},413)});P.use(`*`,async(e,t)=>G.listenerOf(e)===`local`?t():await ge(e,t));let K=(0,A.bodyLimit)({maxSize:8192,onError:e=>e.json({error:`The telemetry batch is too large.`},413)}),_e=S.createBrowserTelemetryRateLimit();P.post(S.WEB_TELEMETRY_ROUTE,K,async e=>{if(!_e())return e.json({error:`Too many telemetry batches.`},429);let t;try{t=await e.req.json()}catch{return e.json({error:`The telemetry batch must be JSON.`},400)}let n=S.parseBrowserPerformanceBatch(t);if(n===void 0)return e.json({error:`The telemetry batch is invalid.`},400);for(let e of n)await d.recordEvent(e.name,{...e.duration_ms===void 0?{}:{duration_ms:e.duration_ms},...e.count===void 0?{}:{count:e.count}});return e.body(null,204)}),P.post(p.REMOTE_HTTP_ROUTE,async e=>{let t=W.authorize((0,k.getCookie)(e,p.DEVICE_COOKIE,`host`));if(t===void 0)return e.json({error:`This device is not paired.`},401);let n=W.channelFor(t,`http`);if(n===void 0)return e.json({error:`This device has no sealed HTTP channel.`},401);let r;try{r=await e.req.json()}catch{return e.json({error:`The sealed request envelope was malformed.`},400)}let i=n.open(r);if(!i.ok)return e.json({error:`The sealed request was refused: ${i.failure}.`},400);let a;try{a=JSON.parse(new TextDecoder().decode(i.plaintext))}catch{return e.json({error:`The sealed HTTP request was malformed.`},400)}let o=ue(a),s=o===void 0?void 0:de(o,e),c=o===void 0?void 0:fe(o.body);if(o===void 0||s===void 0||c===void 0)return e.json({error:`The sealed HTTP request was invalid.`},400);let l=new URL(o.target,new URL(e.req.url).origin),u=await P.request(l,{method:o.method,headers:s,...o.method===`GET`||o.method===`HEAD`?{}:{body:Uint8Array.from(c).buffer}},{...e.env,sealedDeviceId:t}),d=Buffer.from(await u.arrayBuffer()),f=n.seal(new TextEncoder().encode(JSON.stringify({v:1,status:u.status,headers:Array.from(u.headers.entries()),body:d.toString(`base64`)})));return f.ok?e.json(f.envelope):e.json({error:`The sealed response failed: ${f.failure}.`},503)});let q=r.createPiWebSocketListener({onError:e=>u(`protocol: ${e.message}`)}),J=r.createPiWebSocketListener({onError:e=>u(`protocol: ${e.message}`)}),Y=t.createPiHubService({records:()=>x.records(),spawn:e=>x.create(e),onNotice:u}),ve={...Y,createSession:async()=>{throw new O.PiServerError(`not_implemented`,`Remote protocol clients cannot create sessions`)}},X=new O.PiServer(Y,{listeners:[q],onError:e=>u(`protocol: ${e.message}`)}),Z=new O.PiServer(ve,{listeners:[J],onError:e=>u(`protocol: ${e.message}`)});await Promise.all([X.start(),Z.start()]);let Q=ne.createProviderAuth({runtime:e.authRuntime,onNotice:u});ee.registerAuthRoutes(P,Q),te.registerSettingsRoutes(P,{repositories:()=>{let e=new Map;for(let t of x.records())e.set(t.cwd,{path:t.cwd,name:w.default.basename(t.cwd)||t.cwd,active:!0});return[...e.values()].sort((e,t)=>e.path.localeCompare(t.path))},models:()=>Q.listModels()});let $=me(P,await(0,le.loadPackageApis)(`hub`,{onNotice:u}),u);he(P,x,u,d),P.get(`/api/health`,e=>e.json({ok:!0,role:`hub`,protocol:1,sessions:x.snapshot().length,pid:process.pid})),P.post(s.SESSIONS_API_ROUTE,async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:`The request body must be JSON.`},400)}if(!V(t)||typeof t.cwd!=`string`||t.cwd===``)return e.json({error:`A cwd string is required.`},400);let n=typeof t.name==`string`&&t.name!==``?t.name:void 0;await T.ensureSynced();let r=await x.create({cwd:t.cwd,name:n,trace:S.readTraceContext(e.req.raw.headers)});return r.ok?e.json({sessionId:r.sessionId},201):e.json({error:r.error},r.code===`invalid_request`?400:502)}),P.post(`${s.SESSIONS_API_ROUTE}/:sessionId/restart`,async e=>{let t=e.req.param(`sessionId`);await T.ensureSynced();let n=await x.restart(t,S.readTraceContext(e.req.raw.headers));return n.ok?e.json({sessionId:n.sessionId},202):e.json({error:n.error},n.code===`invalid_request`?404:502)}),P.delete(`${s.SESSIONS_API_ROUTE}/:sessionId`,e=>{let t=e.req.param(`sessionId`),n=x.stop(t);return n.ok?e.json({sessionId:t},202):e.json({error:n.error},n.code===`unknown`?404:n.code===`self`?409:502)}),P.get(`/api/sessions/:sessionId/files`,async e=>{let t=e.req.param(`sessionId`),n=x.snapshot().find(e=>e.id===t);if(!n)return e.json({error:`Unknown session.`},404);let r=e.req.query(`q`)??``,i=await b.listSessionFiles(n.cwd,r,20);return e.json({files:i})});let ye=t=>{if(!(G.listenerOf(t)===`local`||(0,M.insideSandbox)(process.env)))return e.browseRoot??process.cwd()};return P.get(s.DIRECTORIES_API_ROUTE,async e=>{let t=ye(e),n=await y.suggestDirectories(e.req.query(`q`)??``,{limit:12,...t===void 0?{}:{root:t}});return e.json({directories:n})}),P.get(i.SESSION_FILE_ROUTE,async e=>{let t=e.req.param(`sessionId`),n=x.snapshot().find(e=>e.id===t);if(!n)return e.json({error:`Unknown session.`},404);let r=e.req.query(`path`)??``,o=await b.readSessionFile(n.cwd,r,i.MAX_SESSION_FILE_BYTES);return o.status===`forbidden`?e.json({error:`The path leaves the session directory.`},403):o.status===`not-found`?e.json({error:`No such file.`},404):o.status===`too-large`?e.json({error:`The file is too large to preview.`},413):e.body(new Uint8Array(o.body),200,a.sessionFileHeaders(r))}),P.get(p.SESSION_SOCKET_ROUTE,I.upgradeWebSocket(e=>{let t=G.listenerOf(e)===`local`,n=t?void 0:W.authorize((0,k.getCookie)(e,p.DEVICE_COOKIE,`host`)),r=n===void 0?void 0:W.channelFor(n,`session`),i=new Set,a=new Map,o,l,d,f,m=e=>{for(let[t,n]of a)(e===void 0||n.sessionId===e)&&(a.delete(t),C.unsubscribe(n.sessionId,n.threadId))};return{onOpen(e,p){if(!t&&r===void 0){p.close(1008,`sealed session channel required`);return}let h=e=>{let n=JSON.stringify(e),i=n;if(!t){if(r===void 0)return;let e=r.seal(new TextEncoder().encode(n));if(!e.ok){p.close(1008,`sealed session channel failed`);return}i=JSON.stringify(e.envelope)}try{p.send(i)}catch{}};d={post:h,local:t},E.add(d),n!==void 0&&(f=W.trackSocket(n,(e,t)=>p.close(e,t))),h(s.hubHello(x.channelTypes())),h({type:s.SESSIONS_SNAPSHOT_TYPE,sessions:x.snapshot()}),o=x.onEvent(e=>{e.kind===`upsert`?h({type:s.SESSION_UPSERT_TYPE,session:e.session}):e.kind===`removed`?(i.delete(e.sessionId),m(e.sessionId),h({type:s.SESSION_REMOVED_TYPE,sessionId:e.sessionId})):e.kind===`channel`?i.has(e.sessionId)&&h({type:e.frameType,sessionId:e.sessionId,payload:e.payload}):(c.parseDoomNotificationEntry(e.frame)!==void 0||i.has(e.sessionId))&&h(s.sessionFrameEnvelope(e.sessionId,e.frame))}),l=C.onFrame(e=>{a.has(U(e.sessionId,e.threadId))&&h(s.threadFrameEnvelope(e.sessionId,e.threadId,e.frame))}),u(`browser attached`)},onMessage(e){if(typeof e.data!=`string`)return;let n;try{n=JSON.parse(e.data)}catch{return}if(!t){if(r===void 0)return;let e=r.open(n);if(!e.ok)return;try{n=JSON.parse(new TextDecoder().decode(e.plaintext))}catch{return}}if(!V(n)||typeof n.sessionId!=`string`)return;let o=n.sessionId;if(n.type===`subscribe`){let e=x.backlog(o);if(!e)return;i.add(o),d?.post(e);for(let e of x.channelFrames(o))d?.post(e);return}if(n.type===`unsubscribe`){i.delete(o);return}if(n.type===`history_request`){let e=x.history(o,{...typeof n.before==`string`?{before:n.before}:{},...typeof n.limit==`number`?{limit:n.limit}:{}});if(!e)return;d?.post(e);return}if(n.type===`subscribe_thread`||n.type===`unsubscribe_thread`){if(typeof n.threadId!=`string`)return;let e=n.threadId,t=U(o,e);if(n.type===`unsubscribe_thread`){a.delete(t)&&C.unsubscribe(o,e);return}if(a.has(t)||x.backlog(o)===void 0)return;a.set(t,{sessionId:o,threadId:e}),d?.post(s.threadBacklog(o,e,C.subscribe(o,e)));return}if(n.type===`session_command`&&V(n.frame)){if(n.frame.type===`attach`)return;x.command(o,n.frame)}},onClose(){d&&E.delete(d),d=void 0,f?.(),f=void 0,o?.(),o=void 0,l?.(),l=void 0,i.clear(),m(),u(`browser detached`)}}})),P.get(p.PROTOCOL_SOCKET_ROUTE,I.upgradeWebSocket(e=>{let t=G.listenerOf(e)===`local`,n=t?void 0:W.authorize((0,k.getCookie)(e,p.DEVICE_COOKIE,`host`)),r=n===void 0?void 0:W.channelFor(n,`protocol`),i,a;return{onOpen(e,o){if(!t&&r===void 0){o.close(1008,`sealed protocol channel required`);return}n!==void 0&&(a=W.trackSocket(n,(e,t)=>o.close(e,t))),i=(t?q:J).accept({send:e=>{if(t){o.send(e);return}if(r===void 0)return;let n=e instanceof Uint8Array?e:new Uint8Array(e),i=r.seal(n);if(!i.ok){o.close(1008,`sealed protocol channel failed`);return}o.send(new TextEncoder().encode(JSON.stringify(i.envelope)))},close:()=>o.close(),get readyState(){return o.readyState}})},async onMessage(e){let n=e.data;if(typeof n==`string`)return;let a=n instanceof Blob?new Uint8Array(await n.arrayBuffer()):(n instanceof ArrayBuffer,new Uint8Array(n));if(t){i?.onData(a);return}if(r===void 0)return;let o;try{o=JSON.parse(new TextDecoder().decode(a))}catch{return}let s=r.open(o);s.ok&&i?.onData(s.plaintext)},onClose(){a?.(),a=void 0,i?.onClose(),i=void 0},onError(e){i?.onError(e instanceof Error?e:Error(`The protocol socket failed`)),i=void 0}}})),P.get(`*`,e=>{let t=new URL(e.req.url).pathname,n=t===`/`?void 0:o.resolveAssetPath(f,t),r=n?B(n):void 0;if(n&&r)return e.body(new Uint8Array(r),200,{"Content-Type":o.contentTypeFor(n)});let i=B(w.default.join(f,N));return i?e.body(new Uint8Array(i),200,{"Content-Type":`text/html; charset=utf-8`}):e.text(`The cockpit bundle is missing. Run the package build.`,500)}),T.watch(()=>{j({type:s.HUB_RESYNCED_TYPE},!1)}),new Promise((t,n)=>{let r=(0,D.serve)({fetch:P.fetch,port:e.port,hostname:l},e=>{L=e.port,I.injectWebSocket(r);let n=`http://${l}:${e.port}`;u(`cockpit on ${n}`);let i,a=async()=>{await W.close(),await new Promise(e=>{C.close(),T.close(),X.close(),Z.close(),x.close(),Q.close();for(let e of $)e.close();for(let e of I.wss.clients)e.terminate();I.wss.close(),r.closeAllConnections?.(),r.close(()=>e())}),await S.shutdownWebTelemetry(d)};t({url:n,port:e.port,close:()=>i??=a()})});r.once(`error`,e=>{C.close(),x.close(),Q.close(),W.close();for(let e of $)e.close();d.shutdown(),n(e)})})}exports.serveWeb=W;
1
+ const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("./piHubService.cjs"),n=require("./syncGuard.cjs"),r=require("./piWebSocketListener.cjs"),i=require("../types/media.cjs"),a=require("../services/fileMedia.cjs"),o=require("../services/staticAssets.cjs"),s=require("../types/hub.cjs"),c=require("../types/notification.cjs");require("../types/session.cjs");const l=require("./gitStatus.cjs"),u=require("./registryWatcher.cjs"),d=require("./serverSpawner.cjs"),f=require("./packageApiProxy.cjs"),p=require("./sessionHub.cjs"),m=require("../types/remoteAccess.cjs"),h=require("../services/remoteGuardPolicy.cjs"),g=require("../services/sessionMigration.cjs"),_=require("./authRoutes.cjs"),ee=require("./settingsRoutes.cjs"),te=require("./providerAuth.cjs"),ne=require("./remoteGuard.cjs"),re=require("./remoteAccess.cjs"),ie=require("./remoteAccessStore.cjs"),ae=require("./remoteRoutes.cjs"),v=require("./tunnelProcess.cjs"),oe=require("./directoryListing.cjs"),y=require("./sessionFiles.cjs"),se=require("./threadJournals.cjs"),ce=require("./webHubPluginLoader.cjs"),b=require("./webTelemetry.cjs");let x=require("node:fs");x=e.__toESM(x,1);let S=require("node:path");S=e.__toESM(S,1);let C=require("node:url"),w=require("node:crypto"),T=require("node:os");T=e.__toESM(T,1);let E=require("@hono/node-server"),le=require("@hono/node-ws"),D=require("@earendil-works/pi-server"),ue=require("@agimon-ai/doompi/services"),de=require("@agimon-ai/doompi/services/syncState"),fe=require("hono"),O=require("hono/cookie"),k=require("hono/body-limit"),A=require("@agimon-ai/doompi-extension-contracts/package-api"),pe=require("@agimon-ai/doompi-extension-contracts/package-api-loader"),j=require("@agimon-ai/doompi-extension-contracts/sandbox-harness"),M=require("@agimon-ai/doompi/utils/repository");const N=`index.html`,P=[`.doompi`,`web`,`current`,`web`],F=8388608,I=new Set([`GET`,`HEAD`,`POST`,`PUT`,`PATCH`,`DELETE`]),L=new Set([`connection`,`content-length`,`cookie`,`forwarded`,`host`,`origin`,`transfer-encoding`,`upgrade`,`x-forwarded-for`,`x-forwarded-host`,`x-forwarded-proto`]);function me(e){if(typeof e!=`object`||!e)return;let t=e;if(t.v!==1||typeof t.method!=`string`||!I.has(t.method)||typeof t.target!=`string`||!t.target.startsWith(`/`)||t.target.startsWith(`//`)||t.target.includes(`#`)||!Array.isArray(t.headers)||!t.headers.every(e=>Array.isArray(e)&&e.length===2&&e.every(e=>typeof e==`string`))||t.body!==void 0&&typeof t.body!=`string`)return;let n=new URL(t.target,`http://sealed.doompi.invalid`);if(n.pathname!==m.REMOTE_HTTP_ROUTE&&n.pathname!==m.REMOTE_CHANNEL_ROUTE)return t}function he(e,t){try{let n=new Headers;for(let[t,r]of e.headers){let e=t.toLowerCase();L.has(e)||e.startsWith(`sec-`)||n.append(t,r)}for(let e of[`cookie`,`host`,`origin`]){let r=t.req.header(e);r!==void 0&&n.set(e,r)}return n}catch{return}}function ge(e){if(e===void 0)return Buffer.alloc(0);if(!/^(?:[A-Za-z\d+/]{4})*(?:[A-Za-z\d+/]{2}==|[A-Za-z\d+/]{3}=)?$/.test(e))return;let t=Buffer.from(e,`base64`);return t.toString(`base64`)===e?t:void 0}function R(){let e=S.default.dirname((0,C.fileURLToPath)(require("url").pathToFileURL(__filename).href));for(;;){if(x.default.existsSync(S.default.join(e,`package.json`)))return S.default.join(e,`dist`,`web`);let t=S.default.dirname(e);if(t===e)throw Error(`doompi-web could not locate its own package root.`);e=t}}function _e(e,t){if(e!==void 0)return e;let n=process.env.DOOMPI_WEB_DIST;if(n!==void 0&&n!==``)return n;let r=S.default.join(T.default.homedir(),...P);return x.default.existsSync(S.default.join(r,N))?(t(`serving the synced cockpit bundle from ${r}`),r):R()}function z(e){try{return x.default.statSync(e).isFile()?x.default.readFileSync(e):void 0}catch{return}}function B(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function V(e){return e instanceof Error?e.message:String(e)}function H(e,t){return`${e}\n${t}`}function ve(e,t,n,r){return p.createSessionHub({source:u.watchRegistry(e.registryDir,t),spawner:d.createServerSpawner({registryDir:e.registryDir,command:e.spawnCommand,onNotice:t}),readGit:l.readGitStatus,channels:n,telemetry:r,onNotice:t})}function U(e){return`repo-${(0,w.createHash)(`sha256`).update(e).digest(`base64url`).slice(0,24)}`}function W(e){let t=new Map,n=()=>{for(let[e,n]of t)t.set(e,{...n,repository:{...n.repository,active:!1}});for(let n of e.records())try{let e=x.default.realpathSync((0,M.findRepositoryRoot)(n.cwd));t.set(e,{repository:{id:U(e),path:e,name:S.default.basename(e)||e,active:!0},seenAt:Date.now()})}catch{}let n=[...t.entries()].filter(([,e])=>!e.repository.active).sort((e,t)=>t[1].seenAt-e[1].seenAt);for(let[e]of n.slice(12))t.delete(e);return[...t.values()].sort((e,t)=>Number(t.repository.active)-Number(e.repository.active)||t.seenAt-e.seenAt||e.repository.path.localeCompare(t.repository.path)).map(e=>e.repository)};return{list:n,resolve(e){return n().find(t=>t.id===e)?.path}}}function ye(e,t,n,r,i){let a=[];for(let o of t){let t=`${A.DOOM_API_ROUTE_PREFIX}/${o.basePath}`,s;try{s=o.start({scope:`hub`,onNotice:n,resolveRepository:r,readRepositorySync:i})}catch(e){n(`hub API '${o.basePath}' did not start (${V(e)}); its routes stay unmounted`);continue}a.push(s),e.all(`${t}/*`,async e=>{if(e.req.query(`session`)!==void 0)return e.notFound();let r=new URL(e.req.url);r.pathname=r.pathname.slice(t.length)||`/`;try{return await s.fetch(new Request(r,e.req.raw))}catch(t){return n(`hub API '${o.basePath}' failed on ${r.pathname} (${V(t)})`),e.json({error:`The '${o.basePath}' API failed.`},500)}})}return a}function be(e,t,n,r){e.all(`${A.DOOM_API_ROUTE_PREFIX}/*`,async e=>{let i=e.req.query(s.API_SESSION_QUERY_PARAM);if(i===void 0)return e.notFound();let a=t.snapshot().find(e=>e.id===i);if(a===void 0)return e.json({error:`No session ${i}.`},404);if(a.apiSocketPath===void 0)return e.json({error:`Session ${i} serves no package API.`},404);let o=new URL(e.req.url);o.searchParams.delete(s.API_SESSION_QUERY_PARAM);let c=b.readTraceContext(e.req.raw.headers);try{return await r.runInSpan(`web.package_proxy`,{"operation.name":`web.package_proxy`,"http.method":e.req.method},async t=>await f.proxyToSocket({socketPath:a.apiSocketPath,path:`${o.pathname}${o.search}`,method:e.req.method,headers:e.req.raw.headers,body:e.req.raw.body,trace:b.forwardedTraceContext(t,c)}),c)}catch(t){return n(`session ${i} API is unreachable (${V(t)})`),e.json({error:`Session ${i} is not answering.`},502)}})}async function G(e){let l=e.host??`127.0.0.1`,u=e.onNotice??(()=>{}),d=b.createWebTelemetry(),f=_e(e.assetsDir,u),p=ve(e,u,await ce.loadHubChannels(f,u),d),x=se.createThreadJournals({resolve:(e,t)=>p.threadJournal(e,t),onNotice:u}),C=n.createSyncGuard({cwd:process.cwd(),onNotice:u});await C.ensureSynced();let T=new Set,A=(e,t)=>{for(let n of T)t&&!n.local||n.post(e)},M=new fe.Hono,P=(0,le.createNodeWebSocket)({app:M}),I,L=ie.createRemoteAccessStore({stateDir:e.remoteStateDir,onNotice:u});v.reapStaleTunnel(L.directory,u);let R=e.onHandover===void 0?void 0:t=>{let n=g.planSessionMigration(p.snapshot().map(e=>({id:e.id,cwd:e.cwd,...e.name===void 0?{}:{name:e.name}})),t.sandbox.workspaces);for(let e of g.describeStranded(n.stranded))u(e);for(let e of n.migrate){let t=p.stop(e.id);t.ok||u(`could not stop ${e.name??e.id} before the move: ${t.error}`)}e.onHandover?.({settings:t,sessions:n.migrate})},V=re.createRemoteAccess({store:L,launchTunnel:e.remoteAccess?.launchTunnel??v.createTunnelLauncher({stateDir:L.directory,...e.cloudflaredPath===void 0?{}:{cloudflaredPath:e.cloudflaredPath},onNotice:u,onExit:e=>{u(`remote access: ${e}`),V.disable()}}),bindListener:async()=>await new Promise((e,t)=>{let n=(0,E.serve)({fetch:M.fetch,port:0,hostname:`127.0.0.1`,serverOptions:{maxHeaderSize:16384,headersTimeout:5e3,requestTimeout:1e4}},t=>{P.injectWebSocket(n),e({port:t.port,close:async()=>await new Promise(e=>{n.closeAllConnections?.(),n.close(()=>e())})})}),r=n;r.maxConnections=64,r.maxRequestsPerSocket=100,n.once(`error`,t)}),onNotice:u,...R===void 0?{}:{requestHandover:R},contained:(0,j.insideSandbox)(process.env),broadcastLocal:e=>A(e,!0),broadcastAll:e=>A(e,!1),...e.remoteAccess?.now===void 0?{}:{now:e.remoteAccess.now}}),U=ne.createRemoteGuard({loopbackPort:()=>I,tunnelPolicy:()=>V.tunnelPolicy(),authorize:e=>V.authorize((0,O.getCookie)(e,m.DEVICE_COOKIE,`host`)),trustedDevice:e=>e.env?.sealedDeviceId,channelReady:(e,t)=>V.channelFor(e,t)!==void 0,stepUp:{required:e=>V.stepUpRequired(e),verify:async(e,t,n)=>{if(typeof n!=`object`||!n)return!1;let r=n.ceremonyId,i=n.response;if(typeof r!=`string`)return!1;let a=e.env?.sealedDeviceId??V.authorize((0,O.getCookie)(e,`doompi_device`,`host`)),o=a===void 0?`local`:`device:${a}`;return await V.passkeys().finishStepUp(r,o,t,i)}},extraOrigins:h.allowedOriginsFromEnv(process.env.DOOMPI_WEB_ALLOW_ORIGIN)});M.use(`*`,U.middleware),M.use(`*`,async(e,t)=>{let n=new URL(e.req.url).pathname;if(n===m.REMOTE_HTTP_ROUTE)return t();let r=b.requestOperation(n);if(r===void 0)return t();let i=performance.now();return await d.runInSpan(r,{"operation.name":r,"http.method":e.req.method},async()=>{await t(),await d.recordEvent(`web.api.request`,{"operation.name":r,"http.method":e.req.method,"http.status_code":e.res.status,duration_ms:Math.max(0,Math.round(performance.now()-i))})},b.readTraceContext(e.req.raw.headers))}),M.use(`*`,async(e,t)=>{if(U.listenerOf(e)===`local`)return t();let n=Number(e.req.header(`content-length`));return Number.isFinite(n)&&n>F?e.json({error:`The tunnel request body is too large.`},413):t()}),ae.registerRemoteRoutes(M,{remote:V,listenerOf:e=>U.listenerOf(e)});let G=(0,k.bodyLimit)({maxSize:F,onError:e=>e.json({error:`The tunnel request body is too large.`},413)});M.use(`*`,async(e,t)=>U.listenerOf(e)===`local`?t():await G(e,t));let xe=(0,k.bodyLimit)({maxSize:8192,onError:e=>e.json({error:`The telemetry batch is too large.`},413)}),Se=b.createBrowserTelemetryRateLimit();M.post(b.WEB_TELEMETRY_ROUTE,xe,async e=>{if(!Se())return e.json({error:`Too many telemetry batches.`},429);let t;try{t=await e.req.json()}catch{return e.json({error:`The telemetry batch must be JSON.`},400)}let n=b.parseBrowserPerformanceBatch(t);if(n===void 0)return e.json({error:`The telemetry batch is invalid.`},400);for(let e of n)await d.recordEvent(e.name,{...e.duration_ms===void 0?{}:{duration_ms:e.duration_ms},...e.count===void 0?{}:{count:e.count}});return e.body(null,204)}),M.post(m.REMOTE_HTTP_ROUTE,async e=>{let t=V.authorize((0,O.getCookie)(e,m.DEVICE_COOKIE,`host`));if(t===void 0)return e.json({error:`This device is not paired.`},401);let n=V.channelFor(t,`http`);if(n===void 0)return e.json({error:`This device has no sealed HTTP channel.`},401);let r;try{r=await e.req.json()}catch{return e.json({error:`The sealed request envelope was malformed.`},400)}let i=n.open(r);if(!i.ok)return e.json({error:`The sealed request was refused: ${i.failure}.`},400);let a;try{a=JSON.parse(new TextDecoder().decode(i.plaintext))}catch{return e.json({error:`The sealed HTTP request was malformed.`},400)}let o=me(a),s=o===void 0?void 0:he(o,e),c=o===void 0?void 0:ge(o.body);if(o===void 0||s===void 0||c===void 0)return e.json({error:`The sealed HTTP request was invalid.`},400);let l=new URL(o.target,new URL(e.req.url).origin),u=await M.request(l,{method:o.method,headers:s,...o.method===`GET`||o.method===`HEAD`?{}:{body:Uint8Array.from(c).buffer}},{...e.env,sealedDeviceId:t}),d=Buffer.from(await u.arrayBuffer()),f=n.seal(new TextEncoder().encode(JSON.stringify({v:1,status:u.status,headers:Array.from(u.headers.entries()),body:d.toString(`base64`)})));return f.ok?e.json(f.envelope):e.json({error:`The sealed response failed: ${f.failure}.`},503)});let K=r.createPiWebSocketListener({onError:e=>u(`protocol: ${e.message}`)}),q=r.createPiWebSocketListener({onError:e=>u(`protocol: ${e.message}`)}),J=t.createPiHubService({records:()=>p.records(),spawn:e=>p.create(e),onNotice:u}),Ce={...J,createSession:async()=>{throw new D.PiServerError(`not_implemented`,`Remote protocol clients cannot create sessions`)}},Y=new D.PiServer(J,{listeners:[K],onError:e=>u(`protocol: ${e.message}`)}),X=new D.PiServer(Ce,{listeners:[q],onError:e=>u(`protocol: ${e.message}`)});await Promise.all([Y.start(),X.start()]);let Z=te.createProviderAuth({runtime:e.authRuntime,onNotice:u});_.registerAuthRoutes(M,Z);let Q=W(p),we=()=>Q.list(),Te=e=>Q.resolve(e),Ee=e=>{let t=Te(e);if(t===void 0)return;let n=(0,ue.readSyncDrift)({repoRoot:t}),r;try{r=(0,de.readSyncState)(t)}catch{r=void 0}return{fresh:n.fresh,reasons:[...n.reasons],...r===void 0?{}:{mcpProjection:r.fileState.mcpProjection}}};ee.registerSettingsRoutes(M,{repositories:we,models:()=>Z.listModels()});let $=ye(M,await(0,pe.loadPackageApis)(`hub`,{onNotice:u}),u,Te,Ee);be(M,p,u,d),M.get(`/api/health`,e=>e.json({ok:!0,role:`hub`,protocol:1,sessions:p.snapshot().length,pid:process.pid})),M.post(s.SESSIONS_API_ROUTE,async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:`The request body must be JSON.`},400)}if(!B(t)||typeof t.cwd!=`string`||t.cwd===``)return e.json({error:`A cwd string is required.`},400);let n=typeof t.name==`string`&&t.name!==``?t.name:void 0;await C.ensureSynced();let r=await p.create({cwd:t.cwd,name:n,trace:b.readTraceContext(e.req.raw.headers)});return r.ok?e.json({sessionId:r.sessionId},201):e.json({error:r.error},r.code===`invalid_request`?400:502)}),M.post(`${s.SESSIONS_API_ROUTE}/:sessionId/restart`,async e=>{let t=e.req.param(`sessionId`);await C.ensureSynced();let n=await p.restart(t,b.readTraceContext(e.req.raw.headers));return n.ok?e.json({sessionId:n.sessionId},202):e.json({error:n.error},n.code===`invalid_request`?404:502)}),M.delete(`${s.SESSIONS_API_ROUTE}/:sessionId`,e=>{let t=e.req.param(`sessionId`),n=p.stop(t);return n.ok?e.json({sessionId:t},202):e.json({error:n.error},n.code===`unknown`?404:n.code===`self`?409:502)}),M.get(`/api/sessions/:sessionId/files`,async e=>{let t=e.req.param(`sessionId`),n=p.snapshot().find(e=>e.id===t);if(!n)return e.json({error:`Unknown session.`},404);let r=e.req.query(`q`)??``,i=await y.listSessionFiles(n.cwd,r,20);return e.json({files:i})});let De=t=>{if(!(U.listenerOf(t)===`local`||(0,j.insideSandbox)(process.env)))return e.browseRoot??process.cwd()};return M.get(s.DIRECTORIES_API_ROUTE,async e=>{let t=De(e),n=await oe.suggestDirectories(e.req.query(`q`)??``,{limit:12,...t===void 0?{}:{root:t}});return e.json({directories:n})}),M.get(i.SESSION_FILE_ROUTE,async e=>{let t=e.req.param(`sessionId`),n=p.snapshot().find(e=>e.id===t);if(!n)return e.json({error:`Unknown session.`},404);let r=e.req.query(`path`)??``,o=await y.readSessionFile(n.cwd,r,i.MAX_SESSION_FILE_BYTES);return o.status===`forbidden`?e.json({error:`The path leaves the session directory.`},403):o.status===`not-found`?e.json({error:`No such file.`},404):o.status===`too-large`?e.json({error:`The file is too large to preview.`},413):e.body(new Uint8Array(o.body),200,a.sessionFileHeaders(r))}),M.get(m.SESSION_SOCKET_ROUTE,P.upgradeWebSocket(e=>{let t=U.listenerOf(e)===`local`,n=t?void 0:V.authorize((0,O.getCookie)(e,m.DEVICE_COOKIE,`host`)),r=n===void 0?void 0:V.channelFor(n,`session`),i=new Set,a=(0,w.randomUUID)(),o=new Map,l,d,f,h,g=e=>{for(let[t,n]of o)(e===void 0||n.sessionId===e)&&(o.delete(t),x.unsubscribe(n.sessionId,n.threadId))};return{onOpen(e,m){if(!t&&r===void 0){m.close(1008,`sealed session channel required`);return}let _=e=>{let n=JSON.stringify(e),i=n;if(!t){if(r===void 0)return;let e=r.seal(new TextEncoder().encode(n));if(!e.ok){m.close(1008,`sealed session channel failed`);return}i=JSON.stringify(e.envelope)}try{m.send(i)}catch{}};f={post:_,local:t},T.add(f),n!==void 0&&(h=V.trackSocket(n,(e,t)=>m.close(e,t))),_(s.hubHello(p.channelTypes())),_({type:s.SESSIONS_SNAPSHOT_TYPE,sessions:p.snapshot()}),l=p.onEvent(e=>{e.kind===`upsert`?_({type:s.SESSION_UPSERT_TYPE,session:e.session}):e.kind===`removed`?(i.delete(e.sessionId),g(e.sessionId),_({type:s.SESSION_REMOVED_TYPE,sessionId:e.sessionId})):e.kind===`channel`?(e.connectionId===a||e.connectionId===void 0&&i.has(e.sessionId))&&_({type:e.frameType,sessionId:e.sessionId,payload:e.payload}):(c.parseDoomNotificationEntry(e.frame)!==void 0||i.has(e.sessionId))&&_(s.sessionFrameEnvelope(e.sessionId,e.frame))}),d=x.onFrame(e=>{o.has(H(e.sessionId,e.threadId))&&_(s.threadFrameEnvelope(e.sessionId,e.threadId,e.frame))}),u(`browser attached`)},onMessage(e){if(typeof e.data!=`string`)return;let n;try{n=JSON.parse(e.data)}catch{return}if(!t){if(r===void 0)return;let e=r.open(n);if(!e.ok)return;try{n=JSON.parse(new TextDecoder().decode(e.plaintext))}catch{return}}if(!B(n)||typeof n.sessionId!=`string`)return;let c=n.sessionId;if(typeof n.type==`string`&&p.channelTypes().includes(n.type)&&(i.has(c)||p.channelReceivesWithoutSubscription(n.type))){p.receiveChannel(c,n.type,n.payload,a);return}if(n.type===`subscribe`){let e=p.backlog(c);if(!e)return;i.add(c),f?.post(e);for(let e of p.channelFrames(c))f?.post(e);return}if(n.type===`unsubscribe`){i.delete(c);return}if(n.type===`history_request`){let e=p.history(c,{...typeof n.before==`string`?{before:n.before}:{},...typeof n.limit==`number`?{limit:n.limit}:{}});if(!e)return;f?.post(e);return}if(n.type===`subscribe_thread`||n.type===`unsubscribe_thread`){if(typeof n.threadId!=`string`)return;let e=n.threadId,t=H(c,e);if(n.type===`unsubscribe_thread`){o.delete(t)&&x.unsubscribe(c,e);return}if(o.has(t)||p.backlog(c)===void 0)return;o.set(t,{sessionId:c,threadId:e}),f?.post(s.threadBacklog(c,e,x.subscribe(c,e)));return}if(n.type===`session_command`&&B(n.frame)){if(n.frame.type===`attach`)return;p.command(c,n.frame)}},onClose(){p.disconnectChannels(a),f&&T.delete(f),f=void 0,h?.(),h=void 0,l?.(),l=void 0,d?.(),d=void 0,i.clear(),g(),u(`browser detached`)}}})),M.get(m.PROTOCOL_SOCKET_ROUTE,P.upgradeWebSocket(e=>{let t=U.listenerOf(e)===`local`,n=t?void 0:V.authorize((0,O.getCookie)(e,m.DEVICE_COOKIE,`host`)),r=n===void 0?void 0:V.channelFor(n,`protocol`),i,a;return{onOpen(e,o){if(!t&&r===void 0){o.close(1008,`sealed protocol channel required`);return}n!==void 0&&(a=V.trackSocket(n,(e,t)=>o.close(e,t))),i=(t?K:q).accept({send:e=>{if(t){o.send(e);return}if(r===void 0)return;let n=e instanceof Uint8Array?e:new Uint8Array(e),i=r.seal(n);if(!i.ok){o.close(1008,`sealed protocol channel failed`);return}o.send(new TextEncoder().encode(JSON.stringify(i.envelope)))},close:()=>o.close(),get readyState(){return o.readyState}})},async onMessage(e){let n=e.data;if(typeof n==`string`)return;let a=n instanceof Blob?new Uint8Array(await n.arrayBuffer()):(n instanceof ArrayBuffer,new Uint8Array(n));if(t){i?.onData(a);return}if(r===void 0)return;let o;try{o=JSON.parse(new TextDecoder().decode(a))}catch{return}let s=r.open(o);s.ok&&i?.onData(s.plaintext)},onClose(){a?.(),a=void 0,i?.onClose(),i=void 0},onError(e){i?.onError(e instanceof Error?e:Error(`The protocol socket failed`)),i=void 0}}})),M.get(`*`,e=>{let t=new URL(e.req.url).pathname,n=t===`/`?void 0:o.resolveAssetPath(f,t),r=n?z(n):void 0;if(n&&r)return e.body(new Uint8Array(r),200,{"Content-Type":o.contentTypeFor(n)});let i=z(S.default.join(f,N));return i?e.body(new Uint8Array(i),200,{"Content-Type":`text/html; charset=utf-8`}):e.text(`The cockpit bundle is missing. Run the package build.`,500)}),C.watch(()=>{A({type:s.HUB_RESYNCED_TYPE},!1)}),new Promise((t,n)=>{let r=(0,E.serve)({fetch:M.fetch,port:e.port,hostname:l},e=>{I=e.port,P.injectWebSocket(r);let n=`http://${l}:${e.port}`;u(`cockpit on ${n}`);let i,a=async()=>{await V.close(),await new Promise(e=>{x.close(),C.close(),Y.close(),X.close(),p.close(),Z.close();for(let e of $)e.close();for(let e of P.wss.clients)e.terminate();P.wss.close(),r.closeAllConnections?.(),r.close(()=>e())}),await b.shutdownWebTelemetry(d)};t({url:n,port:e.port,close:()=>i??=a()})});r.once(`error`,e=>{x.close(),p.close(),Z.close(),V.close();for(let e of $)e.close();d.shutdown(),n(e)})})}exports.createSettingsRepositoryRegistry=W,exports.serveWeb=G;
2
2
  //# sourceMappingURL=httpServer.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"httpServer.cjs","names":["REMOTE_HTTP_ROUTE","REMOTE_CHANNEL_ROUTE","path","fileURLToPath","fs","os","createSessionHub","watchRegistry","createServerSpawner","readGitStatus","DOOM_API_ROUTE_PREFIX","API_SESSION_QUERY_PARAM","readTraceContext","proxyToSocket","forwardedTraceContext","createWebTelemetry","loadHubChannels","createThreadJournals","createSyncGuard","Hono","createNodeWebSocket","createRemoteAccessStore","planSessionMigration","describeStranded","createRemoteAccess","createTunnelLauncher","serve","insideSandbox","createRemoteGuard","getCookie","DEVICE_COOKIE","allowedOriginsFromEnv","requestOperation","bodyLimit","createBrowserTelemetryRateLimit","WEB_TELEMETRY_ROUTE","parseBrowserPerformanceBatch","createPiWebSocketListener","createPiHubService","PiServerError","PiServer","createProviderAuth","loadPackageApis","SESSIONS_API_ROUTE","listSessionFiles","DIRECTORIES_API_ROUTE","suggestDirectories","SESSION_FILE_ROUTE","readSessionFile","MAX_SESSION_FILE_BYTES","sessionFileHeaders","SESSION_SOCKET_ROUTE","hubHello","SESSIONS_SNAPSHOT_TYPE","SESSION_UPSERT_TYPE","SESSION_REMOVED_TYPE","parseDoomNotificationEntry","sessionFrameEnvelope","threadFrameEnvelope","threadBacklog","PROTOCOL_SOCKET_ROUTE","resolveAssetPath","contentTypeFor","HUB_RESYNCED_TYPE","shutdownWebTelemetry"],"sources":["../../../src/adapters/httpServer.ts"],"sourcesContent":["import fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { serve } from '@hono/node-server';\nimport { createNodeWebSocket } from '@hono/node-ws';\nimport { PiServer, PiServerError } from '@earendil-works/pi-server';\nimport type { DoomTelemetry } from '@agimon-ai/doompi-telemetry';\nimport { createPiHubService } from './piHubService.ts';\nimport { createSyncGuard } from './syncGuard.ts';\nimport { createPiWebSocketListener } from './piWebSocketListener.ts';\nimport { type Context, Hono } from 'hono';\nimport { getCookie } from 'hono/cookie';\nimport { bodyLimit } from 'hono/body-limit';\nimport type { WebHubChannel } from '@agimon-ai/doompi-web-contracts';\nimport {\n DOOM_API_ROUTE_PREFIX,\n type DoomApi,\n type DoomApiHandler,\n} from '@agimon-ai/doompi-extension-contracts/package-api';\nimport { loadPackageApis } from '@agimon-ai/doompi-extension-contracts/package-api-loader';\nimport { insideSandbox } from '@agimon-ai/doompi-extension-contracts/sandbox-harness';\nimport { sessionFileHeaders } from '../services/fileMedia.ts';\nimport { contentTypeFor, resolveAssetPath } from '../services/staticAssets.ts';\nimport { MAX_SESSION_FILE_BYTES, SESSION_FILE_ROUTE } from '../types/media.ts';\nimport type { WebServer, WebServerOptions } from '../types/bridge.ts';\nimport {\n API_SESSION_QUERY_PARAM,\n DIRECTORIES_API_ROUTE,\n HISTORY_REQUEST_TYPE,\n HUB_PROTOCOL_VERSION,\n HUB_RESYNCED_TYPE,\n HUB_ROLE,\n SESSIONS_API_ROUTE,\n SESSIONS_SNAPSHOT_TYPE,\n SESSION_COMMAND_TYPE,\n SESSION_REMOVED_TYPE,\n SESSION_UPSERT_TYPE,\n SUBSCRIBE_THREAD_TYPE,\n SUBSCRIBE_TYPE,\n UNSUBSCRIBE_THREAD_TYPE,\n UNSUBSCRIBE_TYPE,\n hubHello,\n sessionFrameEnvelope,\n threadBacklog,\n threadFrameEnvelope,\n} from '../types/hub.ts';\nimport { parseDoomNotificationEntry } from '../types/notification.ts';\nimport { ATTACH_TYPE, type SessionFrame } from '../types/session.ts';\nimport { readGitStatus } from './gitStatus.ts';\nimport { watchRegistry } from './registryWatcher.ts';\nimport { createServerSpawner } from './serverSpawner.ts';\nimport { createSessionHub, type SessionHub } from './sessionHub.ts';\nimport { allowedOriginsFromEnv } from '../services/remoteGuardPolicy.ts';\nimport { describeStranded, planSessionMigration } from '../services/sessionMigration.ts';\nimport { registerAuthRoutes } from './authRoutes.ts';\nimport { registerSettingsRoutes } from './settingsRoutes.ts';\nimport { createProviderAuth } from './providerAuth.ts';\nimport { createRemoteGuard } from './remoteGuard.ts';\nimport { createRemoteAccess } from './remoteAccess.ts';\nimport { createRemoteAccessStore } from './remoteAccessStore.ts';\nimport { registerRemoteRoutes } from './remoteRoutes.ts';\nimport { createTunnelLauncher, reapStaleTunnel } from './tunnelProcess.ts';\nimport {\n DEVICE_COOKIE,\n PROTOCOL_SOCKET_ROUTE,\n REMOTE_CHANNEL_ROUTE,\n REMOTE_HTTP_ROUTE,\n SESSION_SOCKET_ROUTE,\n type RemoteAccessSettings,\n} from '../types/remoteAccess.ts';\nimport { suggestDirectories } from './directoryListing.ts';\nimport { listSessionFiles, readSessionFile } from './sessionFiles.ts';\nimport { proxyToSocket } from './packageApiProxy.ts';\nimport { createThreadJournals } from './threadJournals.ts';\nimport { loadHubChannels } from './webHubPluginLoader.ts';\nimport {\n createBrowserTelemetryRateLimit,\n createWebTelemetry,\n forwardedTraceContext,\n parseBrowserPerformanceBatch,\n readTraceContext,\n requestOperation,\n shutdownWebTelemetry,\n WEB_TELEMETRY_ROUTE,\n} from './webTelemetry.ts';\n\n// Pi's protocol rides its own socket so the DoomPi channel keeps the\n// vocabulary the protocol has no shape for: dialogs, minor modes, selection.\n/** Directory suggestions per picker query; more than this means \"type further\". */\nconst DIRECTORY_SUGGESTION_LIMIT = 12;\nconst INDEX_FILE = 'index.html';\n/** Env override for the assets directory, set by launchers that know a synced bundle. */\nconst WEB_DIST_ENV = 'DOOMPI_WEB_DIST';\n/** Comma-separated origins the operator allows past the guard, for dev setups this package cannot guess. */\nconst ALLOW_ORIGIN_ENV = 'DOOMPI_WEB_ALLOW_ORIGIN';\n/** Where `doompi sync` publishes the machine's cockpit bundle. */\nconst SYNCED_WEB_DIRECTORY = ['.doompi', 'web', 'current', 'web'];\nconst SEALED_HTTP_VERSION = 1;\n/** Upper bound for any body accepted from the tunnel before route-specific limits. */\nconst TUNNEL_BODY_BYTES = 8 * 1024 * 1024;\nconst TUNNEL_HEADER_BYTES = 16 * 1024;\nconst TUNNEL_HEADERS_TIMEOUT_MS = 5000;\nconst TUNNEL_REQUEST_TIMEOUT_MS = 10_000;\nconst TUNNEL_CONNECTION_LIMIT = 64;\nconst TUNNEL_REQUESTS_PER_SOCKET = 100;\nconst SEALED_HTTP_METHODS = new Set(['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE']);\nconst SEALED_HTTP_FORBIDDEN_HEADERS = new Set([\n 'connection',\n 'content-length',\n 'cookie',\n 'forwarded',\n 'host',\n 'origin',\n 'transfer-encoding',\n 'upgrade',\n 'x-forwarded-for',\n 'x-forwarded-host',\n 'x-forwarded-proto',\n]);\n\ninterface SealedRequestBindings {\n incoming?: { socket?: { localPort?: number } };\n sealedDeviceId?: string;\n}\n\ninterface SealedHttpRequest {\n v: number;\n method: string;\n target: string;\n headers: Array<[string, string]>;\n body?: string;\n}\n\nfunction parseSealedHttpRequest(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 }\n const target = new URL(request.target, 'http://sealed.doompi.invalid');\n if (target.pathname === REMOTE_HTTP_ROUTE || target.pathname === REMOTE_CHANNEL_ROUTE) return undefined;\n return request as SealedHttpRequest;\n}\n\nfunction sealedRequestHeaders(request: SealedHttpRequest, outer: Context): Headers | undefined {\n try {\n const headers = new Headers();\n for (const [name, value] of request.headers) {\n const normalized = name.toLowerCase();\n if (SEALED_HTTP_FORBIDDEN_HEADERS.has(normalized) || normalized.startsWith('sec-')) continue;\n headers.append(name, value);\n }\n for (const name of ['cookie', 'host', 'origin'] as const) {\n const value = outer.req.header(name);\n if (value !== undefined) headers.set(name, value);\n }\n return headers;\n } catch {\n return undefined;\n }\n}\n\nfunction decodeSealedHttpBody(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}=)?$/.test(value)) return undefined;\n const body = Buffer.from(value, 'base64');\n return body.toString('base64') === value ? body : undefined;\n}\n\n/**\n * Locates the built SPA next to the package that owns this module.\n *\n * Walking to the manifest rather than counting `..` segments keeps this correct\n * whether the code runs from `src` in development or from the unbundled `dist`\n * tree, whose depth is a build detail.\n */\nfunction packagedAssetsDir(): string {\n let dir = path.dirname(fileURLToPath(import.meta.url));\n for (;;) {\n if (fs.existsSync(path.join(dir, 'package.json'))) return path.join(dir, 'dist', 'web');\n const parent = path.dirname(dir);\n if (parent === dir) throw new Error('doompi-web could not locate its own package root.');\n dir = parent;\n }\n}\n\n/**\n * The assets to serve, in preference order: an explicit option, the env\n * override, the bundle `doompi sync` published for this machine (which\n * carries the installed plugin set), then the package's own prebuilt bundle\n * (built-in plugins only).\n */\nfunction resolveAssetsDir(explicit: string | undefined, notice: (message: string) => void): string {\n if (explicit !== undefined) return explicit;\n const fromEnv = process.env[WEB_DIST_ENV];\n if (fromEnv !== undefined && fromEnv !== '') return fromEnv;\n const synced = path.join(os.homedir(), ...SYNCED_WEB_DIRECTORY);\n if (fs.existsSync(path.join(synced, INDEX_FILE))) {\n notice(`serving the synced cockpit bundle from ${synced}`);\n return synced;\n }\n return packagedAssetsDir();\n}\n\nfunction readAsset(filePath: string): Buffer | undefined {\n try {\n const stat = fs.statSync(filePath);\n if (!stat.isFile()) return undefined;\n return fs.readFileSync(filePath);\n } catch {\n return undefined;\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction describeError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/** One page's hold on one thread; a session id never contains a newline, so the pair cannot collide. */\nfunction threadKey(sessionId: string, threadId: string): string {\n return `${sessionId}\\n${threadId}`;\n}\n\nfunction buildHub(\n options: WebServerOptions,\n notice: (message: string) => void,\n channels: readonly WebHubChannel[],\n telemetry: DoomTelemetry,\n): SessionHub {\n return createSessionHub({\n source: watchRegistry(options.registryDir, notice),\n spawner: createServerSpawner({\n registryDir: options.registryDir,\n command: options.spawnCommand,\n onNotice: notice,\n }),\n readGit: readGitStatus,\n channels,\n telemetry,\n onNotice: notice,\n });\n}\n\n/**\n * Mounts the hub-scoped package APIs under /api/plugin/<basePath>/.\n *\n * The prefix is stripped before the API sees the request, so a package\n * declares its routes relative to its own mount and never repeats where a host\n * put it. An API that throws answers 500 for its own routes alone; one bad\n * package never takes the cockpit down with it.\n */\nfunction mountHubApis(app: Hono, apis: readonly DoomApi[], notice: (message: string) => void): DoomApiHandler[] {\n const handlers: DoomApiHandler[] = [];\n for (const api of apis) {\n const mount = `${DOOM_API_ROUTE_PREFIX}/${api.basePath}`;\n let handler: DoomApiHandler;\n try {\n handler = api.start({ scope: 'hub', onNotice: notice });\n } catch (error) {\n notice(`hub API '${api.basePath}' did not start (${describeError(error)}); its routes stay unmounted`);\n continue;\n }\n handlers.push(handler);\n app.all(`${mount}/*`, async (context) => {\n if (context.req.query(API_SESSION_QUERY_PARAM) !== undefined) return context.notFound();\n const url = new URL(context.req.url);\n url.pathname = url.pathname.slice(mount.length) || '/';\n try {\n return await handler.fetch(new Request(url, context.req.raw));\n } catch (error) {\n notice(`hub API '${api.basePath}' failed on ${url.pathname} (${describeError(error)})`);\n return context.json({ error: `The '${api.basePath}' API failed.` }, 500);\n }\n });\n }\n return handlers;\n}\n\n/**\n * Forwards a package API request carrying ?session= to that session's own\n * server, over the API socket its registry record names.\n *\n * The session servers are where session-scoped data lives, but the browser must\n * not talk to them: their attach tokens stay in this process and the page only\n * ever reaches loopback. So the hub is the one door, and this is the hop behind\n * it. A session that never mounted an API answers 404 with the reason rather\n * than leaving the page waiting.\n */\nfunction mountSessionApiProxy(\n app: Hono,\n hub: SessionHub,\n notice: (message: string) => void,\n telemetry: DoomTelemetry,\n): void {\n app.all(`${DOOM_API_ROUTE_PREFIX}/*`, async (context) => {\n const sessionId = context.req.query(API_SESSION_QUERY_PARAM);\n if (sessionId === undefined) return context.notFound();\n const summary = hub.snapshot().find((candidate) => candidate.id === sessionId);\n if (summary === undefined) return context.json({ error: `No session ${sessionId}.` }, 404);\n if (summary.apiSocketPath === undefined) {\n return context.json({ error: `Session ${sessionId} serves no package API.` }, 404);\n }\n const url = new URL(context.req.url);\n url.searchParams.delete(API_SESSION_QUERY_PARAM);\n const parent = readTraceContext(context.req.raw.headers);\n try {\n return await telemetry.runInSpan(\n 'web.package_proxy',\n { 'operation.name': 'web.package_proxy', 'http.method': context.req.method },\n async (trace) =>\n await proxyToSocket({\n socketPath: summary.apiSocketPath as string,\n path: `${url.pathname}${url.search}`,\n method: context.req.method,\n headers: context.req.raw.headers,\n body: context.req.raw.body,\n trace: forwardedTraceContext(trace, parent),\n }),\n parent,\n );\n } catch (error) {\n notice(`session ${sessionId} API is unreachable (${describeError(error)})`);\n return context.json({ error: `Session ${sessionId} is not answering.` }, 502);\n }\n });\n}\n\n/**\n * Serves the cockpit and multiplexes every session behind it.\n *\n * Attach tokens stay in this process. The browser authenticates by reaching a\n * loopback port, so a page never holds a credential that would let it talk to\n * a session directly. One page WebSocket carries all sessions: hub frames\n * describe the set, and session traffic travels enveloped by session id.\n */\nexport async function serveWeb(options: WebServerOptions): Promise<WebServer> {\n const host = options.host ?? '127.0.0.1';\n const notice = options.onNotice ?? ((): void => {});\n const telemetry = createWebTelemetry();\n const assetsDir = resolveAssetsDir(options.assetsDir, notice);\n const hub = buildHub(options, notice, await loadHubChannels(assetsDir, notice), telemetry);\n // Threads are journals the data channels can name (a subagent run's own\n // session file); the hub tails one only while a page follows it.\n const threads = createThreadJournals({\n resolve: (sessionId, threadId) => hub.threadJournal(sessionId, threadId),\n onNotice: notice,\n });\n // A session reads what sync produced, and nothing makes the person who\n // opened the cockpit run sync first. The hub keeps the repository current.\n const syncGuard = createSyncGuard({ repoRoot: process.cwd(), onNotice: notice });\n await syncGuard.ensureSynced();\n /**\n * Every attached page, tagged with the listener it arrived on.\n *\n * The split matters: an approval prompt must reach the host and only the\n * host, because a paired phone that could approve devices would be able to\n * make its own access permanent.\n */\n const pages = new Set<{ post: (frame: object) => void; local: boolean }>();\n const broadcast = (frame: object, localOnly: boolean): void => {\n for (const page of pages) {\n if (localOnly && !page.local) continue;\n page.post(frame);\n }\n };\n\n const app = new Hono();\n const nodeWs = createNodeWebSocket({ app });\n /** Known once the listener binds; until then the guard treats every request as remote. */\n let loopbackPort: number | undefined;\n const store = createRemoteAccessStore({ stateDir: options.remoteStateDir, onNotice: notice });\n reapStaleTunnel(store.directory, notice);\n /**\n * Stands the host's sessions down so the container can take them over.\n *\n * Done here rather than in the launcher because this is where the hub is, and\n * the stop has to happen before the move: a recreated session opens the same\n * working tree, and two agents in one tree would fight over it.\n */\n const handover =\n options.onHandover === undefined\n ? undefined\n : (settings: RemoteAccessSettings): void => {\n const plan = planSessionMigration(\n hub.snapshot().map((session) => ({\n id: session.id,\n cwd: session.cwd,\n ...(session.name === undefined ? {} : { name: session.name }),\n })),\n settings.sandbox.workspaces,\n );\n for (const line of describeStranded(plan.stranded)) notice(line);\n for (const session of plan.migrate) {\n const stopped = hub.stop(session.id);\n if (!stopped.ok) notice(`could not stop ${session.name ?? session.id} before the move: ${stopped.error}`);\n }\n options.onHandover?.({ settings, sessions: plan.migrate });\n };\n\n const remote = createRemoteAccess({\n store,\n launchTunnel:\n options.remoteAccess?.launchTunnel ??\n createTunnelLauncher({\n stateDir: store.directory,\n ...(options.cloudflaredPath === undefined ? {} : { cloudflaredPath: options.cloudflaredPath }),\n onNotice: notice,\n onExit: (message) => {\n notice(`remote access: ${message}`);\n void remote.disable();\n },\n }),\n // The tunnel gets its own loopback socket. That is the only reliable way to\n // tell its traffic from the host's, since cloudflared connects from\n // 127.0.0.1 and forges nothing a header could reveal.\n bindListener: async () =>\n await new Promise((resolve, reject) => {\n const tunnelServer = serve(\n {\n fetch: app.fetch,\n port: 0,\n hostname: '127.0.0.1',\n serverOptions: {\n maxHeaderSize: TUNNEL_HEADER_BYTES,\n headersTimeout: TUNNEL_HEADERS_TIMEOUT_MS,\n requestTimeout: TUNNEL_REQUEST_TIMEOUT_MS,\n },\n },\n (info) => {\n nodeWs.injectWebSocket(tunnelServer);\n resolve({\n port: info.port,\n close: async () =>\n await new Promise<void>((done) => {\n (tunnelServer as { closeAllConnections?: () => void }).closeAllConnections?.();\n tunnelServer.close(() => done());\n }),\n });\n },\n );\n const bounded = tunnelServer as typeof tunnelServer & { maxConnections: number; maxRequestsPerSocket: number };\n bounded.maxConnections = TUNNEL_CONNECTION_LIMIT;\n bounded.maxRequestsPerSocket = TUNNEL_REQUESTS_PER_SOCKET;\n tunnelServer.once('error', reject);\n }),\n onNotice: notice,\n ...(handover === undefined ? {} : { requestHandover: handover }),\n contained: insideSandbox(process.env),\n broadcastLocal: (frame) => broadcast(frame, true),\n broadcastAll: (frame) => broadcast(frame, false),\n ...(options.remoteAccess?.now === undefined ? {} : { now: options.remoteAccess.now }),\n });\n // First route on the app, because Hono composes matching handlers in\n // registration order: a guard added after a terminating handler never runs\n // for that path. It also refuses socket upgrades, which is what closes the\n // cross-site WebSocket hijack that loopback binding never covered.\n const guard = createRemoteGuard({\n loopbackPort: () => loopbackPort,\n tunnelPolicy: () => remote.tunnelPolicy(),\n authorize: (context) => remote.authorize(getCookie(context, DEVICE_COOKIE, 'host')),\n trustedDevice: (context) => (context.env as SealedRequestBindings | undefined)?.sealedDeviceId,\n channelReady: (deviceId, scope) => remote.channelFor(deviceId, scope) !== undefined,\n stepUp: {\n required: (action) => remote.stepUpRequired(action),\n verify: async (context, action, assertion) => {\n if (typeof assertion !== 'object' || assertion === null) return false;\n const ceremonyId = (assertion as { ceremonyId?: unknown }).ceremonyId;\n const response = (assertion as { response?: unknown }).response;\n if (typeof ceremonyId !== 'string') return false;\n const trusted = (context.env as SealedRequestBindings | undefined)?.sealedDeviceId;\n const deviceId = trusted ?? remote.authorize(getCookie(context, DEVICE_COOKIE, 'host'));\n const caller = deviceId === undefined ? 'local' : `device:${deviceId}`;\n return await remote.passkeys().finishStepUp(ceremonyId, caller, action, response);\n },\n },\n extraOrigins: allowedOriginsFromEnv(process.env[ALLOW_ORIGIN_ENV]),\n });\n app.use('*', guard.middleware);\n app.use('*', async (context, next) => {\n const pathname = new URL(context.req.url).pathname;\n if (pathname === REMOTE_HTTP_ROUTE) return next();\n const operation = requestOperation(pathname);\n if (operation === undefined) return next();\n const started = performance.now();\n return await telemetry.runInSpan(\n operation,\n { 'operation.name': operation, 'http.method': context.req.method },\n async () => {\n await next();\n await telemetry.recordEvent('web.api.request', {\n 'operation.name': operation,\n 'http.method': context.req.method,\n 'http.status_code': context.res.status,\n duration_ms: Math.max(0, Math.round(performance.now() - started)),\n });\n },\n readTraceContext(context.req.raw.headers),\n );\n });\n // Reject a declared oversized body before any public route reads it. Chunked\n // bodies are bounded by the route-specific or downstream streaming limiter.\n app.use('*', async (context, next) => {\n if (guard.listenerOf(context) === 'local') return next();\n const declared = Number(context.req.header('content-length'));\n if (Number.isFinite(declared) && declared > TUNNEL_BODY_BYTES) {\n return context.json({ error: 'The tunnel request body is too large.' }, 413);\n }\n return next();\n });\n registerRemoteRoutes(app, { remote, listenerOf: (context) => guard.listenerOf(context) });\n const tunnelBodyLimit = bodyLimit({\n maxSize: TUNNEL_BODY_BYTES,\n onError: (context) => context.json({ error: 'The tunnel request body is too large.' }, 413),\n });\n app.use('*', async (context, next) => {\n if (guard.listenerOf(context) === 'local') return next();\n return await tunnelBodyLimit(context, next);\n });\n const browserTelemetryBodyLimit = bodyLimit({\n maxSize: 8 * 1024,\n onError: (context) => context.json({ error: 'The telemetry batch is too large.' }, 413),\n });\n const acceptBrowserTelemetryRequest = createBrowserTelemetryRateLimit();\n app.post(WEB_TELEMETRY_ROUTE, browserTelemetryBodyLimit, async (context) => {\n if (!acceptBrowserTelemetryRequest()) return context.json({ error: 'Too many telemetry batches.' }, 429);\n let body: unknown;\n try {\n body = await context.req.json();\n } catch {\n return context.json({ error: 'The telemetry batch must be JSON.' }, 400);\n }\n const events = parseBrowserPerformanceBatch(body);\n if (events === undefined) return context.json({ error: 'The telemetry batch is invalid.' }, 400);\n for (const event of events) {\n await telemetry.recordEvent(event.name, {\n ...(event.duration_ms === undefined ? {} : { duration_ms: event.duration_ms }),\n ...(event.count === undefined ? {} : { count: event.count }),\n });\n }\n return context.body(null, 204);\n });\n app.post(REMOTE_HTTP_ROUTE, async (context) => {\n const deviceId = remote.authorize(getCookie(context, DEVICE_COOKIE, 'host'));\n if (deviceId === undefined) return context.json({ error: 'This device is not paired.' }, 401);\n const channel = remote.channelFor(deviceId, 'http');\n if (channel === undefined) return context.json({ error: 'This device has no sealed HTTP channel.' }, 401);\n\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\n let decoded: unknown;\n try {\n decoded = JSON.parse(new TextDecoder().decode(opened.plaintext));\n } catch {\n return context.json({ error: 'The sealed HTTP request was malformed.' }, 400);\n }\n const request = parseSealedHttpRequest(decoded);\n const headers = request === undefined ? undefined : sealedRequestHeaders(request, context);\n const body = request === undefined ? undefined : decodeSealedHttpBody(request.body);\n if (request === undefined || headers === undefined || body === undefined) {\n return context.json({ error: 'The sealed HTTP request was invalid.' }, 400);\n }\n\n const target = new URL(request.target, new URL(context.req.url).origin);\n const innerResponse = await app.request(\n target,\n {\n method: request.method,\n headers,\n ...(request.method === 'GET' || request.method === 'HEAD' ? {} : { body: Uint8Array.from(body).buffer }),\n },\n { ...(context.env as SealedRequestBindings), sealedDeviceId: deviceId },\n );\n const responseBody = Buffer.from(await innerResponse.arrayBuffer());\n const sealed = channel.seal(\n new TextEncoder().encode(\n JSON.stringify({\n v: SEALED_HTTP_VERSION,\n status: innerResponse.status,\n headers: Array.from(innerResponse.headers.entries()),\n body: responseBody.toString('base64'),\n }),\n ),\n );\n if (!sealed.ok) return context.json({ error: `The sealed response failed: ${sealed.failure}.` }, 503);\n return context.json(sealed.envelope);\n });\n // Every running session already serves Pi's protocol; the hub composes them\n // into one server so a browser sees a single endpoint with many sessions.\n // Remote clients get a distinct protocol service that can attach to existing\n // sessions but cannot bypass HTTP step-up by creating one directly.\n const localProtocolListener = createPiWebSocketListener({ onError: (error) => notice(`protocol: ${error.message}`) });\n const remoteProtocolListener = createPiWebSocketListener({\n onError: (error) => notice(`protocol: ${error.message}`),\n });\n const protocolService = createPiHubService({\n records: () => hub.records(),\n spawn: (input) => hub.create(input),\n onNotice: notice,\n });\n const remoteProtocolService = {\n ...protocolService,\n createSession: async () => {\n throw new PiServerError('not_implemented', 'Remote protocol clients cannot create sessions');\n },\n };\n const localProtocolServer = new PiServer(protocolService, {\n listeners: [localProtocolListener],\n onError: (error) => notice(`protocol: ${error.message}`),\n });\n const remoteProtocolServer = new PiServer(remoteProtocolService, {\n listeners: [remoteProtocolListener],\n onError: (error) => notice(`protocol: ${error.message}`),\n });\n await Promise.all([localProtocolServer.start(), remoteProtocolServer.start()]);\n // Provider credentials belong to the machine, not to a session: the hub\n // keeps one Pi runtime over the shared auth.json and signs in for all.\n const providerAuth = createProviderAuth({ runtime: options.authRuntime, onNotice: notice });\n registerAuthRoutes(app, providerAuth);\n // Settings read and write the machine's Doom config. The page names a\n // repository rather than a session, so the picker is fed from the working\n // directories the hub already manages.\n registerSettingsRoutes(app, {\n repositories: () => {\n const seen = new Map<string, { path: string; name: string; active: boolean }>();\n for (const record of hub.records()) {\n seen.set(record.cwd, { path: record.cwd, name: path.basename(record.cwd) || record.cwd, active: true });\n }\n return [...seen.values()].sort((left, right) => left.path.localeCompare(right.path));\n },\n models: () => providerAuth.listModels(),\n });\n // Package APIs, mounted before the SPA fallback so their routes are reachable\n // and everything else still falls through to the bundle. Hub-scoped ones run\n // here; session-scoped ones live in each session's own server and are proxied.\n const pluginApis = mountHubApis(app, await loadPackageApis('hub', { onNotice: notice }), notice);\n mountSessionApiProxy(app, hub, notice, telemetry);\n\n app.get('/api/health', (context) =>\n context.json({\n ok: true,\n role: HUB_ROLE,\n protocol: HUB_PROTOCOL_VERSION,\n sessions: hub.snapshot().length,\n pid: process.pid,\n }),\n );\n\n app.post(SESSIONS_API_ROUTE, async (context) => {\n let body: unknown;\n try {\n body = await context.req.json();\n } catch {\n return context.json({ error: 'The request body must be JSON.' }, 400);\n }\n if (!isRecord(body) || typeof body.cwd !== 'string' || body.cwd === '') {\n return context.json({ error: 'A cwd string is required.' }, 400);\n }\n const name = typeof body.name === 'string' && body.name !== '' ? body.name : undefined;\n await syncGuard.ensureSynced();\n const outcome = await hub.create({ cwd: body.cwd, name, trace: readTraceContext(context.req.raw.headers) });\n if (outcome.ok) return context.json({ sessionId: outcome.sessionId }, 201);\n return context.json({ error: outcome.error }, outcome.code === 'invalid_request' ? 400 : 502);\n });\n\n // A running server reads the composition once: its extensions when the agent\n // starts, its package API routes when the process does. So a rebuild reaches\n // an existing session only by replacing the process, and the sync has to\n // finish before the replacement starts or it reads the same stale artifacts.\n app.post(`${SESSIONS_API_ROUTE}/:sessionId/restart`, async (context) => {\n const sessionId = context.req.param('sessionId');\n await syncGuard.ensureSynced();\n const outcome = await hub.restart(sessionId, readTraceContext(context.req.raw.headers));\n if (outcome.ok) return context.json({ sessionId: outcome.sessionId }, 202);\n return context.json({ error: outcome.error }, outcome.code === 'invalid_request' ? 404 : 502);\n });\n\n app.delete(`${SESSIONS_API_ROUTE}/:sessionId`, (context) => {\n const sessionId = context.req.param('sessionId');\n const outcome = hub.stop(sessionId);\n if (outcome.ok) return context.json({ sessionId }, 202);\n return context.json(\n { error: outcome.error },\n outcome.code === 'unknown' ? 404 : outcome.code === 'self' ? 409 : 502,\n );\n });\n\n // File completion for the composer's @ references: bounded, cwd-scoped,\n // and only for sessions the hub actually manages.\n app.get('/api/sessions/:sessionId/files', async (context) => {\n const sessionId = context.req.param('sessionId');\n const summary = hub.snapshot().find((candidate) => candidate.id === sessionId);\n if (!summary) return context.json({ error: 'Unknown session.' }, 404);\n const query = context.req.query('q') ?? '';\n const files = await listSessionFiles(summary.cwd, query, 20);\n return context.json({ files });\n });\n\n /**\n * The subtree directory suggestions may name, or undefined for all of them.\n *\n * A paired device can ask this route, and answering from the whole home\n * directory hands it a map of the machine: every project, every client name,\n * every checkout. So a request arriving on the tunnel is pinned to the\n * directory the cockpit was started from, which is the one the person running\n * it already chose.\n *\n * Only that request. The person at this keyboard already has a shell, so\n * pinning them would cost the picker its usefulness and buy nothing. A\n * contained cockpit needs none of it either, because its mounts are the\n * boundary and nothing outside them is visible to it at all.\n */\n const browseRoot = (context: Context): string | undefined => {\n if (guard.listenerOf(context) === 'local' || insideSandbox(process.env)) return undefined;\n return options.browseRoot ?? process.cwd();\n };\n\n // Directory suggestions for the new-session picker: the children of the\n // typed parent while a path is being drilled into, and a ranked search of\n // the home directory for anything else, so a name or a path remembered from\n // another machine still finds the folder.\n app.get(DIRECTORIES_API_ROUTE, async (context) => {\n const root = browseRoot(context);\n const directories = await suggestDirectories(context.req.query('q') ?? '', {\n limit: DIRECTORY_SUGGESTION_LIMIT,\n ...(root === undefined ? {} : { root }),\n });\n return context.json({ directories });\n });\n\n // The timeline's @file previews: one cwd-contained file, capped in size.\n app.get(SESSION_FILE_ROUTE, async (context) => {\n const sessionId = context.req.param('sessionId');\n const summary = hub.snapshot().find((candidate) => candidate.id === sessionId);\n if (!summary) return context.json({ error: 'Unknown session.' }, 404);\n const relativePath = context.req.query('path') ?? '';\n const file = await readSessionFile(summary.cwd, relativePath, MAX_SESSION_FILE_BYTES);\n if (file.status === 'forbidden') return context.json({ error: 'The path leaves the session directory.' }, 403);\n if (file.status === 'not-found') return context.json({ error: 'No such file.' }, 404);\n if (file.status === 'too-large') return context.json({ error: 'The file is too large to preview.' }, 413);\n return context.body(new Uint8Array(file.body), 200, sessionFileHeaders(relativePath));\n });\n\n app.get(\n SESSION_SOCKET_ROUTE,\n nodeWs.upgradeWebSocket((context) => {\n const local = guard.listenerOf(context) === 'local';\n // Resolved once at upgrade: the device is fixed for the socket's life, and\n // looking it up per frame would bump last-seen on every keystroke.\n const deviceId = local ? undefined : remote.authorize(getCookie(context, DEVICE_COOKIE, 'host'));\n const channel = deviceId === undefined ? undefined : remote.channelFor(deviceId, 'session');\n const subscriptions = new Set<string>();\n /** The threads this page follows; one socket may follow several of one session. */\n const threadSubscriptions = new Map<string, { sessionId: string; threadId: string }>();\n let disconnect: (() => void) | undefined;\n let disconnectThreads: (() => void) | undefined;\n /** Held on the socket, not inside onOpen, so close can withdraw it. */\n let registered: { post: (frame: object) => void; local: boolean } | undefined;\n /** Withdraws this socket from the remote registry, so switch-off can close it. */\n let untrack: (() => void) | undefined;\n /** Lets go of every followed thread, or only a departed session's. */\n const releaseThreads = (sessionId?: string): void => {\n for (const [key, held] of threadSubscriptions) {\n if (sessionId !== undefined && held.sessionId !== sessionId) continue;\n threadSubscriptions.delete(key);\n threads.unsubscribe(held.sessionId, held.threadId);\n }\n };\n return {\n onOpen(_event, ws) {\n if (!local && channel === undefined) {\n ws.close(1008, 'sealed session channel required');\n return;\n }\n const post = (frame: SessionFrame | object): void => {\n const text = JSON.stringify(frame);\n let outgoing = text;\n if (!local) {\n if (channel === undefined) return;\n const sealed = channel.seal(new TextEncoder().encode(text));\n if (!sealed.ok) {\n ws.close(1008, 'sealed session channel failed');\n return;\n }\n outgoing = JSON.stringify(sealed.envelope);\n }\n try {\n ws.send(outgoing);\n } catch {\n // The browser went away mid-write; onClose tears the socket down.\n }\n };\n registered = { post, local };\n pages.add(registered);\n // A socket that has upgraded has left the HTTP server's connection\n // tracking, so closing the tunnel listener does not reach it.\n // Without this a paired phone keeps driving the agent after remote\n // access is switched off.\n if (deviceId !== undefined) untrack = remote.trackSocket(deviceId, (code, reason) => ws.close(code, reason));\n post(hubHello(hub.channelTypes()));\n post({ type: SESSIONS_SNAPSHOT_TYPE, sessions: hub.snapshot() });\n disconnect = hub.onEvent((event) => {\n if (event.kind === 'upsert') post({ type: SESSION_UPSERT_TYPE, session: event.session });\n else if (event.kind === 'removed') {\n subscriptions.delete(event.sessionId);\n releaseThreads(event.sessionId);\n post({ type: SESSION_REMOVED_TYPE, sessionId: event.sessionId });\n } else if (event.kind === 'channel') {\n if (subscriptions.has(event.sessionId)) {\n post({ type: event.frameType, sessionId: event.sessionId, payload: event.payload });\n }\n } else {\n const notification = parseDoomNotificationEntry(event.frame);\n if (notification !== undefined || subscriptions.has(event.sessionId)) {\n post(sessionFrameEnvelope(event.sessionId, event.frame));\n }\n }\n });\n disconnectThreads = threads.onFrame((event) => {\n if (threadSubscriptions.has(threadKey(event.sessionId, event.threadId))) {\n post(threadFrameEnvelope(event.sessionId, event.threadId, event.frame));\n }\n });\n notice('browser attached');\n },\n onMessage(event) {\n if (typeof event.data !== 'string') return;\n let parsed: unknown;\n try {\n parsed = JSON.parse(event.data);\n } catch {\n return;\n }\n if (!local) {\n if (channel === undefined) return;\n const opened = channel.open(parsed);\n // A frame that will not open was altered or replayed. Dropping it\n // is the only safe answer; there is no plaintext to fall back to.\n if (!opened.ok) return;\n try {\n parsed = JSON.parse(new TextDecoder().decode(opened.plaintext));\n } catch {\n return;\n }\n }\n if (!isRecord(parsed) || typeof parsed.sessionId !== 'string') return;\n const sessionId = parsed.sessionId;\n if (parsed.type === SUBSCRIBE_TYPE) {\n const backlog = hub.backlog(sessionId);\n if (!backlog) return; // Unknown session; the snapshot said otherwise.\n subscriptions.add(sessionId);\n registered?.post(backlog);\n for (const frame of hub.channelFrames(sessionId)) registered?.post(frame);\n return;\n }\n if (parsed.type === UNSUBSCRIBE_TYPE) {\n subscriptions.delete(sessionId);\n return;\n }\n if (parsed.type === HISTORY_REQUEST_TYPE) {\n // Older transcript, on demand. The hub kept what the attach path\n // was too small to publish, so scrolling back reads from memory\n // rather than asking the session to re-read its journal.\n const page = hub.history(sessionId, {\n ...(typeof parsed.before === 'string' ? { before: parsed.before } : {}),\n ...(typeof parsed.limit === 'number' ? { limit: parsed.limit } : {}),\n });\n if (!page) return;\n registered?.post(page);\n return;\n }\n if (parsed.type === SUBSCRIBE_THREAD_TYPE || parsed.type === UNSUBSCRIBE_THREAD_TYPE) {\n if (typeof parsed.threadId !== 'string') return;\n const threadId = parsed.threadId;\n const key = threadKey(sessionId, threadId);\n if (parsed.type === UNSUBSCRIBE_THREAD_TYPE) {\n if (threadSubscriptions.delete(key)) threads.unsubscribe(sessionId, threadId);\n return;\n }\n if (threadSubscriptions.has(key) || hub.backlog(sessionId) === undefined) return;\n threadSubscriptions.set(key, { sessionId, threadId });\n registered?.post(threadBacklog(sessionId, threadId, threads.subscribe(sessionId, threadId)));\n return;\n }\n if (parsed.type === SESSION_COMMAND_TYPE && isRecord(parsed.frame)) {\n // The hub owns the handshake; a page must not be able to replay it.\n if (parsed.frame.type === ATTACH_TYPE) return;\n hub.command(sessionId, parsed.frame);\n }\n },\n onClose() {\n if (registered) pages.delete(registered);\n registered = undefined;\n untrack?.();\n untrack = undefined;\n disconnect?.();\n disconnect = undefined;\n disconnectThreads?.();\n disconnectThreads = undefined;\n subscriptions.clear();\n releaseThreads();\n notice('browser detached');\n },\n };\n }),\n );\n\n app.get(\n PROTOCOL_SOCKET_ROUTE,\n nodeWs.upgradeWebSocket((context) => {\n const local = guard.listenerOf(context) === 'local';\n const deviceId = local ? undefined : remote.authorize(getCookie(context, DEVICE_COOKIE, 'host'));\n const channel = deviceId === undefined ? undefined : remote.channelFor(deviceId, 'protocol');\n let handler: ReturnType<typeof localProtocolListener.accept>;\n let untrack: (() => void) | undefined;\n return {\n onOpen(_event, ws) {\n if (!local && channel === undefined) {\n ws.close(1008, 'sealed protocol channel required');\n return;\n }\n if (deviceId !== undefined) untrack = remote.trackSocket(deviceId, (code, reason) => ws.close(code, reason));\n handler = (local ? localProtocolListener : remoteProtocolListener).accept({\n send: (data) => {\n if (local) {\n ws.send(data as ArrayBuffer);\n return;\n }\n if (channel === undefined) return;\n const bytes = data instanceof Uint8Array ? data : new Uint8Array(data as ArrayBuffer);\n const sealed = channel.seal(bytes);\n if (!sealed.ok) {\n ws.close(1008, 'sealed protocol channel failed');\n return;\n }\n ws.send(new TextEncoder().encode(JSON.stringify(sealed.envelope)));\n },\n close: () => ws.close(),\n get readyState() {\n return ws.readyState;\n },\n });\n },\n async onMessage(event) {\n const data = event.data;\n if (typeof data === 'string') return;\n const bytes =\n data instanceof Blob\n ? new Uint8Array(await data.arrayBuffer())\n : data instanceof ArrayBuffer\n ? new Uint8Array(data)\n : new Uint8Array(data);\n if (local) {\n handler?.onData(bytes);\n return;\n }\n if (channel === undefined) return;\n let envelope: unknown;\n try {\n envelope = JSON.parse(new TextDecoder().decode(bytes));\n } catch {\n return;\n }\n const opened = channel.open(envelope);\n if (opened.ok) handler?.onData(opened.plaintext);\n },\n onClose() {\n untrack?.();\n untrack = undefined;\n handler?.onClose();\n handler = undefined;\n },\n onError(event) {\n handler?.onError(event instanceof Error ? event : new Error('The protocol socket failed'));\n handler = undefined;\n },\n };\n }),\n );\n\n app.get('*', (context) => {\n const requested = new URL(context.req.url).pathname;\n const direct = requested === '/' ? undefined : resolveAssetPath(assetsDir, requested);\n const body = direct ? readAsset(direct) : undefined;\n if (direct && body) {\n return context.body(new Uint8Array(body), 200, { 'Content-Type': contentTypeFor(direct) });\n }\n const index = readAsset(path.join(assetsDir, INDEX_FILE));\n if (!index) return context.text('The cockpit bundle is missing. Run the package build.', 500);\n return context.body(new Uint8Array(index), 200, { 'Content-Type': 'text/html; charset=utf-8' });\n });\n\n // A rebuilt bundle only changes on disk, so the page it replaced has to be\n // told; nothing about a loaded bundle notices that its source moved.\n syncGuard.watch(() => {\n broadcast({ type: HUB_RESYNCED_TYPE }, false);\n });\n\n return new Promise<WebServer>((resolve, reject) => {\n const server = serve({ fetch: app.fetch, port: options.port, hostname: host }, (info) => {\n // Before this the guard has no loopback port to compare against and\n // treats everything as remote, which is the safe direction to be wrong in.\n loopbackPort = info.port;\n nodeWs.injectWebSocket(server);\n const url = `http://${host}:${info.port}`;\n notice(`cockpit on ${url}`);\n let closePromise: Promise<void> | undefined;\n const close = async (): Promise<void> => {\n // Remote access first, so the tunnel is down and every paired socket\n // is closed before the rest of the hub starts letting go.\n await remote.close();\n await new Promise<void>((done) => {\n threads.close();\n syncGuard.close();\n void localProtocolServer.close();\n void remoteProtocolServer.close();\n hub.close();\n providerAuth.close();\n for (const handler of pluginApis) handler.close();\n // An upgraded socket leaves the HTTP server's connection tracking,\n // so only the WebSocket server can let go of it. Without this the\n // close callback waits on a browser that has no reason to leave.\n for (const client of nodeWs.wss.clients) client.terminate();\n nodeWs.wss.close();\n (server as { closeAllConnections?: () => void }).closeAllConnections?.();\n server.close(() => done());\n });\n await shutdownWebTelemetry(telemetry);\n };\n resolve({\n url,\n port: info.port,\n close: () => (closePromise ??= close()),\n });\n });\n server.once('error', (error) => {\n threads.close();\n hub.close();\n providerAuth.close();\n void remote.close();\n for (const handler of pluginApis) handler.close();\n void telemetry.shutdown();\n reject(error);\n });\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"httpServer.cjs","names":["REMOTE_HTTP_ROUTE","REMOTE_CHANNEL_ROUTE","path","fileURLToPath","fs","os","createSessionHub","watchRegistry","createServerSpawner","readGitStatus","createHash","findRepositoryRoot","DOOM_API_ROUTE_PREFIX","API_SESSION_QUERY_PARAM","readTraceContext","proxyToSocket","forwardedTraceContext","createWebTelemetry","loadHubChannels","createThreadJournals","createSyncGuard","Hono","createNodeWebSocket","createRemoteAccessStore","planSessionMigration","describeStranded","createRemoteAccess","createTunnelLauncher","serve","insideSandbox","createRemoteGuard","getCookie","DEVICE_COOKIE","allowedOriginsFromEnv","requestOperation","bodyLimit","createBrowserTelemetryRateLimit","WEB_TELEMETRY_ROUTE","parseBrowserPerformanceBatch","createPiWebSocketListener","createPiHubService","PiServerError","PiServer","createProviderAuth","readSyncDrift","readSyncState","loadPackageApis","SESSIONS_API_ROUTE","listSessionFiles","DIRECTORIES_API_ROUTE","suggestDirectories","SESSION_FILE_ROUTE","readSessionFile","MAX_SESSION_FILE_BYTES","sessionFileHeaders","SESSION_SOCKET_ROUTE","randomUUID","hubHello","SESSIONS_SNAPSHOT_TYPE","SESSION_UPSERT_TYPE","SESSION_REMOVED_TYPE","parseDoomNotificationEntry","sessionFrameEnvelope","threadFrameEnvelope","threadBacklog","PROTOCOL_SOCKET_ROUTE","resolveAssetPath","contentTypeFor","HUB_RESYNCED_TYPE","shutdownWebTelemetry"],"sources":["../../../src/adapters/httpServer.ts"],"sourcesContent":["import { createHash, randomUUID } from 'node:crypto';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { serve } from '@hono/node-server';\nimport { createNodeWebSocket } from '@hono/node-ws';\nimport { PiServer, PiServerError } from '@earendil-works/pi-server';\nimport { readSyncDrift } from '@agimon-ai/doompi/services';\nimport { readSyncState } from '@agimon-ai/doompi/services/syncState';\nimport type { DoomTelemetry } from '@agimon-ai/doompi-telemetry';\nimport { createPiHubService } from './piHubService.ts';\nimport { createSyncGuard } from './syncGuard.ts';\nimport { createPiWebSocketListener } from './piWebSocketListener.ts';\nimport { type Context, Hono } from 'hono';\nimport { getCookie } from 'hono/cookie';\nimport { bodyLimit } from 'hono/body-limit';\nimport type { WebHubChannel } from '@agimon-ai/doompi-web-contracts';\nimport {\n DOOM_API_ROUTE_PREFIX,\n type DoomApi,\n type DoomApiHandler,\n type DoomRepositorySyncView,\n} from '@agimon-ai/doompi-extension-contracts/package-api';\nimport { loadPackageApis } from '@agimon-ai/doompi-extension-contracts/package-api-loader';\nimport { insideSandbox } from '@agimon-ai/doompi-extension-contracts/sandbox-harness';\nimport { findRepositoryRoot } from '@agimon-ai/doompi/utils/repository';\nimport { sessionFileHeaders } from '../services/fileMedia.ts';\nimport { contentTypeFor, resolveAssetPath } from '../services/staticAssets.ts';\nimport { MAX_SESSION_FILE_BYTES, SESSION_FILE_ROUTE } from '../types/media.ts';\nimport type { SettingsRepository } from '../types/settings.ts';\nimport type { WebServer, WebServerOptions } from '../types/bridge.ts';\nimport {\n API_SESSION_QUERY_PARAM,\n DIRECTORIES_API_ROUTE,\n HISTORY_REQUEST_TYPE,\n HUB_PROTOCOL_VERSION,\n HUB_RESYNCED_TYPE,\n HUB_ROLE,\n SESSIONS_API_ROUTE,\n SESSIONS_SNAPSHOT_TYPE,\n SESSION_COMMAND_TYPE,\n SESSION_REMOVED_TYPE,\n SESSION_UPSERT_TYPE,\n SUBSCRIBE_THREAD_TYPE,\n SUBSCRIBE_TYPE,\n UNSUBSCRIBE_THREAD_TYPE,\n UNSUBSCRIBE_TYPE,\n hubHello,\n sessionFrameEnvelope,\n threadBacklog,\n threadFrameEnvelope,\n} from '../types/hub.ts';\nimport { parseDoomNotificationEntry } from '../types/notification.ts';\nimport { ATTACH_TYPE, type SessionFrame } from '../types/session.ts';\nimport { readGitStatus } from './gitStatus.ts';\nimport { watchRegistry } from './registryWatcher.ts';\nimport { createServerSpawner } from './serverSpawner.ts';\nimport { createSessionHub, type SessionHub } from './sessionHub.ts';\nimport { allowedOriginsFromEnv } from '../services/remoteGuardPolicy.ts';\nimport { describeStranded, planSessionMigration } from '../services/sessionMigration.ts';\nimport { registerAuthRoutes } from './authRoutes.ts';\nimport { registerSettingsRoutes } from './settingsRoutes.ts';\nimport { createProviderAuth } from './providerAuth.ts';\nimport { createRemoteGuard } from './remoteGuard.ts';\nimport { createRemoteAccess } from './remoteAccess.ts';\nimport { createRemoteAccessStore } from './remoteAccessStore.ts';\nimport { registerRemoteRoutes } from './remoteRoutes.ts';\nimport { createTunnelLauncher, reapStaleTunnel } from './tunnelProcess.ts';\nimport {\n DEVICE_COOKIE,\n PROTOCOL_SOCKET_ROUTE,\n REMOTE_CHANNEL_ROUTE,\n REMOTE_HTTP_ROUTE,\n SESSION_SOCKET_ROUTE,\n type RemoteAccessSettings,\n} from '../types/remoteAccess.ts';\nimport { suggestDirectories } from './directoryListing.ts';\nimport { listSessionFiles, readSessionFile } from './sessionFiles.ts';\nimport { proxyToSocket } from './packageApiProxy.ts';\nimport { createThreadJournals } from './threadJournals.ts';\nimport { loadHubChannels } from './webHubPluginLoader.ts';\nimport {\n createBrowserTelemetryRateLimit,\n createWebTelemetry,\n forwardedTraceContext,\n parseBrowserPerformanceBatch,\n readTraceContext,\n requestOperation,\n shutdownWebTelemetry,\n WEB_TELEMETRY_ROUTE,\n} from './webTelemetry.ts';\n\n// Pi's protocol rides its own socket so the DoomPi channel keeps the\n// vocabulary the protocol has no shape for: dialogs, minor modes, selection.\n/** Directory suggestions per picker query; more than this means \"type further\". */\nconst DIRECTORY_SUGGESTION_LIMIT = 12;\nconst INDEX_FILE = 'index.html';\n/** Env override for the assets directory, set by launchers that know a synced bundle. */\nconst WEB_DIST_ENV = 'DOOMPI_WEB_DIST';\n/** Comma-separated origins the operator allows past the guard, for dev setups this package cannot guess. */\nconst ALLOW_ORIGIN_ENV = 'DOOMPI_WEB_ALLOW_ORIGIN';\n/** Where `doompi sync` publishes the machine's cockpit bundle. */\nconst SYNCED_WEB_DIRECTORY = ['.doompi', 'web', 'current', 'web'];\nconst SEALED_HTTP_VERSION = 1;\n/** Upper bound for any body accepted from the tunnel before route-specific limits. */\nconst TUNNEL_BODY_BYTES = 8 * 1024 * 1024;\nconst TUNNEL_HEADER_BYTES = 16 * 1024;\nconst TUNNEL_HEADERS_TIMEOUT_MS = 5000;\nconst TUNNEL_REQUEST_TIMEOUT_MS = 10_000;\nconst TUNNEL_CONNECTION_LIMIT = 64;\nconst TUNNEL_REQUESTS_PER_SOCKET = 100;\nconst SEALED_HTTP_METHODS = new Set(['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE']);\nconst SEALED_HTTP_FORBIDDEN_HEADERS = new Set([\n 'connection',\n 'content-length',\n 'cookie',\n 'forwarded',\n 'host',\n 'origin',\n 'transfer-encoding',\n 'upgrade',\n 'x-forwarded-for',\n 'x-forwarded-host',\n 'x-forwarded-proto',\n]);\n\ninterface SealedRequestBindings {\n incoming?: { socket?: { localPort?: number } };\n sealedDeviceId?: string;\n}\n\ninterface SealedHttpRequest {\n v: number;\n method: string;\n target: string;\n headers: Array<[string, string]>;\n body?: string;\n}\n\nfunction parseSealedHttpRequest(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 }\n const target = new URL(request.target, 'http://sealed.doompi.invalid');\n if (target.pathname === REMOTE_HTTP_ROUTE || target.pathname === REMOTE_CHANNEL_ROUTE) return undefined;\n return request as SealedHttpRequest;\n}\n\nfunction sealedRequestHeaders(request: SealedHttpRequest, outer: Context): Headers | undefined {\n try {\n const headers = new Headers();\n for (const [name, value] of request.headers) {\n const normalized = name.toLowerCase();\n if (SEALED_HTTP_FORBIDDEN_HEADERS.has(normalized) || normalized.startsWith('sec-')) continue;\n headers.append(name, value);\n }\n for (const name of ['cookie', 'host', 'origin'] as const) {\n const value = outer.req.header(name);\n if (value !== undefined) headers.set(name, value);\n }\n return headers;\n } catch {\n return undefined;\n }\n}\n\nfunction decodeSealedHttpBody(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}=)?$/.test(value)) return undefined;\n const body = Buffer.from(value, 'base64');\n return body.toString('base64') === value ? body : undefined;\n}\n\n/**\n * Locates the built SPA next to the package that owns this module.\n *\n * Walking to the manifest rather than counting `..` segments keeps this correct\n * whether the code runs from `src` in development or from the unbundled `dist`\n * tree, whose depth is a build detail.\n */\nfunction packagedAssetsDir(): string {\n let dir = path.dirname(fileURLToPath(import.meta.url));\n for (;;) {\n if (fs.existsSync(path.join(dir, 'package.json'))) return path.join(dir, 'dist', 'web');\n const parent = path.dirname(dir);\n if (parent === dir) throw new Error('doompi-web could not locate its own package root.');\n dir = parent;\n }\n}\n\n/**\n * The assets to serve, in preference order: an explicit option, the env\n * override, the bundle `doompi sync` published for this machine (which\n * carries the installed plugin set), then the package's own prebuilt bundle\n * (built-in plugins only).\n */\nfunction resolveAssetsDir(explicit: string | undefined, notice: (message: string) => void): string {\n if (explicit !== undefined) return explicit;\n const fromEnv = process.env[WEB_DIST_ENV];\n if (fromEnv !== undefined && fromEnv !== '') return fromEnv;\n const synced = path.join(os.homedir(), ...SYNCED_WEB_DIRECTORY);\n if (fs.existsSync(path.join(synced, INDEX_FILE))) {\n notice(`serving the synced cockpit bundle from ${synced}`);\n return synced;\n }\n return packagedAssetsDir();\n}\n\nfunction readAsset(filePath: string): Buffer | undefined {\n try {\n const stat = fs.statSync(filePath);\n if (!stat.isFile()) return undefined;\n return fs.readFileSync(filePath);\n } catch {\n return undefined;\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction describeError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/** One page's hold on one thread; a session id never contains a newline, so the pair cannot collide. */\nfunction threadKey(sessionId: string, threadId: string): string {\n return `${sessionId}\\n${threadId}`;\n}\n\nfunction buildHub(\n options: WebServerOptions,\n notice: (message: string) => void,\n channels: readonly WebHubChannel[],\n telemetry: DoomTelemetry,\n): SessionHub {\n return createSessionHub({\n source: watchRegistry(options.registryDir, notice),\n spawner: createServerSpawner({\n registryDir: options.registryDir,\n command: options.spawnCommand,\n onNotice: notice,\n }),\n readGit: readGitStatus,\n channels,\n telemetry,\n onNotice: notice,\n });\n}\n\n/** Stable browser identity for an admitted canonical repository root. */\nfunction repositoryId(root: string): string {\n return `repo-${createHash('sha256').update(root).digest('base64url').slice(0, 24)}`;\n}\n\n/** Active repositories plus a bounded set seen earlier during this hub process. */\nexport function createSettingsRepositoryRegistry(hub: SessionHub): {\n list(): SettingsRepository[];\n resolve(repositoryId: string): string | undefined;\n} {\n const recent = new Map<string, { repository: SettingsRepository; seenAt: number }>();\n const refresh = (): SettingsRepository[] => {\n for (const [root, entry] of recent) {\n recent.set(root, { ...entry, repository: { ...entry.repository, active: false } });\n }\n for (const record of hub.records()) {\n try {\n const root = fs.realpathSync(findRepositoryRoot(record.cwd));\n recent.set(root, {\n repository: { id: repositoryId(root), path: root, name: path.basename(root) || root, active: true },\n seenAt: Date.now(),\n });\n } catch {\n // A session outside a marked repository is valid, but has no repository settings surface.\n }\n }\n const inactive = [...recent.entries()]\n .filter(([, entry]) => !entry.repository.active)\n .sort((left, right) => right[1].seenAt - left[1].seenAt);\n for (const [root] of inactive.slice(12)) recent.delete(root);\n return [...recent.values()]\n .sort(\n (left, right) =>\n Number(right.repository.active) - Number(left.repository.active) ||\n right.seenAt - left.seenAt ||\n left.repository.path.localeCompare(right.repository.path),\n )\n .map((entry) => entry.repository);\n };\n return {\n list: refresh,\n resolve(repositoryId) {\n return refresh().find((entry) => entry.id === repositoryId)?.path;\n },\n };\n}\n\n/**\n * Mounts the hub-scoped package APIs under /api/plugin/<basePath>/.\n *\n * The prefix is stripped before the API sees the request, so a package\n * declares its routes relative to its own mount and never repeats where a host\n * put it. An API that throws answers 500 for its own routes alone; one bad\n * package never takes the cockpit down with it.\n */\nfunction mountHubApis(\n app: Hono,\n apis: readonly DoomApi[],\n notice: (message: string) => void,\n resolveRepository: (repositoryId: string) => string | undefined,\n readRepositorySync: (repositoryId: string) => DoomRepositorySyncView | undefined,\n): DoomApiHandler[] {\n const handlers: DoomApiHandler[] = [];\n for (const api of apis) {\n const mount = `${DOOM_API_ROUTE_PREFIX}/${api.basePath}`;\n let handler: DoomApiHandler;\n try {\n handler = api.start({ scope: 'hub', onNotice: notice, resolveRepository, readRepositorySync });\n } catch (error) {\n notice(`hub API '${api.basePath}' did not start (${describeError(error)}); its routes stay unmounted`);\n continue;\n }\n handlers.push(handler);\n app.all(`${mount}/*`, async (context) => {\n if (context.req.query(API_SESSION_QUERY_PARAM) !== undefined) return context.notFound();\n const url = new URL(context.req.url);\n url.pathname = url.pathname.slice(mount.length) || '/';\n try {\n return await handler.fetch(new Request(url, context.req.raw));\n } catch (error) {\n notice(`hub API '${api.basePath}' failed on ${url.pathname} (${describeError(error)})`);\n return context.json({ error: `The '${api.basePath}' API failed.` }, 500);\n }\n });\n }\n return handlers;\n}\n\n/**\n * Forwards a package API request carrying ?session= to that session's own\n * server, over the API socket its registry record names.\n *\n * The session servers are where session-scoped data lives, but the browser must\n * not talk to them: their attach tokens stay in this process and the page only\n * ever reaches loopback. So the hub is the one door, and this is the hop behind\n * it. A session that never mounted an API answers 404 with the reason rather\n * than leaving the page waiting.\n */\nfunction mountSessionApiProxy(\n app: Hono,\n hub: SessionHub,\n notice: (message: string) => void,\n telemetry: DoomTelemetry,\n): void {\n app.all(`${DOOM_API_ROUTE_PREFIX}/*`, async (context) => {\n const sessionId = context.req.query(API_SESSION_QUERY_PARAM);\n if (sessionId === undefined) return context.notFound();\n const summary = hub.snapshot().find((candidate) => candidate.id === sessionId);\n if (summary === undefined) return context.json({ error: `No session ${sessionId}.` }, 404);\n if (summary.apiSocketPath === undefined) {\n return context.json({ error: `Session ${sessionId} serves no package API.` }, 404);\n }\n const url = new URL(context.req.url);\n url.searchParams.delete(API_SESSION_QUERY_PARAM);\n const parent = readTraceContext(context.req.raw.headers);\n try {\n return await telemetry.runInSpan(\n 'web.package_proxy',\n { 'operation.name': 'web.package_proxy', 'http.method': context.req.method },\n async (trace) =>\n await proxyToSocket({\n socketPath: summary.apiSocketPath as string,\n path: `${url.pathname}${url.search}`,\n method: context.req.method,\n headers: context.req.raw.headers,\n body: context.req.raw.body,\n trace: forwardedTraceContext(trace, parent),\n }),\n parent,\n );\n } catch (error) {\n notice(`session ${sessionId} API is unreachable (${describeError(error)})`);\n return context.json({ error: `Session ${sessionId} is not answering.` }, 502);\n }\n });\n}\n\n/**\n * Serves the cockpit and multiplexes every session behind it.\n *\n * Attach tokens stay in this process. The browser authenticates by reaching a\n * loopback port, so a page never holds a credential that would let it talk to\n * a session directly. One page WebSocket carries all sessions: hub frames\n * describe the set, and session traffic travels enveloped by session id.\n */\nexport async function serveWeb(options: WebServerOptions): Promise<WebServer> {\n const host = options.host ?? '127.0.0.1';\n const notice = options.onNotice ?? ((): void => {});\n const telemetry = createWebTelemetry();\n const assetsDir = resolveAssetsDir(options.assetsDir, notice);\n const hub = buildHub(options, notice, await loadHubChannels(assetsDir, notice), telemetry);\n // Threads are journals the data channels can name (a subagent run's own\n // session file); the hub tails one only while a page follows it.\n const threads = createThreadJournals({\n resolve: (sessionId, threadId) => hub.threadJournal(sessionId, threadId),\n onNotice: notice,\n });\n // A session reads what sync produced, and nothing makes the person who\n // opened the cockpit run sync first. The hub keeps the repository current.\n const syncGuard = createSyncGuard({ cwd: process.cwd(), onNotice: notice });\n await syncGuard.ensureSynced();\n /**\n * Every attached page, tagged with the listener it arrived on.\n *\n * The split matters: an approval prompt must reach the host and only the\n * host, because a paired phone that could approve devices would be able to\n * make its own access permanent.\n */\n const pages = new Set<{ post: (frame: object) => void; local: boolean }>();\n const broadcast = (frame: object, localOnly: boolean): void => {\n for (const page of pages) {\n if (localOnly && !page.local) continue;\n page.post(frame);\n }\n };\n\n const app = new Hono();\n const nodeWs = createNodeWebSocket({ app });\n /** Known once the listener binds; until then the guard treats every request as remote. */\n let loopbackPort: number | undefined;\n const store = createRemoteAccessStore({ stateDir: options.remoteStateDir, onNotice: notice });\n reapStaleTunnel(store.directory, notice);\n /**\n * Stands the host's sessions down so the container can take them over.\n *\n * Done here rather than in the launcher because this is where the hub is, and\n * the stop has to happen before the move: a recreated session opens the same\n * working tree, and two agents in one tree would fight over it.\n */\n const handover =\n options.onHandover === undefined\n ? undefined\n : (settings: RemoteAccessSettings): void => {\n const plan = planSessionMigration(\n hub.snapshot().map((session) => ({\n id: session.id,\n cwd: session.cwd,\n ...(session.name === undefined ? {} : { name: session.name }),\n })),\n settings.sandbox.workspaces,\n );\n for (const line of describeStranded(plan.stranded)) notice(line);\n for (const session of plan.migrate) {\n const stopped = hub.stop(session.id);\n if (!stopped.ok) notice(`could not stop ${session.name ?? session.id} before the move: ${stopped.error}`);\n }\n options.onHandover?.({ settings, sessions: plan.migrate });\n };\n\n const remote = createRemoteAccess({\n store,\n launchTunnel:\n options.remoteAccess?.launchTunnel ??\n createTunnelLauncher({\n stateDir: store.directory,\n ...(options.cloudflaredPath === undefined ? {} : { cloudflaredPath: options.cloudflaredPath }),\n onNotice: notice,\n onExit: (message) => {\n notice(`remote access: ${message}`);\n void remote.disable();\n },\n }),\n // The tunnel gets its own loopback socket. That is the only reliable way to\n // tell its traffic from the host's, since cloudflared connects from\n // 127.0.0.1 and forges nothing a header could reveal.\n bindListener: async () =>\n await new Promise((resolve, reject) => {\n const tunnelServer = serve(\n {\n fetch: app.fetch,\n port: 0,\n hostname: '127.0.0.1',\n serverOptions: {\n maxHeaderSize: TUNNEL_HEADER_BYTES,\n headersTimeout: TUNNEL_HEADERS_TIMEOUT_MS,\n requestTimeout: TUNNEL_REQUEST_TIMEOUT_MS,\n },\n },\n (info) => {\n nodeWs.injectWebSocket(tunnelServer);\n resolve({\n port: info.port,\n close: async () =>\n await new Promise<void>((done) => {\n (tunnelServer as { closeAllConnections?: () => void }).closeAllConnections?.();\n tunnelServer.close(() => done());\n }),\n });\n },\n );\n const bounded = tunnelServer as typeof tunnelServer & { maxConnections: number; maxRequestsPerSocket: number };\n bounded.maxConnections = TUNNEL_CONNECTION_LIMIT;\n bounded.maxRequestsPerSocket = TUNNEL_REQUESTS_PER_SOCKET;\n tunnelServer.once('error', reject);\n }),\n onNotice: notice,\n ...(handover === undefined ? {} : { requestHandover: handover }),\n contained: insideSandbox(process.env),\n broadcastLocal: (frame) => broadcast(frame, true),\n broadcastAll: (frame) => broadcast(frame, false),\n ...(options.remoteAccess?.now === undefined ? {} : { now: options.remoteAccess.now }),\n });\n // First route on the app, because Hono composes matching handlers in\n // registration order: a guard added after a terminating handler never runs\n // for that path. It also refuses socket upgrades, which is what closes the\n // cross-site WebSocket hijack that loopback binding never covered.\n const guard = createRemoteGuard({\n loopbackPort: () => loopbackPort,\n tunnelPolicy: () => remote.tunnelPolicy(),\n authorize: (context) => remote.authorize(getCookie(context, DEVICE_COOKIE, 'host')),\n trustedDevice: (context) => (context.env as SealedRequestBindings | undefined)?.sealedDeviceId,\n channelReady: (deviceId, scope) => remote.channelFor(deviceId, scope) !== undefined,\n stepUp: {\n required: (action) => remote.stepUpRequired(action),\n verify: async (context, action, assertion) => {\n if (typeof assertion !== 'object' || assertion === null) return false;\n const ceremonyId = (assertion as { ceremonyId?: unknown }).ceremonyId;\n const response = (assertion as { response?: unknown }).response;\n if (typeof ceremonyId !== 'string') return false;\n const trusted = (context.env as SealedRequestBindings | undefined)?.sealedDeviceId;\n const deviceId = trusted ?? remote.authorize(getCookie(context, DEVICE_COOKIE, 'host'));\n const caller = deviceId === undefined ? 'local' : `device:${deviceId}`;\n return await remote.passkeys().finishStepUp(ceremonyId, caller, action, response);\n },\n },\n extraOrigins: allowedOriginsFromEnv(process.env[ALLOW_ORIGIN_ENV]),\n });\n app.use('*', guard.middleware);\n app.use('*', async (context, next) => {\n const pathname = new URL(context.req.url).pathname;\n if (pathname === REMOTE_HTTP_ROUTE) return next();\n const operation = requestOperation(pathname);\n if (operation === undefined) return next();\n const started = performance.now();\n return await telemetry.runInSpan(\n operation,\n { 'operation.name': operation, 'http.method': context.req.method },\n async () => {\n await next();\n await telemetry.recordEvent('web.api.request', {\n 'operation.name': operation,\n 'http.method': context.req.method,\n 'http.status_code': context.res.status,\n duration_ms: Math.max(0, Math.round(performance.now() - started)),\n });\n },\n readTraceContext(context.req.raw.headers),\n );\n });\n // Reject a declared oversized body before any public route reads it. Chunked\n // bodies are bounded by the route-specific or downstream streaming limiter.\n app.use('*', async (context, next) => {\n if (guard.listenerOf(context) === 'local') return next();\n const declared = Number(context.req.header('content-length'));\n if (Number.isFinite(declared) && declared > TUNNEL_BODY_BYTES) {\n return context.json({ error: 'The tunnel request body is too large.' }, 413);\n }\n return next();\n });\n registerRemoteRoutes(app, { remote, listenerOf: (context) => guard.listenerOf(context) });\n const tunnelBodyLimit = bodyLimit({\n maxSize: TUNNEL_BODY_BYTES,\n onError: (context) => context.json({ error: 'The tunnel request body is too large.' }, 413),\n });\n app.use('*', async (context, next) => {\n if (guard.listenerOf(context) === 'local') return next();\n return await tunnelBodyLimit(context, next);\n });\n const browserTelemetryBodyLimit = bodyLimit({\n maxSize: 8 * 1024,\n onError: (context) => context.json({ error: 'The telemetry batch is too large.' }, 413),\n });\n const acceptBrowserTelemetryRequest = createBrowserTelemetryRateLimit();\n app.post(WEB_TELEMETRY_ROUTE, browserTelemetryBodyLimit, async (context) => {\n if (!acceptBrowserTelemetryRequest()) return context.json({ error: 'Too many telemetry batches.' }, 429);\n let body: unknown;\n try {\n body = await context.req.json();\n } catch {\n return context.json({ error: 'The telemetry batch must be JSON.' }, 400);\n }\n const events = parseBrowserPerformanceBatch(body);\n if (events === undefined) return context.json({ error: 'The telemetry batch is invalid.' }, 400);\n for (const event of events) {\n await telemetry.recordEvent(event.name, {\n ...(event.duration_ms === undefined ? {} : { duration_ms: event.duration_ms }),\n ...(event.count === undefined ? {} : { count: event.count }),\n });\n }\n return context.body(null, 204);\n });\n app.post(REMOTE_HTTP_ROUTE, async (context) => {\n const deviceId = remote.authorize(getCookie(context, DEVICE_COOKIE, 'host'));\n if (deviceId === undefined) return context.json({ error: 'This device is not paired.' }, 401);\n const channel = remote.channelFor(deviceId, 'http');\n if (channel === undefined) return context.json({ error: 'This device has no sealed HTTP channel.' }, 401);\n\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\n let decoded: unknown;\n try {\n decoded = JSON.parse(new TextDecoder().decode(opened.plaintext));\n } catch {\n return context.json({ error: 'The sealed HTTP request was malformed.' }, 400);\n }\n const request = parseSealedHttpRequest(decoded);\n const headers = request === undefined ? undefined : sealedRequestHeaders(request, context);\n const body = request === undefined ? undefined : decodeSealedHttpBody(request.body);\n if (request === undefined || headers === undefined || body === undefined) {\n return context.json({ error: 'The sealed HTTP request was invalid.' }, 400);\n }\n\n const target = new URL(request.target, new URL(context.req.url).origin);\n const innerResponse = await app.request(\n target,\n {\n method: request.method,\n headers,\n ...(request.method === 'GET' || request.method === 'HEAD' ? {} : { body: Uint8Array.from(body).buffer }),\n },\n { ...(context.env as SealedRequestBindings), sealedDeviceId: deviceId },\n );\n const responseBody = Buffer.from(await innerResponse.arrayBuffer());\n const sealed = channel.seal(\n new TextEncoder().encode(\n JSON.stringify({\n v: SEALED_HTTP_VERSION,\n status: innerResponse.status,\n headers: Array.from(innerResponse.headers.entries()),\n body: responseBody.toString('base64'),\n }),\n ),\n );\n if (!sealed.ok) return context.json({ error: `The sealed response failed: ${sealed.failure}.` }, 503);\n return context.json(sealed.envelope);\n });\n // Every running session already serves Pi's protocol; the hub composes them\n // into one server so a browser sees a single endpoint with many sessions.\n // Remote clients get a distinct protocol service that can attach to existing\n // sessions but cannot bypass HTTP step-up by creating one directly.\n const localProtocolListener = createPiWebSocketListener({ onError: (error) => notice(`protocol: ${error.message}`) });\n const remoteProtocolListener = createPiWebSocketListener({\n onError: (error) => notice(`protocol: ${error.message}`),\n });\n const protocolService = createPiHubService({\n records: () => hub.records(),\n spawn: (input) => hub.create(input),\n onNotice: notice,\n });\n const remoteProtocolService = {\n ...protocolService,\n createSession: async () => {\n throw new PiServerError('not_implemented', 'Remote protocol clients cannot create sessions');\n },\n };\n const localProtocolServer = new PiServer(protocolService, {\n listeners: [localProtocolListener],\n onError: (error) => notice(`protocol: ${error.message}`),\n });\n const remoteProtocolServer = new PiServer(remoteProtocolService, {\n listeners: [remoteProtocolListener],\n onError: (error) => notice(`protocol: ${error.message}`),\n });\n await Promise.all([localProtocolServer.start(), remoteProtocolServer.start()]);\n // Provider credentials belong to the machine, not to a session: the hub\n // keeps one Pi runtime over the shared auth.json and signs in for all.\n const providerAuth = createProviderAuth({ runtime: options.authRuntime, onNotice: notice });\n registerAuthRoutes(app, providerAuth);\n // Settings read and write the machine's Doom config. Session working\n // directories are normalized to their nearest repository marker before the\n // picker or a package API can address them. The bounded registry keeps recent\n // roots available after their last session closes without trusting browser paths.\n const repositoryRegistry = createSettingsRepositoryRegistry(hub);\n const repositories = (): SettingsRepository[] => repositoryRegistry.list();\n const resolveRepository = (repositoryId: string): string | undefined => repositoryRegistry.resolve(repositoryId);\n const readRepositorySync = (repositoryId: string): DoomRepositorySyncView | undefined => {\n const root = resolveRepository(repositoryId);\n if (root === undefined) return undefined;\n const drift = readSyncDrift({ repoRoot: root });\n let state: ReturnType<typeof readSyncState>;\n try {\n state = readSyncState(root);\n } catch {\n state = undefined;\n }\n return {\n fresh: drift.fresh,\n reasons: [...drift.reasons],\n ...(state === undefined ? {} : { mcpProjection: state.fileState.mcpProjection }),\n };\n };\n registerSettingsRoutes(app, {\n repositories,\n models: () => providerAuth.listModels(),\n });\n // Package APIs, mounted before the SPA fallback so their routes are reachable\n // and everything else still falls through to the bundle. Hub-scoped ones run\n // here; session-scoped ones live in each session's own server and are proxied.\n const pluginApis = mountHubApis(\n app,\n await loadPackageApis('hub', { onNotice: notice }),\n notice,\n resolveRepository,\n readRepositorySync,\n );\n mountSessionApiProxy(app, hub, notice, telemetry);\n\n app.get('/api/health', (context) =>\n context.json({\n ok: true,\n role: HUB_ROLE,\n protocol: HUB_PROTOCOL_VERSION,\n sessions: hub.snapshot().length,\n pid: process.pid,\n }),\n );\n\n app.post(SESSIONS_API_ROUTE, async (context) => {\n let body: unknown;\n try {\n body = await context.req.json();\n } catch {\n return context.json({ error: 'The request body must be JSON.' }, 400);\n }\n if (!isRecord(body) || typeof body.cwd !== 'string' || body.cwd === '') {\n return context.json({ error: 'A cwd string is required.' }, 400);\n }\n const name = typeof body.name === 'string' && body.name !== '' ? body.name : undefined;\n await syncGuard.ensureSynced();\n const outcome = await hub.create({ cwd: body.cwd, name, trace: readTraceContext(context.req.raw.headers) });\n if (outcome.ok) return context.json({ sessionId: outcome.sessionId }, 201);\n return context.json({ error: outcome.error }, outcome.code === 'invalid_request' ? 400 : 502);\n });\n\n // A running server reads the composition once: its extensions when the agent\n // starts, its package API routes when the process does. So a rebuild reaches\n // an existing session only by replacing the process, and the sync has to\n // finish before the replacement starts or it reads the same stale artifacts.\n app.post(`${SESSIONS_API_ROUTE}/:sessionId/restart`, async (context) => {\n const sessionId = context.req.param('sessionId');\n await syncGuard.ensureSynced();\n const outcome = await hub.restart(sessionId, readTraceContext(context.req.raw.headers));\n if (outcome.ok) return context.json({ sessionId: outcome.sessionId }, 202);\n return context.json({ error: outcome.error }, outcome.code === 'invalid_request' ? 404 : 502);\n });\n\n app.delete(`${SESSIONS_API_ROUTE}/:sessionId`, (context) => {\n const sessionId = context.req.param('sessionId');\n const outcome = hub.stop(sessionId);\n if (outcome.ok) return context.json({ sessionId }, 202);\n return context.json(\n { error: outcome.error },\n outcome.code === 'unknown' ? 404 : outcome.code === 'self' ? 409 : 502,\n );\n });\n\n // File completion for the composer's @ references: bounded, cwd-scoped,\n // and only for sessions the hub actually manages.\n app.get('/api/sessions/:sessionId/files', async (context) => {\n const sessionId = context.req.param('sessionId');\n const summary = hub.snapshot().find((candidate) => candidate.id === sessionId);\n if (!summary) return context.json({ error: 'Unknown session.' }, 404);\n const query = context.req.query('q') ?? '';\n const files = await listSessionFiles(summary.cwd, query, 20);\n return context.json({ files });\n });\n\n /**\n * The subtree directory suggestions may name, or undefined for all of them.\n *\n * A paired device can ask this route, and answering from the whole home\n * directory hands it a map of the machine: every project, every client name,\n * every checkout. So a request arriving on the tunnel is pinned to the\n * directory the cockpit was started from, which is the one the person running\n * it already chose.\n *\n * Only that request. The person at this keyboard already has a shell, so\n * pinning them would cost the picker its usefulness and buy nothing. A\n * contained cockpit needs none of it either, because its mounts are the\n * boundary and nothing outside them is visible to it at all.\n */\n const browseRoot = (context: Context): string | undefined => {\n if (guard.listenerOf(context) === 'local' || insideSandbox(process.env)) return undefined;\n return options.browseRoot ?? process.cwd();\n };\n\n // Directory suggestions for the new-session picker: the children of the\n // typed parent while a path is being drilled into, and a ranked search of\n // the home directory for anything else, so a name or a path remembered from\n // another machine still finds the folder.\n app.get(DIRECTORIES_API_ROUTE, async (context) => {\n const root = browseRoot(context);\n const directories = await suggestDirectories(context.req.query('q') ?? '', {\n limit: DIRECTORY_SUGGESTION_LIMIT,\n ...(root === undefined ? {} : { root }),\n });\n return context.json({ directories });\n });\n\n // The timeline's @file previews: one cwd-contained file, capped in size.\n app.get(SESSION_FILE_ROUTE, async (context) => {\n const sessionId = context.req.param('sessionId');\n const summary = hub.snapshot().find((candidate) => candidate.id === sessionId);\n if (!summary) return context.json({ error: 'Unknown session.' }, 404);\n const relativePath = context.req.query('path') ?? '';\n const file = await readSessionFile(summary.cwd, relativePath, MAX_SESSION_FILE_BYTES);\n if (file.status === 'forbidden') return context.json({ error: 'The path leaves the session directory.' }, 403);\n if (file.status === 'not-found') return context.json({ error: 'No such file.' }, 404);\n if (file.status === 'too-large') return context.json({ error: 'The file is too large to preview.' }, 413);\n return context.body(new Uint8Array(file.body), 200, sessionFileHeaders(relativePath));\n });\n\n app.get(\n SESSION_SOCKET_ROUTE,\n nodeWs.upgradeWebSocket((context) => {\n const local = guard.listenerOf(context) === 'local';\n // Resolved once at upgrade: the device is fixed for the socket's life, and\n // looking it up per frame would bump last-seen on every keystroke.\n const deviceId = local ? undefined : remote.authorize(getCookie(context, DEVICE_COOKIE, 'host'));\n const channel = deviceId === undefined ? undefined : remote.channelFor(deviceId, 'session');\n const subscriptions = new Set<string>();\n const connectionId = randomUUID();\n /** The threads this page follows; one socket may follow several of one session. */\n const threadSubscriptions = new Map<string, { sessionId: string; threadId: string }>();\n let disconnect: (() => void) | undefined;\n let disconnectThreads: (() => void) | undefined;\n /** Held on the socket, not inside onOpen, so close can withdraw it. */\n let registered: { post: (frame: object) => void; local: boolean } | undefined;\n /** Withdraws this socket from the remote registry, so switch-off can close it. */\n let untrack: (() => void) | undefined;\n /** Lets go of every followed thread, or only a departed session's. */\n const releaseThreads = (sessionId?: string): void => {\n for (const [key, held] of threadSubscriptions) {\n if (sessionId !== undefined && held.sessionId !== sessionId) continue;\n threadSubscriptions.delete(key);\n threads.unsubscribe(held.sessionId, held.threadId);\n }\n };\n return {\n onOpen(_event, ws) {\n if (!local && channel === undefined) {\n ws.close(1008, 'sealed session channel required');\n return;\n }\n const post = (frame: SessionFrame | object): void => {\n const text = JSON.stringify(frame);\n let outgoing = text;\n if (!local) {\n if (channel === undefined) return;\n const sealed = channel.seal(new TextEncoder().encode(text));\n if (!sealed.ok) {\n ws.close(1008, 'sealed session channel failed');\n return;\n }\n outgoing = JSON.stringify(sealed.envelope);\n }\n try {\n ws.send(outgoing);\n } catch {\n // The browser went away mid-write; onClose tears the socket down.\n }\n };\n registered = { post, local };\n pages.add(registered);\n // A socket that has upgraded has left the HTTP server's connection\n // tracking, so closing the tunnel listener does not reach it.\n // Without this a paired phone keeps driving the agent after remote\n // access is switched off.\n if (deviceId !== undefined) untrack = remote.trackSocket(deviceId, (code, reason) => ws.close(code, reason));\n post(hubHello(hub.channelTypes()));\n post({ type: SESSIONS_SNAPSHOT_TYPE, sessions: hub.snapshot() });\n disconnect = hub.onEvent((event) => {\n if (event.kind === 'upsert') post({ type: SESSION_UPSERT_TYPE, session: event.session });\n else if (event.kind === 'removed') {\n subscriptions.delete(event.sessionId);\n releaseThreads(event.sessionId);\n post({ type: SESSION_REMOVED_TYPE, sessionId: event.sessionId });\n } else if (event.kind === 'channel') {\n if (\n event.connectionId === connectionId ||\n (event.connectionId === undefined && subscriptions.has(event.sessionId))\n ) {\n post({ type: event.frameType, sessionId: event.sessionId, payload: event.payload });\n }\n } else {\n const notification = parseDoomNotificationEntry(event.frame);\n if (notification !== undefined || subscriptions.has(event.sessionId)) {\n post(sessionFrameEnvelope(event.sessionId, event.frame));\n }\n }\n });\n disconnectThreads = threads.onFrame((event) => {\n if (threadSubscriptions.has(threadKey(event.sessionId, event.threadId))) {\n post(threadFrameEnvelope(event.sessionId, event.threadId, event.frame));\n }\n });\n notice('browser attached');\n },\n onMessage(event) {\n if (typeof event.data !== 'string') return;\n let parsed: unknown;\n try {\n parsed = JSON.parse(event.data);\n } catch {\n return;\n }\n if (!local) {\n if (channel === undefined) return;\n const opened = channel.open(parsed);\n // A frame that will not open was altered or replayed. Dropping it\n // is the only safe answer; there is no plaintext to fall back to.\n if (!opened.ok) return;\n try {\n parsed = JSON.parse(new TextDecoder().decode(opened.plaintext));\n } catch {\n return;\n }\n }\n if (!isRecord(parsed) || typeof parsed.sessionId !== 'string') return;\n const sessionId = parsed.sessionId;\n if (\n typeof parsed.type === 'string' &&\n hub.channelTypes().includes(parsed.type) &&\n (subscriptions.has(sessionId) || hub.channelReceivesWithoutSubscription(parsed.type))\n ) {\n hub.receiveChannel(sessionId, parsed.type, parsed.payload, connectionId);\n return;\n }\n if (parsed.type === SUBSCRIBE_TYPE) {\n const backlog = hub.backlog(sessionId);\n if (!backlog) return; // Unknown session; the snapshot said otherwise.\n subscriptions.add(sessionId);\n registered?.post(backlog);\n for (const frame of hub.channelFrames(sessionId)) registered?.post(frame);\n return;\n }\n if (parsed.type === UNSUBSCRIBE_TYPE) {\n subscriptions.delete(sessionId);\n return;\n }\n if (parsed.type === HISTORY_REQUEST_TYPE) {\n // Older transcript, on demand. The hub kept what the attach path\n // was too small to publish, so scrolling back reads from memory\n // rather than asking the session to re-read its journal.\n const page = hub.history(sessionId, {\n ...(typeof parsed.before === 'string' ? { before: parsed.before } : {}),\n ...(typeof parsed.limit === 'number' ? { limit: parsed.limit } : {}),\n });\n if (!page) return;\n registered?.post(page);\n return;\n }\n if (parsed.type === SUBSCRIBE_THREAD_TYPE || parsed.type === UNSUBSCRIBE_THREAD_TYPE) {\n if (typeof parsed.threadId !== 'string') return;\n const threadId = parsed.threadId;\n const key = threadKey(sessionId, threadId);\n if (parsed.type === UNSUBSCRIBE_THREAD_TYPE) {\n if (threadSubscriptions.delete(key)) threads.unsubscribe(sessionId, threadId);\n return;\n }\n if (threadSubscriptions.has(key) || hub.backlog(sessionId) === undefined) return;\n threadSubscriptions.set(key, { sessionId, threadId });\n registered?.post(threadBacklog(sessionId, threadId, threads.subscribe(sessionId, threadId)));\n return;\n }\n if (parsed.type === SESSION_COMMAND_TYPE && isRecord(parsed.frame)) {\n // The hub owns the handshake; a page must not be able to replay it.\n if (parsed.frame.type === ATTACH_TYPE) return;\n hub.command(sessionId, parsed.frame);\n }\n },\n onClose() {\n hub.disconnectChannels(connectionId);\n if (registered) pages.delete(registered);\n registered = undefined;\n untrack?.();\n untrack = undefined;\n disconnect?.();\n disconnect = undefined;\n disconnectThreads?.();\n disconnectThreads = undefined;\n subscriptions.clear();\n releaseThreads();\n notice('browser detached');\n },\n };\n }),\n );\n\n app.get(\n PROTOCOL_SOCKET_ROUTE,\n nodeWs.upgradeWebSocket((context) => {\n const local = guard.listenerOf(context) === 'local';\n const deviceId = local ? undefined : remote.authorize(getCookie(context, DEVICE_COOKIE, 'host'));\n const channel = deviceId === undefined ? undefined : remote.channelFor(deviceId, 'protocol');\n let handler: ReturnType<typeof localProtocolListener.accept>;\n let untrack: (() => void) | undefined;\n return {\n onOpen(_event, ws) {\n if (!local && channel === undefined) {\n ws.close(1008, 'sealed protocol channel required');\n return;\n }\n if (deviceId !== undefined) untrack = remote.trackSocket(deviceId, (code, reason) => ws.close(code, reason));\n handler = (local ? localProtocolListener : remoteProtocolListener).accept({\n send: (data) => {\n if (local) {\n ws.send(data as ArrayBuffer);\n return;\n }\n if (channel === undefined) return;\n const bytes = data instanceof Uint8Array ? data : new Uint8Array(data as ArrayBuffer);\n const sealed = channel.seal(bytes);\n if (!sealed.ok) {\n ws.close(1008, 'sealed protocol channel failed');\n return;\n }\n ws.send(new TextEncoder().encode(JSON.stringify(sealed.envelope)));\n },\n close: () => ws.close(),\n get readyState() {\n return ws.readyState;\n },\n });\n },\n async onMessage(event) {\n const data = event.data;\n if (typeof data === 'string') return;\n const bytes =\n data instanceof Blob\n ? new Uint8Array(await data.arrayBuffer())\n : data instanceof ArrayBuffer\n ? new Uint8Array(data)\n : new Uint8Array(data);\n if (local) {\n handler?.onData(bytes);\n return;\n }\n if (channel === undefined) return;\n let envelope: unknown;\n try {\n envelope = JSON.parse(new TextDecoder().decode(bytes));\n } catch {\n return;\n }\n const opened = channel.open(envelope);\n if (opened.ok) handler?.onData(opened.plaintext);\n },\n onClose() {\n untrack?.();\n untrack = undefined;\n handler?.onClose();\n handler = undefined;\n },\n onError(event) {\n handler?.onError(event instanceof Error ? event : new Error('The protocol socket failed'));\n handler = undefined;\n },\n };\n }),\n );\n\n app.get('*', (context) => {\n const requested = new URL(context.req.url).pathname;\n const direct = requested === '/' ? undefined : resolveAssetPath(assetsDir, requested);\n const body = direct ? readAsset(direct) : undefined;\n if (direct && body) {\n return context.body(new Uint8Array(body), 200, { 'Content-Type': contentTypeFor(direct) });\n }\n const index = readAsset(path.join(assetsDir, INDEX_FILE));\n if (!index) return context.text('The cockpit bundle is missing. Run the package build.', 500);\n return context.body(new Uint8Array(index), 200, { 'Content-Type': 'text/html; charset=utf-8' });\n });\n\n // A rebuilt bundle only changes on disk, so the page it replaced has to be\n // told; nothing about a loaded bundle notices that its source moved.\n syncGuard.watch(() => {\n broadcast({ type: HUB_RESYNCED_TYPE }, false);\n });\n\n return new Promise<WebServer>((resolve, reject) => {\n const server = serve({ fetch: app.fetch, port: options.port, hostname: host }, (info) => {\n // Before this the guard has no loopback port to compare against and\n // treats everything as remote, which is the safe direction to be wrong in.\n loopbackPort = info.port;\n nodeWs.injectWebSocket(server);\n const url = `http://${host}:${info.port}`;\n notice(`cockpit on ${url}`);\n let closePromise: Promise<void> | undefined;\n const close = async (): Promise<void> => {\n // Remote access first, so the tunnel is down and every paired socket\n // is closed before the rest of the hub starts letting go.\n await remote.close();\n await new Promise<void>((done) => {\n threads.close();\n syncGuard.close();\n void localProtocolServer.close();\n void remoteProtocolServer.close();\n hub.close();\n providerAuth.close();\n for (const handler of pluginApis) handler.close();\n // An upgraded socket leaves the HTTP server's connection tracking,\n // so only the WebSocket server can let go of it. Without this the\n // close callback waits on a browser that has no reason to leave.\n for (const client of nodeWs.wss.clients) client.terminate();\n nodeWs.wss.close();\n (server as { closeAllConnections?: () => void }).closeAllConnections?.();\n server.close(() => done());\n });\n await shutdownWebTelemetry(telemetry);\n };\n resolve({\n url,\n port: info.port,\n close: () => (closePromise ??= close()),\n });\n });\n server.once('error', (error) => {\n threads.close();\n hub.close();\n providerAuth.close();\n void remote.close();\n for (const handler of pluginApis) handler.close();\n void telemetry.shutdown();\n reject(error);\n });\n });\n}\n"],"mappings":"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"}
@@ -1,4 +1,6 @@
1
+ import "../types/settings.cjs";
1
2
  import { WebServer, WebServerOptions } from "../types/bridge.cjs";
3
+ import "./sessionHub.cjs";
2
4
  //#region src/adapters/httpServer.d.ts
3
5
  /**
4
6
  * Serves the cockpit and multiplexes every session behind it.
@@ -1 +1 @@
1
- {"version":3,"file":"httpServer.d.cts","names":[],"sources":["../../../src/adapters/httpServer.ts"],"mappings":";;;;;;;;;;iBAiWsB,SAAS,SAAS,mBAAmB,QAAQ"}
1
+ {"version":3,"file":"httpServer.d.cts","names":[],"sources":["../../../src/adapters/httpServer.ts"],"mappings":";;;;;;;;;;;;iBA4ZsB,SAAS,SAAS,mBAAmB,QAAQ"}
@@ -1,4 +1,6 @@
1
+ import "../types/settings.mjs";
1
2
  import { WebServer, WebServerOptions } from "../types/bridge.mjs";
3
+ import "./sessionHub.mjs";
2
4
  //#region src/adapters/httpServer.d.ts
3
5
  /**
4
6
  * Serves the cockpit and multiplexes every session behind it.
@@ -1 +1 @@
1
- {"version":3,"file":"httpServer.d.mts","names":[],"sources":["../../../src/adapters/httpServer.ts"],"mappings":";;;;;;;;;;iBAiWsB,SAAS,SAAS,mBAAmB,QAAQ"}
1
+ {"version":3,"file":"httpServer.d.mts","names":[],"sources":["../../../src/adapters/httpServer.ts"],"mappings":";;;;;;;;;;;;iBA4ZsB,SAAS,SAAS,mBAAmB,QAAQ"}