@agimon-ai/doompi-web 0.0.1-alpha.19 → 0.0.1-alpha.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,7 +34,7 @@ To run this repository's build with its composition pinned and watched:
34
34
 
35
35
  ```bash
36
36
  pnpm cockpit:build
37
- pnpm doompi-web --dir=<PWD>
37
+ pnpm doompi-web --dir="$PWD"
38
38
  ```
39
39
 
40
40
  `--dir` accepts `--dir <path>` too. It synchronizes that repository before launch and watches it for development rebuilds.
@@ -56,28 +56,14 @@ Install [doompi-server](https://www.npmjs.com/package/@agimon-ai/doompi-server)
56
56
 
57
57
  ## Security
58
58
 
59
- Attach tokens are read by this process from the token files the registry names and never reach the
60
- browser. The server binds loopback by default, so reaching the cockpit already means having an
61
- account on the machine. Remote access means tunnelling the port, for example over SSH; do not bind
62
- a public address and call it done.
63
-
64
- Loopback binding is not on its own an authorization boundary, because two request shapes escape it
65
- and both reach code execution:
66
-
67
- - **WebSockets are exempt from CORS.** Any page in any browser on this machine can open a socket to
68
- the cockpit's port, and `session_command` frames carry prompts straight to an agent holding a
69
- `bash` tool.
70
- - **A cross-origin POST need not be preflighted.** Sent as `text/plain`, it still parses as JSON
71
- server-side, so a page could spawn a session in any directory without ever reading a response.
72
-
73
- Both are refused. Every request and every socket upgrade is checked against an origin and host
74
- allowlist derived from the port actually bound, plus the vite dev server. A request carrying no
75
- `Origin` is allowed, because curl, the health probe, and the test harness send none and a browser
76
- always sends one where it matters; this defends against a hostile web page, not against a hostile
77
- local program, which can send whatever headers it likes. `DOOMPI_WEB_ALLOW_ORIGIN` takes a
78
- comma-separated list for setups this package cannot guess. The host check is also the DNS rebinding
79
- defence, and it is the only check a socket upgrade gets, since the upgrade path never reaches the
80
- server's own host parsing.
59
+ The hub reads session attach tokens from registry token files; browsers do not receive those tokens.
60
+ It binds loopback by default and checks HTTP requests and WebSocket upgrades against an origin and
61
+ host allowlist. `DOOMPI_WEB_ALLOW_ORIGIN` adds comma-separated origins for custom setups.
62
+
63
+ These checks defend against hostile web pages and DNS rebinding, not hostile local programs.
64
+ The local listener allows requests without an `Origin` header, and a local program can supply its own headers.
65
+ Do not expose the local listener publicly. Use the paired remote-access listener described below
66
+ or a trusted SSH tunnel.
81
67
 
82
68
  ## What the cockpit shows
83
69
 
@@ -130,7 +116,7 @@ perform the handshake itself: `attach` frames sent from the browser are refused.
130
116
 
131
117
  ## iPhone Home Screen app
132
118
 
133
- Use a stable named HTTPS tunnel such as `doompi.agimon.win`. Quick-tunnel hostnames rotate, so they cannot
119
+ Use a stable named HTTPS tunnel on a hostname you control. Quick-tunnel hostnames rotate, so they cannot
134
120
  provide durable PWA identity, passkeys, or reliable Push subscriptions. Open `/pair` in Safari, add it to the
135
121
  Home Screen, then launch the installed app and scan the QR shown by the host.
136
122
 
@@ -188,9 +174,9 @@ With both off, a paired device keeps its session until it is revoked, remote acc
188
174
  the hub restarts. Switching remote access off always revokes every paired session, and that is not a
189
175
  toggle.
190
176
 
191
- Before reporting success, the tunnel is **self-tested through its own public URL**: the pairing page
192
- must answer 200 and `/api/health` must answer 401. Anything else means the agent is on the public
193
- internet unguarded, so the tunnel is killed immediately rather than warned about.
177
+ Before reporting success, the tunnel is self-tested through its public URL: the pairing page must
178
+ answer 200 and `/api/health` must answer 401. A failed probe stops the tunnel. This checks those
179
+ routes, not every possible access path.
194
180
 
195
181
  Requires `cloudflared` on `PATH` (`brew install cloudflared`). A quick tunnel needs no account but gets a new
196
182
  hostname on every start. It does not provide a durable PWA, passkey, or Push identity, and Cloudflare does not
@@ -285,10 +271,10 @@ a failed move never leaves you with nothing listening. `Ctrl-C` stops the contai
285
271
  outright leaves the container running; the next start finds it through `~/.doompi/web/cockpit-container.json`
286
272
  and stops it.
287
273
 
288
- Two things carry across the boundary and nothing else does. Provider credentials go through the
289
- existing broker, which hands the container a per-session token rather than your keys. Git identity is
290
- passed as `GIT_AUTHOR_*` and `GIT_COMMITTER_*`, so an agent can commit but cannot push, because no
291
- key crosses.
274
+ Provider credentials use the broker's per-session token rather than forwarding host API keys.
275
+ Git identity is passed as `GIT_AUTHOR_*` and `GIT_COMMITTER_*`; host SSH keys are not mounted.
276
+ This does not prevent pushes using credentials already present in a mounted workspace or obtained
277
+ inside the container.
292
278
 
293
279
  What this does not defend against, stated plainly: anyone who can talk to the container daemon can
294
280
  escape any container it runs, so the engine is part of the trusted base. Sessions share one container
@@ -298,40 +284,23 @@ still destroy the repository you gave it. Network access is unrestricted, exactl
298
284
 
299
285
  Requires `@agimon-ai/doompi-sandbox` in the composition and a container engine on the host.
300
286
 
301
- ## Planned: WebRTC transport
302
-
303
- Today the page reaches the hub over two WebSockets, and any remote access means putting a tunnel in
304
- front of them, which puts the tunnel provider in the path of everything. A WebRTC data channel would
305
- replace that, with the tunnel demoted to carrying signaling only. Two independent reasons want it,
306
- and the first is the stronger.
307
-
308
- **Realtime voice.** `@agimon-ai/doompi-voice` now captures on the selected browser client, sends
309
- mono PCM16 through its authenticated session media transport, keeps VAD and Whisper on the agent
310
- host, and plays narration with browser speech synthesis. Voice activation is when the page asks for
311
- microphone permission, never on load. A future WebRTC implementation can replace the HTTP media
312
- adapter with Opus tracks while preserving the same client device and transport contracts.
287
+ ## Transport limits
313
288
 
314
- **Confidentiality.** A QR shown on the host can carry the DTLS fingerprint, which authenticates the
315
- peer connection out-of-band. The data path is then peer-to-peer DTLS: the tunnel provider usually
316
- never carries the traffic and can never read it. That is close to VPN-grade confidentiality without
317
- asking anyone to install a VPN.
318
-
319
- The cost is real and worth stating: a WebRTC stack on the Node side (`node-datachannel` or
320
- `werift`), a second client transport alongside `wsClient.ts` and `piTransport.ts`, ICE and NAT
321
- traversal debugging, and a TURN relay fallback for the connections that cannot go direct. TURN
322
- reintroduces a relay, though still one that cannot decrypt. The WebSocket transport stays as the
323
- fallback for when a data channel cannot be established.
324
-
325
- No date. This is a design note, not a commitment.
289
+ The cockpit uses WebSockets for session traffic. WebRTC is not implemented. Browser Voice uses
290
+ its authenticated session media transport, with VAD and Whisper on the agent host and browser
291
+ speech synthesis for narration.
326
292
 
327
293
  ## Public API
328
294
 
329
295
  ```ts
330
296
  import { serveWeb } from '@agimon-ai/doompi-web';
297
+ import { bundleCockpitWeb } from '@agimon-ai/doompi-web/bundler';
331
298
  ```
332
299
 
333
300
  ## Development
334
301
 
302
+ Run from this package directory in the workspace:
303
+
335
304
  ```bash
336
305
  pnpm build
337
306
  pnpm typecheck
@@ -375,8 +344,8 @@ and the host's own slot contributions) and an optional hub entry (`webHubChannel
375
344
  dist). This package's own bundle carries no plugins; the full set is assembled on your machine.
376
345
  `doompi sync` discovers the installed composition's manifests, generates the import entries, and builds
377
346
  the SPA through the `./bundler` subpath inside the repository/worktree's immutable sync generation.
378
- The shared server uses the validated registration for its startup repository. `--assets` and
379
- `DOOMPI_WEB_DIST` override registered assets, then packaged assets provide the fallback. Server-only
347
+ By default, the shared server resolves the union of live-session registrations; `--dir` pins one
348
+ repository. `--assets` and `DOOMPI_WEB_DIST` override registered assets, then packaged assets provide the fallback. Server-only
380
349
  `webPlugins.server.json` sits beside the public `web/` directory, never inside the signed or served
381
350
  bundle. It names each plugin's built hub entry, imported lazily. A missing plugin package logs a notice
382
351
  and its tab is omitted. Session package API routes use the session `cwd` registration, while
@@ -1,3 +1,3 @@
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"),ee=require("./authRoutes.cjs"),te=require("./settingsRoutes.cjs"),ne=require("./providerAuth.cjs"),re=require("./remoteGuard.cjs"),ie=require("./remoteAccess.cjs"),ae=require("./bundlePublication.cjs"),oe=require("./webPush.cjs"),se=require("./remoteAccessStore.cjs"),ce=require("./remoteRoutes.cjs"),le=require("./tunnelProcess.cjs"),ue=require("./directoryListing.cjs"),_=require("./sessionFiles.cjs"),de=require("./threadJournals.cjs"),fe=require("./webHubPluginLoader.cjs"),v=require("./webTelemetry.cjs"),pe=require("./sessionWebComposition.cjs");let y=require("node:fs");y=e.__toESM(y,1);let b=require("node:path");b=e.__toESM(b,1);let x=require("node:url"),S=require("@agimon-ai/doompi/services"),me=require("node:crypto"),C=require("node:os");C=e.__toESM(C,1);let he=require("@hono/node-server"),ge=require("@hono/node-ws"),_e=require("@earendil-works/pi-coding-agent"),ve=require("@earendil-works/pi-server"),ye=require("@agimon-ai/doompi-config"),be=require("@agimon-ai/doompi/services/syncState"),xe=require("@agimon-ai/doompi-extension-contracts/session-protocol"),w=require("@agimon-ai/doompi-web-security"),Se=require("hono"),T=require("hono/cookie"),E=require("hono/body-limit"),D=require("@agimon-ai/doompi-extension-contracts/package-api"),O=require("@agimon-ai/doompi-extension-contracts/package-api-loader"),Ce=require("@agimon-ai/doompi-extension-contracts/sandbox-harness"),k=require("@agimon-ai/doompi/utils/repository");const A=`DOOMPI_WEB_PACKAGE_ROOT`,we=`/api/web-plugins/`,Te=8388608,j=new Set([`GET`,`HEAD`,`POST`,`PUT`,`PATCH`,`DELETE`]),M=new Set([`connection`,`content-length`,`cookie`,`forwarded`,`host`,`origin`,`transfer-encoding`,`upgrade`,`x-forwarded-for`,`x-forwarded-host`,`x-forwarded-proto`]);function Ee(e){if(typeof e!=`object`||!e)return;let t=e;if(t.v!==1||typeof t.method!=`string`||!j.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 De(e,t){try{let n=new Headers;for(let[t,r]of e.headers){let e=t.toLowerCase();M.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 Oe(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 N(e){let t=process.env[A];if(t!==void 0&&t!==``)return b.default.join(t,`dist`,e);let n=b.default.dirname((0,x.fileURLToPath)(require("url").pathToFileURL(__filename).href));for(;;){if(y.default.existsSync(b.default.join(n,`package.json`)))return b.default.join(n,`dist`,e);let t=b.default.dirname(n);if(t===n)throw Error(`doompi-web could not locate its own package root.`);n=t}}function P(){return N(`web`)}function ke(){return N(`pwa`)}function Ae(){let e=process.env[A],t=e!==void 0&&e!==``?e:b.default.dirname((0,x.fileURLToPath)(require("url").pathToFileURL(__filename).href));for(;;){let e=b.default.join(t,`package.json`);if(y.default.existsSync(e))try{let t=JSON.parse(y.default.readFileSync(e,`utf8`)),n=I(t)?t.version:void 0;return typeof n==`string`?n:`unknown`}catch{return`unknown`}let n=b.default.dirname(t);if(n===t)return`unknown`;t=n}}function je(e,t,n){if(e!==void 0)return e;let r=process.env.DOOMPI_WEB_DIST;if(r!==void 0&&r!==``)return r;let i=t?.webDirectory;return i!=null&&y.default.existsSync(b.default.join(i,`index.html`))?(n(`serving the synced cockpit bundle from ${i}`),i):P()}function F(e){try{return y.default.statSync(e).isFile()?y.default.readFileSync(e):void 0}catch{return}}function I(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function Me(e,t){let n=/^bytes=(\d*)-(\d*)$/.exec(e);if(n===null||t===0)return;let[,r=``,i=``]=n;if(r===``&&i===``)return;if(r===``){let e=Number(i);return!Number.isSafeInteger(e)||e<=0?void 0:{start:Math.max(0,t-e),end:t-1}}let a=Number(r),o=i===``?t-1:Number(i);if(!(!Number.isSafeInteger(a)||!Number.isSafeInteger(o)||a>=t||o<a))return{start:a,end:Math.min(o,t-1)}}function Ne(e,t){let n=e.firstMessage.split(`
2
- `,1)[0]?.trim();return(e.name?.trim()||n||b.default.basename(t)||`untitled`).slice(0,80)}function L(e){return e instanceof Error?e.message:String(e)}function Pe(e,t){return`${e}\n${t}`}function Fe(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,...n,...e.computerUse===void 0?{}:{computerUse:e.computerUse},telemetry:r,onNotice:t})}function R(e){return`repo-${(0,me.createHash)(`sha256`).update(e).digest(`base64url`).slice(0,24)}`}function Ie(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=y.default.realpathSync((0,k.findRepositoryRoot)(n.cwd));t.set(e,{repository:{id:R(e),path:e,name:b.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 Le(e,t,n,r,i){let a=[],o=new Map,s=e=>{for(let t of e){if(o.has(t.basePath))continue;let e;try{e=t.start({scope:`hub`,onNotice:n,resolveRepository:r,readRepositorySync:i})}catch(e){n(`hub API '${t.basePath}' did not start (${L(e)}); its routes stay unmounted`);continue}o.set(t.basePath,e),a.push(e)}};return s(t),e.all(`${D.DOOM_API_ROUTE_PREFIX}/:basePath/*`,async(e,t)=>{if(e.req.query(`session`)!==void 0)return t();let r=e.req.param(`basePath`),i=o.get(r);if(i===void 0)return t();let a=`${D.DOOM_API_ROUTE_PREFIX}/${r}`,s=new URL(e.req.url);s.pathname=s.pathname.slice(a.length)||`/`;try{return await i.fetch(new Request(s,e.req.raw))}catch(t){return n(`hub API '${r}' failed on ${s.pathname} (${L(t)})`),e.json({error:`The '${r}' API failed.`},500)}}),{handlers:a,add:s}}function Re(e,t,n,r,i){e.all(`${D.DOOM_API_ROUTE_PREFIX}/*`,async e=>{let a=e.req.query(s.API_SESSION_QUERY_PARAM);if(a===void 0)return e.notFound();let o=t.snapshot().find(e=>e.id===a);if(o===void 0)return e.json({error:`No session ${a}.`},404);if(o.apiSocketPath===void 0)return e.json({error:`Session ${a} serves no package API.`},404);let c=new URL(e.req.url);c.searchParams.delete(s.API_SESSION_QUERY_PARAM);let l=v.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:o.apiSocketPath,path:`${c.pathname}${c.search}`,method:e.req.method,headers:e.req.raw.headers,body:e.req.raw.body,caller:i(e),trace:v.forwardedTraceContext(t,l)}),l)}catch(t){return n(`session ${a} API is unreachable (${L(t)})`),e.json({error:`Session ${a} is not answering.`},502)}})}async function z(e){let l=e.host??`127.0.0.1`,d=e.onNotice??(()=>{}),f=v.createWebTelemetry(),p=se.createRemoteAccessStore({stateDir:e.remoteStateDir,onNotice:d}),x=e=>{try{return y.default.realpathSync((0,k.findRepositoryRoot)(e))}catch{return}},D=e=>{let t=C.default.homedir(),n=(0,k.resolveDoomConfigurationRoot)(e,t);if(n===(0,ye.globalDoomConfigDirectory)(t))return n;try{return y.default.realpathSync(n)}catch{return b.default.resolve(n)}},A=e.compositionDir===void 0?void 0:x(e.compositionDir);if(e.compositionDir!==void 0&&A===void 0)throw Error(`Cockpit composition directory is not inside a repository: ${e.compositionDir}`);let j=(0,ye.globalDoomConfigDirectory)(C.default.homedir()),M=n.createSyncGuard({repoRoot:j,onNotice:d}),N=async e=>{},P=async e=>{if(e===j)await M.ensureSynced();else{let t=n.createSyncGuard({repoRoot:e,onNotice:d});try{await t.ensureSynced()}finally{t.close()}}await N(e)},R=async e=>{try{await P(e)}catch(t){d(`WARNING: startup sync failed for ${e}; continuing to serve the cockpit (${L(t)})`)}};await R(j);let z=async e=>await P(D(e)),ze=[...u.readRegistryRecords(e.registryDir,d).map(e=>D(e.cwd)),...A===void 0?[]:[D(A)]];await Promise.all([...new Set(ze)].map(R));let Be={assetsDir:je(e.assetsDir,void 0,d),apiDirectory:void 0},B=ae.createPluginBundlePublication(p.directory,d),V=Fe(e,d,{loadChannels:async e=>{let t=D(e.cwd);await P(t);let n=(0,S.readSyncRegistration)(t);return n?.webDirectory===null||n?.webDirectory===void 0?[]:await fe.loadHubChannels(n.webDirectory,d)},webComposition:(e,t)=>{let n=pe.resolveSessionWebArtifacts(D(e.cwd));if(n===void 0)return;let r=B.publish(n.id,n.pluginsDir);if(r===void 0)return;let i=r.signed.manifest.revision,a=`${we}${n.id}/${String(i)}`;return{id:n.id,revision:i,manifestUrl:`${a}/manifest`,rawAssetBaseUrl:`${a}/assets`,verifiedAssetBaseUrl:`/verified-plugins/${n.id}/${String(i)}`,entryPath:n.entryPath,stylePaths:n.stylePaths,channels:[...t]}}},f),H=de.createThreadJournals({resolve:(e,t)=>V.threadJournal(e,t),onNotice:d}),U=new Set,Ve=(e,t)=>{for(let n of U)t&&!n.local||n.post(e)},W=new Se.Hono,G=(0,ge.createNodeWebSocket)({app:W}),He,K=ke(),q=ae.createBundlePublication({assetsDir:()=>Be.assetsDir,stateDir:p.directory,onNotice:d}),J;le.reapStaleTunnel(p.directory,d);let Ue=e.onHandover===void 0?void 0:t=>{let n=g.planSessionMigration(V.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))d(e);for(let e of n.migrate){let t=V.stop(e.id);t.ok||d(`could not stop ${e.name??e.id} before the move: ${t.error}`)}e.onHandover?.({settings:t,sessions:n.migrate})},Y=ie.createRemoteAccess({store:p,launchTunnel:e.remoteAccess?.launchTunnel??le.createTunnelLauncher({stateDir:p.directory,...e.cloudflaredPath===void 0?{}:{cloudflaredPath:e.cloudflaredPath},onNotice:d,onExit:e=>{d(`remote access: ${e}`),Y.disable()}}),bindListener:async()=>await new Promise((e,t)=>{let n=(0,he.serve)({fetch:W.fetch,port:0,hostname:`127.0.0.1`,serverOptions:{maxHeaderSize:16384,headersTimeout:5e3,requestTimeout:1e4}},t=>{G.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)}),bundleTrust:()=>q.trust(),onDeviceDropped:e=>J?.remove(e),onNotice:d,...Ue===void 0?{}:{requestHandover:Ue},contained:(0,Ce.insideSandbox)(process.env),broadcastLocal:e=>Ve(e,!0),broadcastAll:e=>Ve(e,!1),...e.remoteAccess?.now===void 0?{}:{now:e.remoteAccess.now}});J=oe.createLiveWebPush({stateDir:p.directory,isConnected:e=>Y.isDeviceConnected(e),onNotice:d});let We=V.onEvent(e=>{e.kind===`frame`&&c.parseDoomNotificationEntry(e.frame)!==void 0&&J?.notify()}),X=re.createRemoteGuard({loopbackPort:()=>He,tunnelPolicy:()=>Y.tunnelPolicy(),authorize:e=>Y.authorize((0,T.getCookie)(e,m.DEVICE_COOKIE,`host`)),trustedDevice:e=>e.env?.sealedDeviceId,channelReady:(e,t)=>Y.channelFor(e,t)!==void 0,stepUp:{required:e=>Y.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??Y.authorize((0,T.getCookie)(e,`doompi_device`,`host`)),o=a===void 0?`local`:`device:${a}`;return await Y.passkeys().finishStepUp(r,o,t,i)}},extraOrigins:h.allowedOriginsFromEnv(process.env.DOOMPI_WEB_ALLOW_ORIGIN)});W.use(`*`,X.middleware),W.use(`*`,async(e,t)=>{let n=new URL(e.req.url).pathname;if(n===m.REMOTE_HTTP_ROUTE)return t();let r=v.requestOperation(n);if(r===void 0)return t();let i=performance.now();return await f.runInSpan(r,{"operation.name":r,"http.method":e.req.method},async()=>{await t(),await f.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))})},v.readTraceContext(e.req.raw.headers))}),W.use(`*`,async(e,t)=>{if(X.listenerOf(e)===`local`)return t();let n=Number(e.req.header(`content-length`));return Number.isFinite(n)&&n>Te?e.json({error:`The tunnel request body is too large.`},413):t()}),ce.registerRemoteRoutes(W,{remote:Y,listenerOf:e=>X.listenerOf(e)});let Ge=(0,E.bodyLimit)({maxSize:Te,onError:e=>e.json({error:`The tunnel request body is too large.`},413)});W.use(`*`,async(e,t)=>X.listenerOf(e)===`local`?t():await Ge(e,t));let Ke=(0,E.bodyLimit)({maxSize:32768,onError:e=>e.json({error:`The telemetry batch is too large.`},413)}),qe=v.createBrowserTelemetryRateLimit();W.post(v.WEB_TELEMETRY_ROUTE,Ke,async e=>{if(!qe())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=v.parseBrowserTelemetryBatch(t);if(n===void 0)return e.json({error:`The telemetry batch is invalid.`},400);for(let e of n){if(v.isBrowserErrorEvent(e)){await f.recordError(v.BROWSER_ERROR_EVENT,v.createBrowserError(e),{"browser.source":e.source,...e.session_id===void 0?{}:{"session.id":e.session_id},"failure.kind":`browser`},{includeException:!0});continue}await f.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)}),W.post(m.REMOTE_HTTP_ROUTE,async e=>{let t=Y.authorize((0,T.getCookie)(e,m.DEVICE_COOKIE,`host`));if(t===void 0)return e.json({error:`This device is not paired.`},401);let n=Y.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=Ee(a),s=o===void 0?void 0:De(o,e),c=o===void 0?void 0:Oe(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 W.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 Je=(0,E.bodyLimit)({maxSize:8192,onError:e=>e.json({error:`The Push subscription is too large.`},413)}),Z=e=>e.env?.sealedDeviceId;W.get(m.REMOTE_PUSH_KEY_ROUTE,e=>Z(e)===void 0?e.json({error:`A sealed paired device is required.`},401):e.json({publicKey:J?.publicKey()},200,{"Cache-Control":`no-store`})),W.post(m.REMOTE_PUSH_ROUTE,Je,async e=>{let t=Z(e);if(t===void 0)return e.json({error:`A sealed paired device is required.`},401);let n;try{n=await e.req.json()}catch{return e.json({error:`The Push subscription must be JSON.`},400)}return J?.subscribe(t,n)===!0?e.body(null,204):e.json({error:`The Push subscription is invalid.`},400)}),W.delete(m.REMOTE_PUSH_ROUTE,e=>{let t=Z(e);return t===void 0?e.json({error:`A sealed paired device is required.`},401):(J?.remove(t),e.body(null,204))});let Ye=r.createPiWebSocketListener({onError:e=>d(`protocol: ${e.message}`)}),Xe=r.createPiWebSocketListener({onError:e=>d(`protocol: ${e.message}`)}),Ze=t.createPiHubService({records:()=>V.records(),spawn:e=>V.create(e),onNotice:d}),Qe=new ve.Server(Ze,{serverId:xe.DOOM_COCKPIT_SERVER_ID,listeners:[Ye],onError:e=>d(`protocol: ${e.message}`)}),$e=new ve.Server(Ze,{serverId:xe.DOOM_COCKPIT_SERVER_ID,listeners:[Xe],onError:e=>d(`protocol: ${e.message}`)});await Promise.all([Qe.start(),$e.start()]);let Q=ne.createProviderAuth({runtime:e.authRuntime,onNotice:d});ee.registerAuthRoutes(W,Q);let et=Ie(V),tt=()=>et.list(),nt=e=>et.resolve(e),rt=e=>{let t=nt(e);if(t===void 0)return;let n=(0,S.readSyncDrift)({repoRoot:t}),r;try{r=(0,S.readSyncRegistration)(t)?(0,be.readSyncState)(t):void 0}catch{r=void 0}return{fresh:n.fresh,reasons:[...n.reasons],...r===void 0?{}:{mcpProjection:r.fileState.mcpProjection}}};te.registerSettingsRoutes(W,{repositories:tt,models:()=>Q.listModels()});let it=Be.apiDirectory,$=Le(W,it===void 0&&!process.env[O.PACKAGE_API_DIR_ENV]?[]:await(0,O.loadPackageApis)(`hub`,{apiDirectory:it,onNotice:d}),d,nt,rt);N=async e=>{let t=(0,S.readSyncRegistration)(e);if(t===void 0)return;let n=await(0,O.loadPackageApis)(`hub`,{apiDirectory:t.apiDirectory,env:{},onNotice:d});$.add(n)},await Promise.all([...new Set([j,...ze])].map(N)),Re(W,V,d,f,e=>X.callerOf(e)),W.get(`/api/health`,e=>e.json({ok:!0,role:`hub`,protocol:2,sessions:V.snapshot().length,pid:process.pid,version:Ae()})),W.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(!I(t)||typeof t.cwd!=`string`||t.cwd===``)return e.json({error:`A cwd string is required.`},400);let n;try{n=y.default.statSync(t.cwd)}catch{return e.json({error:`No such directory: ${t.cwd}`},400)}if(!b.default.isAbsolute(t.cwd)||!n.isDirectory())return e.json({error:`The working directory must be an absolute path to a directory, received "${t.cwd}".`},400);let r=typeof t.name==`string`&&t.name!==``?t.name:void 0;await z(t.cwd);let i=await V.create({cwd:t.cwd,name:r,trace:v.readTraceContext(e.req.raw.headers)});return i.ok?e.json({sessionId:i.sessionId},201):e.json({error:i.error},i.code===`invalid_request`?400:502)}),W.post(`${s.SESSIONS_API_ROUTE}/:sessionId/restart`,async e=>{let t=e.req.param(`sessionId`),n=V.snapshot().find(e=>e.id===t);n!==void 0&&await z(n.cwd);let r=await V.restart(t,v.readTraceContext(e.req.raw.headers));return r.ok?e.json({sessionId:r.sessionId},202):e.json({error:r.error},r.code===`invalid_request`?404:502)}),W.get(`${s.SESSIONS_API_ROUTE}/:sessionId/history`,async e=>{let t=e.req.param(`sessionId`),n=V.snapshot().find(e=>e.id===t);if(!n)return e.json({error:`Unknown session.`},404);let r=await _e.SessionManager.list(n.cwd);return e.json({sessions:r.map(e=>({id:e.id,...e.name===void 0?{}:{name:e.name},firstMessage:e.firstMessage,createdAt:e.created.toISOString(),updatedAt:e.modified.toISOString(),messageCount:e.messageCount}))})}),W.post(`${s.SESSIONS_API_ROUTE}/:sessionId/resume`,async e=>{let t=e.req.param(`sessionId`),n=V.snapshot().find(e=>e.id===t);if(!n)return e.json({error:`Unknown session.`},404);let r;try{r=await e.req.json()}catch{return e.json({error:`The request body must be JSON.`},400)}if(!I(r)||typeof r.targetSessionId!=`string`||r.targetSessionId===``)return e.json({error:`A targetSessionId string is required.`},400);let i=(await _e.SessionManager.list(n.cwd)).find(e=>e.id===r.targetSessionId);if(!i)return e.json({error:`That Pi thread does not belong to this workspace.`},404);await z(n.cwd);let a=await V.resume(t,{sessionId:i.id,name:Ne(i,n.cwd)},v.readTraceContext(e.req.raw.headers));return a.ok?e.json({sessionId:a.sessionId},202):e.json({error:a.error},a.code===`invalid_request`?409:502)}),W.delete(`${s.SESSIONS_API_ROUTE}/:sessionId`,e=>{let t=e.req.param(`sessionId`),n=V.stop(t);return n.ok?e.json({sessionId:t},202):e.json({error:n.error},n.code===`unknown`?404:n.code===`self`?409:502)}),W.get(`/api/sessions/:sessionId/files`,async e=>{let t=e.req.param(`sessionId`),n=V.snapshot().find(e=>e.id===t);if(!n)return e.json({error:`Unknown session.`},404);let r=e.req.query(`q`)??``,i=await _.listSessionFiles(n.cwd,r,20);return e.json({files:i})});let at=t=>{if(!(X.listenerOf(t)===`local`||(0,Ce.insideSandbox)(process.env)))return e.browseRoot??process.cwd()};W.get(s.DIRECTORIES_API_ROUTE,async e=>{let t=at(e),n=await ue.suggestDirectories(e.req.query(`q`)??``,{limit:12,...t===void 0?{}:{root:t}});return e.json({directories:n})}),W.get(i.SESSION_FILE_ROUTE,async e=>{let t=e.req.param(`sessionId`),n=V.snapshot().find(e=>e.id===t);if(!n)return e.json({error:`Unknown session.`},404);let r=e.req.query(`path`)??``,o=await _.readSessionFile(n.cwd,r,i.MAX_SESSION_FILE_BYTES);if(o.status===`forbidden`)return e.json({error:`The path leaves the session directory.`},403);if(o.status===`not-found`)return e.json({error:`No such file.`},404);if(o.status===`too-large`)return e.json({error:`The file is too large to preview.`},413);let s={...a.sessionFileHeaders(r),[i.SESSION_FILE_SHA256_HEADER]:o.sha256,"Accept-Ranges":`bytes`},c=e.req.header(`range`);if(c===void 0)return e.body(new Uint8Array(o.body),200,s);let l=Me(c,o.body.byteLength);if(l===void 0)return e.body(null,416,{...s,"Content-Range":`bytes */${String(o.body.byteLength)}`});let u=o.body.subarray(l.start,l.end+1);return e.body(new Uint8Array(u),206,{...s,"Content-Range":`bytes ${String(l.start)}-${String(l.end)}/${String(o.body.byteLength)}`,"Content-Length":String(u.byteLength)})});let ot=(0,E.bodyLimit)({maxSize:i.MAX_SESSION_FILE_BYTES,onError:e=>e.json({error:`The file is too large to save.`},413)});return W.put(i.SESSION_FILE_ROUTE,ot,async e=>{let t=e.req.param(`sessionId`),n=V.snapshot().find(e=>e.id===t);if(!n)return e.json({error:`Unknown session.`},404);if(V.planSaveState(t)!==`allowed`)return e.json({error:`Host saves are locked until Plan is authoritatively inactive.`},423);let r=e.req.header(i.SESSION_FILE_EXPECTED_SHA256_HEADER);if(r===void 0||!/^[a-f0-9]{64}$/.test(r))return e.json({error:`A lowercase SHA-256 ${i.SESSION_FILE_EXPECTED_SHA256_HEADER} header is required.`},400);let a=Buffer.from(await e.req.arrayBuffer()),o=await _.writeSessionFile(n.cwd,e.req.query(`path`)??``,a,r,i.MAX_SESSION_FILE_BYTES,()=>V.planSaveState(t)===`allowed`);return o.status===`ok`?e.body(null,204,{[i.SESSION_FILE_SHA256_HEADER]:o.sha256}):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 save.`},413):o.status===`locked`?e.json({error:`Host saves are locked until Plan is authoritatively inactive.`},423):e.json({error:`The file changed after it was read.`},409)}),W.get(m.SESSION_SOCKET_ROUTE,G.upgradeWebSocket(e=>{let t=X.listenerOf(e)===`local`,n=t?void 0:Y.authorize((0,T.getCookie)(e,m.DEVICE_COOKIE,`host`)),r=n===void 0?void 0:Y.channelFor(n,`session`),i=new Set,a=(0,me.randomUUID)(),o=new Map,l,u,f,p,h=e=>{for(let[t,n]of o)(e===void 0||n.sessionId===e)&&(o.delete(t),H.unsubscribe(n.sessionId,n.threadId))};return{onOpen(e,m){if(!t&&r===void 0){m.close(1008,`sealed session channel required`);return}let g=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:g,local:t},U.add(f),n!==void 0&&(p=Y.trackSocket(n,(e,t)=>m.close(e,t))),g(s.hubHello(V.channelTypes())),g({type:s.SESSIONS_SNAPSHOT_TYPE,sessions:V.snapshot()}),l=V.onEvent(e=>{e.kind===`upsert`?g({type:s.SESSION_UPSERT_TYPE,session:e.session}):e.kind===`removed`?(i.delete(e.sessionId),h(e.sessionId),g({type:s.SESSION_REMOVED_TYPE,sessionId:e.sessionId})):e.kind===`channel`?(e.connectionId===a||e.connectionId===void 0&&i.has(e.sessionId))&&g({type:e.frameType,sessionId:e.sessionId,payload:e.payload}):(c.parseDoomNotificationEntry(e.frame)!==void 0||i.has(e.sessionId))&&g(s.sessionFrameEnvelope(e.sessionId,e.frame))}),u=H.onFrame(e=>{o.has(Pe(e.sessionId,e.threadId))&&g(s.threadFrameEnvelope(e.sessionId,e.threadId,e.frame))}),d(`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(!I(n)||typeof n.sessionId!=`string`)return;let c=n.sessionId;if(typeof n.type==`string`&&V.channelTypes().includes(n.type)&&(i.has(c)||V.channelReceivesWithoutSubscription(c,n.type))){V.receiveChannel(c,n.type,n.payload,a);return}if(n.type===`subscribe`){let e=V.backlog(c);if(!e)return;i.add(c),f?.post(e);for(let e of V.channelFrames(c))f?.post(e);return}if(n.type===`unsubscribe`){i.delete(c);return}if(n.type===`history_request`){let e=V.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=Pe(c,e);if(n.type===`unsubscribe_thread`){o.delete(t)&&H.unsubscribe(c,e);return}if(o.has(t)||V.backlog(c)===void 0)return;o.set(t,{sessionId:c,threadId:e}),f?.post(s.threadBacklog(c,e,H.subscribe(c,e)));return}if(n.type===`session_command`&&I(n.frame)){if(n.frame.type===`attach`)return;V.command(c,n.frame)}},onClose(){V.disconnectChannels(a),f&&U.delete(f),f=void 0,p?.(),p=void 0,l?.(),l=void 0,u?.(),u=void 0,i.clear(),h(),d(`browser detached`)}}})),W.get(m.PROTOCOL_SOCKET_ROUTE,G.upgradeWebSocket(e=>{let t=X.listenerOf(e)===`local`,n=t?void 0:Y.authorize((0,T.getCookie)(e,m.DEVICE_COOKIE,`host`)),r=n===void 0?void 0:Y.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=Y.trackSocket(n,(e,t)=>o.close(e,t))),i=(t?Ye:Xe).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}}})),W.get(`/manifest.webmanifest`,e=>{let t=F(b.default.join(K,`manifest.webmanifest`));return t===void 0?e.text(`The PWA manifest is unavailable.`,404):e.body(new Uint8Array(t),200,{"Cache-Control":`no-store`,"Content-Type":`application/manifest+json; charset=utf-8`})}),W.get(`/sw.js`,e=>{let t=F(b.default.join(K,`sw.js`));return t===void 0?e.text(`The trusted service worker is unavailable.`,404):e.body(new Uint8Array(t),200,{"Cache-Control":`no-cache`,"Content-Type":`text/javascript; charset=utf-8`,"Service-Worker-Allowed":`/`})}),W.get(`/pwa/*`,e=>{let t=new URL(e.req.url).pathname.slice(5),n=o.resolveAssetPath(K,`/${t}`),r=n===void 0?void 0:F(n);return n===void 0||r===void 0?e.text(`PWA asset not found.`,404):e.body(new Uint8Array(r),200,{"Cache-Control":`no-store`,"Content-Type":o.contentTypeFor(n),"X-Content-Type-Options":`nosniff`})}),W.get(`${we}*`,e=>{let t=new URL(e.req.url).pathname,n=/^\/api\/web-plugins\/([a-f0-9]{64})\/([1-9][0-9]*)\/(manifest|assets(\/.*))$/u.exec(t),r=n===null?void 0:Number(n[2]),i=n===null||!Number.isSafeInteger(r)?void 0:B.get(n[1]??``,r);if(n===null||i===void 0)return e.text(`That signed plugin composition is unavailable.`,404);if(n[3]===`manifest`)return e.json(i.signed,200,{"Cache-Control":`no-store`});let a=(0,w.assetFor)(i.signed.manifest,n[4]??``),s=a===void 0?void 0:o.resolveAssetPath(i.assetsDir,a.path),c=s===void 0?void 0:F(s);return a===void 0||s===void 0||c===void 0?e.text(`Plugin composition asset not found.`,404):e.body(new Uint8Array(c),200,{"Cache-Control":`no-store`,"Content-Length":String(c.byteLength),"Content-Type":a.contentType,"X-Content-Type-Options":`nosniff`})}),W.get(w.BUNDLE_MANIFEST_ROUTE,e=>{let t=q.current();return t===void 0?e.json({error:`No signed cockpit bundle is available.`},503):e.json(t.signed,200,{"Cache-Control":`no-store`})}),W.get(`/bundle-assets/*`,e=>{let t=new URL(e.req.url).pathname,n=/^\/bundle-assets\/([1-9][0-9]*)(\/.*)$/u.exec(t),r=n===null?void 0:Number(n[1]),i=q.current();if(n===null||!Number.isSafeInteger(r)||i===void 0||i.signed.manifest.revision!==r)return e.text(`That signed bundle revision is unavailable.`,404);let a=(0,w.assetFor)(i.signed.manifest,n[2]??``),s=a===void 0?void 0:o.resolveAssetPath(i.assetsDir,a.path),c=s===void 0?void 0:F(s);return a===void 0||s===void 0||c===void 0?e.text(`Bundle asset not found.`,404):e.body(new Uint8Array(c),200,{"Cache-Control":`no-store`,"Content-Length":String(c.byteLength),"Content-Type":a.contentType,"X-Content-Type-Options":`nosniff`})}),W.get(`/`,e=>e.redirect(m.PAIRING_PAGE_ROUTE)),W.get(`*`,e=>{let t=new URL(e.req.url).pathname,n=e.req.header(`accept`)?.includes(`text/html`)===!0;return!t.startsWith(`/api/`)&&n?e.redirect(`/`):e.text(`Not found.`,404)}),M.watch(()=>{for(let e of V.records())D(e.cwd)===j&&V.reloadChannels(e.id)}),new Promise((t,n)=>{let r=(0,he.serve)({fetch:W.fetch,port:e.port,hostname:l},e=>{He=e.port,G.injectWebSocket(r);let n=`http://${l}:${e.port}`;d(`cockpit on ${n}`);let i,a=async()=>{await Y.close(),await new Promise(e=>{H.close(),M.close(),Qe.close(),$e.close(),We(),J?.close(),V.close(),Q.close();for(let e of $.handlers)e.close();B.close();for(let e of G.wss.clients)e.terminate();G.wss.close(),r.closeAllConnections?.(),r.close(()=>e())}),await v.shutdownWebTelemetry(f)};t({url:n,port:e.port,close:()=>i??=a()})});r.once(`error`,e=>{M.close(),H.close(),We(),J?.close(),V.close(),Q.close(),Y.close();for(let e of $.handlers)e.close();B.close(),f.shutdown(),n(e)})})}exports.createSettingsRepositoryRegistry=Ie,exports.mountHubApis=Le,exports.packagedVersion=Ae,exports.serveWeb=z;
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"),ee=require("../services/remoteGuardPolicy.cjs"),h=require("../services/sessionMigration.cjs"),te=require("./authRoutes.cjs"),ne=require("./settingsRoutes.cjs"),re=require("./providerAuth.cjs"),ie=require("./remoteGuard.cjs"),ae=require("./remoteAccess.cjs"),oe=require("./bundlePublication.cjs"),se=require("./webPush.cjs"),ce=require("./remoteAccessStore.cjs"),le=require("./remoteRoutes.cjs"),ue=require("./tunnelProcess.cjs"),de=require("./directoryListing.cjs"),g=require("./sessionFiles.cjs"),fe=require("./threadJournals.cjs"),pe=require("./webHubPluginLoader.cjs"),_=require("./webTelemetry.cjs"),me=require("./sessionWebComposition.cjs");let v=require("node:fs");v=e.__toESM(v,1);let y=require("node:path");y=e.__toESM(y,1);let b=require("node:url"),x=require("@agimon-ai/doompi/services"),he=require("node:crypto"),S=require("node:os");S=e.__toESM(S,1);let ge=require("@hono/node-server"),_e=require("@hono/node-ws"),ve=require("@earendil-works/pi-coding-agent"),ye=require("@earendil-works/pi-server"),be=require("@agimon-ai/doompi-config"),xe=require("@agimon-ai/doompi/services/syncState"),Se=require("@agimon-ai/doompi-extension-contracts/session-protocol"),Ce=require("@agimon-ai/doompi-web-security"),we=require("hono"),C=require("hono/cookie"),w=require("hono/body-limit"),T=require("@agimon-ai/doompi-extension-contracts/package-api"),E=require("@agimon-ai/doompi-extension-contracts/package-api-loader"),Te=require("@agimon-ai/doompi-extension-contracts/sandbox-harness"),D=require("@agimon-ai/doompi/utils/repository");const Ee=`index.html`,O=`DOOMPI_WEB_PACKAGE_ROOT`,De=`/api/web-plugins/`,Oe=8388608,k=new Set([`GET`,`HEAD`,`POST`,`PUT`,`PATCH`,`DELETE`]),A=new Set([`connection`,`content-length`,`cookie`,`forwarded`,`host`,`origin`,`transfer-encoding`,`upgrade`,`x-forwarded-for`,`x-forwarded-host`,`x-forwarded-proto`]);function ke(e){if(typeof e!=`object`||!e)return;let t=e;if(t.v!==1||typeof t.method!=`string`||!k.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 Ae(e,t){try{let n=new Headers;for(let[t,r]of e.headers){let e=t.toLowerCase();A.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 je(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 j(e){let t=process.env[O];if(t!==void 0&&t!==``)return y.default.join(t,`dist`,e);let n=y.default.dirname((0,b.fileURLToPath)(require("url").pathToFileURL(__filename).href));for(;;){if(v.default.existsSync(y.default.join(n,`package.json`)))return y.default.join(n,`dist`,e);let t=y.default.dirname(n);if(t===n)throw Error(`doompi-web could not locate its own package root.`);n=t}}function M(){return j(`web`)}function Me(){return j(`pwa`)}function Ne(){let e=process.env[O],t=e!==void 0&&e!==``?e:y.default.dirname((0,b.fileURLToPath)(require("url").pathToFileURL(__filename).href));for(;;){let e=y.default.join(t,`package.json`);if(v.default.existsSync(e))try{let t=JSON.parse(v.default.readFileSync(e,`utf8`)),n=P(t)?t.version:void 0;return typeof n==`string`?n:`unknown`}catch{return`unknown`}let n=y.default.dirname(t);if(n===t)return`unknown`;t=n}}function Pe(e,t,n){if(e!==void 0)return e;let r=process.env.DOOMPI_WEB_DIST;if(r!==void 0&&r!==``)return r;let i=t?.webDirectory;return i!=null&&v.default.existsSync(y.default.join(i,Ee))?(n(`serving the synced cockpit bundle from ${i}`),i):M()}function N(e){try{return v.default.statSync(e).isFile()?v.default.readFileSync(e):void 0}catch{return}}function P(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function Fe(e,t){let n=/^bytes=(\d*)-(\d*)$/.exec(e);if(n===null||t===0)return;let[,r=``,i=``]=n;if(r===``&&i===``)return;if(r===``){let e=Number(i);return!Number.isSafeInteger(e)||e<=0?void 0:{start:Math.max(0,t-e),end:t-1}}let a=Number(r),o=i===``?t-1:Number(i);if(!(!Number.isSafeInteger(a)||!Number.isSafeInteger(o)||a>=t||o<a))return{start:a,end:Math.min(o,t-1)}}function Ie(e,t){let n=e.firstMessage.split(`
2
+ `,1)[0]?.trim();return(e.name?.trim()||n||y.default.basename(t)||`untitled`).slice(0,80)}function F(e){return e instanceof Error?e.message:String(e)}function Le(e,t){return`${e}\n${t}`}function Re(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,...n,...e.computerUse===void 0?{}:{computerUse:e.computerUse},telemetry:r,onNotice:t})}function I(e){return`repo-${(0,he.createHash)(`sha256`).update(e).digest(`base64url`).slice(0,24)}`}function ze(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=v.default.realpathSync((0,D.findRepositoryRoot)(n.cwd));t.set(e,{repository:{id:I(e),path:e,name:y.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 Be(e,t,n,r,i,a=()=>`default`,o=`default`){let c=[],l=new Map,u=(e,t=o)=>{let a=l.get(t)??new Map;l.set(t,a);for(let t of e){if(a.has(t.basePath))continue;let e;try{e=t.start({scope:`hub`,onNotice:n,resolveRepository:r,readRepositorySync:i})}catch(e){n(`hub API '${t.basePath}' did not start (${F(e)}); its routes stay unmounted`);continue}a.set(t.basePath,e),c.push(e)}};return u(t),e.all(`${T.DOOM_API_ROUTE_PREFIX}/:basePath/*`,async(e,t)=>{let r=e.req.query(s.API_SESSION_QUERY_PARAM),i=e.req.query(T.DOOM_HUB_API_SESSION_QUERY_PARAM);if(r!==void 0&&i!==void 0)return e.json({error:`A package API request cannot select both a session API and a hub bundle.`},400);if(r!==void 0)return t();let c=i===void 0?o:a(i);if(c===void 0)return e.notFound();let u=e.req.param(`basePath`),d=l.get(c)?.get(u);if(d===void 0)return t();let f=`${T.DOOM_API_ROUTE_PREFIX}/${u}`,p=new URL(e.req.url);p.pathname=p.pathname.slice(f.length)||`/`,p.searchParams.delete(T.DOOM_HUB_API_SESSION_QUERY_PARAM);try{return await d.fetch(new Request(p,e.req.raw))}catch(t){return n(`hub API '${u}' failed on ${p.pathname} (${F(t)})`),e.json({error:`The '${u}' API failed.`},500)}}),{handlers:c,add:u,remove:e=>{let t=l.get(e);if(t!==void 0){l.delete(e);for(let e of t.values()){e.close();let t=c.indexOf(e);t>=0&&c.splice(t,1)}}}}}function Ve(e,t,n,r,i){e.all(`${T.DOOM_API_ROUTE_PREFIX}/*`,async e=>{let a=e.req.query(s.API_SESSION_QUERY_PARAM);if(a===void 0)return e.notFound();let o=t.snapshot().find(e=>e.id===a);if(o===void 0)return e.json({error:`No session ${a}.`},404);if(o.apiSocketPath===void 0)return e.json({error:`Session ${a} serves no package API.`},404);let c=new URL(e.req.url);c.searchParams.delete(s.API_SESSION_QUERY_PARAM);let l=_.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:o.apiSocketPath,path:`${c.pathname}${c.search}`,method:e.req.method,headers:e.req.raw.headers,body:e.req.raw.body,caller:i(e),trace:_.forwardedTraceContext(t,l)}),l)}catch(t){return n(`session ${a} API is unreachable (${F(t)})`),e.json({error:`Session ${a} is not answering.`},502)}})}async function L(e){let l=e.host??`127.0.0.1`,d=e.onNotice??(()=>{}),f=_.createWebTelemetry(),p=ce.createRemoteAccessStore({stateDir:e.remoteStateDir,onNotice:d}),b=e=>{try{return v.default.realpathSync((0,D.findRepositoryRoot)(e))}catch{return}},T=e=>{let t=S.default.homedir(),n=(0,D.resolveDoomConfigurationRoot)(e,t);if(n===(0,be.globalDoomConfigDirectory)(t))return n;try{return v.default.realpathSync(n)}catch{return y.default.resolve(n)}},O=e.compositionDir===void 0?void 0:b(e.compositionDir);if(e.compositionDir!==void 0&&O===void 0)throw Error(`Cockpit composition directory is not inside a repository: ${e.compositionDir}`);let k=(0,be.globalDoomConfigDirectory)(S.default.homedir()),A=n.createSyncGuard({repoRoot:k,onNotice:d}),j=async e=>{},M=new Set,I=e=>{try{let t=(0,x.readSyncRegistration)(e);if(t?.webDirectory===null||t?.webDirectory===void 0)return;let n=y.default.dirname(t.webDirectory);return v.default.existsSync(y.default.join(t.webDirectory,Ee))&&v.default.existsSync(y.default.join(n,`plugins`,`composition.js`))&&v.default.existsSync(y.default.join(n,`plugins`,`manifest.json`))&&v.default.existsSync(t.apiDirectory)?t:void 0}catch{return}},L=e=>e!==k&&M.has(e)?I(k):I(e)??(e===k?void 0:I(k)),He=async e=>{try{if(e===k)await A.ensureSynced();else{let t=n.createSyncGuard({repoRoot:e,onNotice:d});try{await t.ensureSynced()}finally{t.close()}}M.delete(e)}catch(t){if(e===k||I(k)===void 0)throw t;M.add(e),d(`sync failed for ${e}; using the global DoomPi bundle (${F(t)})`)}await j(e)},Ue=async e=>{try{await He(e)}catch(t){d(`WARNING: startup sync failed for ${e}; continuing to serve the cockpit (${F(t)})`)}};await Ue(k);let R=async e=>await He(T(e)),We=[...u.readRegistryRecords(e.registryDir,d).map(e=>T(e.cwd)),...O===void 0?[]:[T(O)]];await Promise.all([...new Set(We)].map(Ue));let Ge={assetsDir:Pe(e.assetsDir,void 0,d),apiDirectory:void 0},z=oe.createPluginBundlePublication(p.directory,d),B=Re(e,d,{loadChannels:async e=>{let t=T(e.cwd);await He(t);let n=L(t);return n?.webDirectory===null||n?.webDirectory===void 0?[]:await pe.loadHubChannels(n.webDirectory,d)},webComposition:(e,t)=>{let n=L(T(e.cwd)),r=n===void 0?void 0:me.resolveSessionWebArtifacts(n.root,k);if(r===void 0)return;let i=z.publish(r.id,r.pluginsDir);if(i===void 0)return;let a=i.signed.manifest.revision,o=`${De}${r.id}/${String(a)}`;return{id:r.id,revision:a,manifestUrl:`${o}/manifest`,rawAssetBaseUrl:`${o}/assets`,verifiedAssetBaseUrl:`/verified-plugins/${r.id}/${String(a)}`,entryPath:r.entryPath,stylePaths:r.stylePaths,channels:[...t]}}},f),V=fe.createThreadJournals({resolve:(e,t)=>B.threadJournal(e,t),onNotice:d}),H=new Set,Ke=(e,t)=>{for(let n of H)t&&!n.local||n.post(e)},U=new we.Hono,W=(0,_e.createNodeWebSocket)({app:U}),qe,G=Me(),K=oe.createBundlePublication({assetsDir:()=>Ge.assetsDir,stateDir:p.directory,onNotice:d}),q;ue.reapStaleTunnel(p.directory,d);let Je=e.onHandover===void 0?void 0:t=>{let n=h.planSessionMigration(B.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))d(e);for(let e of n.migrate){let t=B.stop(e.id);t.ok||d(`could not stop ${e.name??e.id} before the move: ${t.error}`)}e.onHandover?.({settings:t,sessions:n.migrate})},J=ae.createRemoteAccess({store:p,launchTunnel:e.remoteAccess?.launchTunnel??ue.createTunnelLauncher({stateDir:p.directory,...e.cloudflaredPath===void 0?{}:{cloudflaredPath:e.cloudflaredPath},onNotice:d,onExit:e=>{d(`remote access: ${e}`),J.disable()}}),bindListener:async()=>await new Promise((e,t)=>{let n=(0,ge.serve)({fetch:U.fetch,port:0,hostname:`127.0.0.1`,serverOptions:{maxHeaderSize:16384,headersTimeout:5e3,requestTimeout:1e4}},t=>{W.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)}),bundleTrust:()=>K.trust(),onDeviceDropped:e=>q?.remove(e),onNotice:d,...Je===void 0?{}:{requestHandover:Je},contained:(0,Te.insideSandbox)(process.env),broadcastLocal:e=>Ke(e,!0),broadcastAll:e=>Ke(e,!1),...e.remoteAccess?.now===void 0?{}:{now:e.remoteAccess.now}});q=se.createLiveWebPush({stateDir:p.directory,isConnected:e=>J.isDeviceConnected(e),onNotice:d});let Ye=B.onEvent(e=>{e.kind===`frame`&&c.parseDoomNotificationEntry(e.frame)!==void 0&&q?.notify()}),Y=ie.createRemoteGuard({loopbackPort:()=>qe,tunnelPolicy:()=>J.tunnelPolicy(),authorize:e=>J.authorize((0,C.getCookie)(e,m.DEVICE_COOKIE,`host`)),trustedDevice:e=>e.env?.sealedDeviceId,channelReady:(e,t)=>J.channelFor(e,t)!==void 0,stepUp:{required:e=>J.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??J.authorize((0,C.getCookie)(e,`doompi_device`,`host`)),o=a===void 0?`local`:`device:${a}`;return await J.passkeys().finishStepUp(r,o,t,i)}},extraOrigins:ee.allowedOriginsFromEnv(process.env.DOOMPI_WEB_ALLOW_ORIGIN)});U.use(`*`,Y.middleware),U.use(`*`,async(e,t)=>{let n=new URL(e.req.url).pathname;if(n===m.REMOTE_HTTP_ROUTE)return t();let r=_.requestOperation(n);if(r===void 0)return t();let i=performance.now();return await f.runInSpan(r,{"operation.name":r,"http.method":e.req.method},async()=>{await t(),await f.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))})},_.readTraceContext(e.req.raw.headers))}),U.use(`*`,async(e,t)=>{if(Y.listenerOf(e)===`local`)return t();let n=Number(e.req.header(`content-length`));return Number.isFinite(n)&&n>Oe?e.json({error:`The tunnel request body is too large.`},413):t()}),le.registerRemoteRoutes(U,{remote:J,listenerOf:e=>Y.listenerOf(e)});let Xe=(0,w.bodyLimit)({maxSize:Oe,onError:e=>e.json({error:`The tunnel request body is too large.`},413)});U.use(`*`,async(e,t)=>Y.listenerOf(e)===`local`?t():await Xe(e,t));let Ze=(0,w.bodyLimit)({maxSize:32768,onError:e=>e.json({error:`The telemetry batch is too large.`},413)}),Qe=_.createBrowserTelemetryRateLimit();U.post(_.WEB_TELEMETRY_ROUTE,Ze,async e=>{if(!Qe())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=_.parseBrowserTelemetryBatch(t);if(n===void 0)return e.json({error:`The telemetry batch is invalid.`},400);for(let e of n){if(_.isBrowserErrorEvent(e)){await f.recordError(_.BROWSER_ERROR_EVENT,_.createBrowserError(e),{"browser.source":e.source,...e.session_id===void 0?{}:{"session.id":e.session_id},"failure.kind":`browser`},{includeException:!0});continue}await f.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)}),U.post(m.REMOTE_HTTP_ROUTE,async e=>{let t=J.authorize((0,C.getCookie)(e,m.DEVICE_COOKIE,`host`));if(t===void 0)return e.json({error:`This device is not paired.`},401);let n=J.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=ke(a),s=o===void 0?void 0:Ae(o,e),c=o===void 0?void 0:je(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 U.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 $e=(0,w.bodyLimit)({maxSize:8192,onError:e=>e.json({error:`The Push subscription is too large.`},413)}),et=e=>e.env?.sealedDeviceId;U.get(m.REMOTE_PUSH_KEY_ROUTE,e=>et(e)===void 0?e.json({error:`A sealed paired device is required.`},401):e.json({publicKey:q?.publicKey()},200,{"Cache-Control":`no-store`})),U.post(m.REMOTE_PUSH_ROUTE,$e,async e=>{let t=et(e);if(t===void 0)return e.json({error:`A sealed paired device is required.`},401);let n;try{n=await e.req.json()}catch{return e.json({error:`The Push subscription must be JSON.`},400)}return q?.subscribe(t,n)===!0?e.body(null,204):e.json({error:`The Push subscription is invalid.`},400)}),U.delete(m.REMOTE_PUSH_ROUTE,e=>{let t=et(e);return t===void 0?e.json({error:`A sealed paired device is required.`},401):(q?.remove(t),e.body(null,204))});let tt=r.createPiWebSocketListener({onError:e=>d(`protocol: ${e.message}`)}),nt=r.createPiWebSocketListener({onError:e=>d(`protocol: ${e.message}`)}),rt=t.createPiHubService({records:()=>B.records(),spawn:e=>B.create(e),onNotice:d}),it=new ye.Server(rt,{serverId:Se.DOOM_COCKPIT_SERVER_ID,listeners:[tt],onError:e=>d(`protocol: ${e.message}`)}),at=new ye.Server(rt,{serverId:Se.DOOM_COCKPIT_SERVER_ID,listeners:[nt],onError:e=>d(`protocol: ${e.message}`)});await Promise.all([it.start(),at.start()]);let X=re.createProviderAuth({runtime:e.authRuntime,onNotice:d});te.registerAuthRoutes(U,X);let ot=ze(B),st=()=>ot.list(),ct=e=>ot.resolve(e),lt=e=>{let t=ct(e);if(t===void 0)return;let n=(0,x.readSyncDrift)({repoRoot:t}),r;try{r=(0,x.readSyncRegistration)(t)?(0,xe.readSyncState)(t):void 0}catch{r=void 0}return{fresh:n.fresh,reasons:[...n.reasons],...r===void 0?{}:{mcpProjection:r.fileState.mcpProjection}}};ne.registerSettingsRoutes(U,{repositories:st,models:()=>X.listModels()});let ut=Ge.apiDirectory,dt=process.env[E.PACKAGE_API_DIR_ENV],ft=ut??(dt===void 0||dt===``?void 0:dt)??I(k)?.apiDirectory,pt=ft??`default`,Z=Be(U,ft===void 0?[]:await(0,E.loadPackageApis)(`hub`,{apiDirectory:ft,env:{},onNotice:d}),d,ct,lt,e=>{let t=B.snapshot().find(t=>t.id===e);if(t!==void 0)return L(T(t.cwd))?.apiDirectory},pt),Q=new Map,mt=e=>e===pt||L(k)?.apiDirectory===e||B.snapshot().some(t=>L(T(t.cwd))?.apiDirectory===e),ht=e=>{e!==void 0&&!mt(e)&&Z.remove(e)};j=async e=>{let t=Q.get(e),n=L(e);if(n===void 0){Q.delete(e),ht(t);return}let r=await(0,E.loadPackageApis)(`hub`,{apiDirectory:n.apiDirectory,env:{},onNotice:d});Z.add(r,n.apiDirectory),Q.set(e,n.apiDirectory),t!==n.apiDirectory&&ht(t)};let $=new Map(B.snapshot().map(e=>[e.id,T(e.cwd)])),gt=B.onEvent(e=>{if(e.kind===`upsert`){$.set(e.session.id,T(e.session.cwd));return}if(e.kind!==`removed`)return;let t=$.get(e.sessionId);if($.delete(e.sessionId),t===void 0||t===k||[...$.values()].includes(t))return;let n=Q.get(t);Q.delete(t),ht(n)});await Promise.all([...new Set([k,...We])].map(j)),Ve(U,B,d,f,e=>Y.callerOf(e)),U.get(`/api/health`,e=>e.json({ok:!0,role:`hub`,protocol:2,sessions:B.snapshot().length,pid:process.pid,version:Ne()})),U.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(!P(t)||typeof t.cwd!=`string`||t.cwd===``)return e.json({error:`A cwd string is required.`},400);let n;try{n=v.default.statSync(t.cwd)}catch{return e.json({error:`No such directory: ${t.cwd}`},400)}if(!y.default.isAbsolute(t.cwd)||!n.isDirectory())return e.json({error:`The working directory must be an absolute path to a directory, received "${t.cwd}".`},400);let r=typeof t.name==`string`&&t.name!==``?t.name:void 0;await R(t.cwd);let i=await B.create({cwd:t.cwd,name:r,trace:_.readTraceContext(e.req.raw.headers)});return i.ok?e.json({sessionId:i.sessionId},201):e.json({error:i.error},i.code===`invalid_request`?400:502)}),U.post(`${s.SESSIONS_API_ROUTE}/:sessionId/restart`,async e=>{let t=e.req.param(`sessionId`),n=B.snapshot().find(e=>e.id===t);n!==void 0&&await R(n.cwd);let r=await B.restart(t,_.readTraceContext(e.req.raw.headers));return r.ok?e.json({sessionId:r.sessionId},202):e.json({error:r.error},r.code===`invalid_request`?404:502)}),U.get(`${s.SESSIONS_API_ROUTE}/:sessionId/history`,async e=>{let t=e.req.param(`sessionId`),n=B.snapshot().find(e=>e.id===t);if(!n)return e.json({error:`Unknown session.`},404);let r=await ve.SessionManager.list(n.cwd);return e.json({sessions:r.map(e=>({id:e.id,...e.name===void 0?{}:{name:e.name},firstMessage:e.firstMessage,createdAt:e.created.toISOString(),updatedAt:e.modified.toISOString(),messageCount:e.messageCount}))})}),U.post(`${s.SESSIONS_API_ROUTE}/:sessionId/resume`,async e=>{let t=e.req.param(`sessionId`),n=B.snapshot().find(e=>e.id===t);if(!n)return e.json({error:`Unknown session.`},404);let r;try{r=await e.req.json()}catch{return e.json({error:`The request body must be JSON.`},400)}if(!P(r)||typeof r.targetSessionId!=`string`||r.targetSessionId===``)return e.json({error:`A targetSessionId string is required.`},400);let i=(await ve.SessionManager.list(n.cwd)).find(e=>e.id===r.targetSessionId);if(!i)return e.json({error:`That Pi thread does not belong to this workspace.`},404);await R(n.cwd);let a=await B.resume(t,{sessionId:i.id,name:Ie(i,n.cwd)},_.readTraceContext(e.req.raw.headers));return a.ok?e.json({sessionId:a.sessionId},202):e.json({error:a.error},a.code===`invalid_request`?409:502)}),U.delete(`${s.SESSIONS_API_ROUTE}/:sessionId`,e=>{let t=e.req.param(`sessionId`),n=B.stop(t);return n.ok?e.json({sessionId:t},202):e.json({error:n.error},n.code===`unknown`?404:n.code===`self`?409:502)}),U.get(`/api/sessions/:sessionId/files`,async e=>{let t=e.req.param(`sessionId`),n=B.snapshot().find(e=>e.id===t);if(!n)return e.json({error:`Unknown session.`},404);let r=e.req.query(`q`)??``,i=await g.listSessionFiles(n.cwd,r,20);return e.json({files:i})});let _t=t=>{if(!(Y.listenerOf(t)===`local`||(0,Te.insideSandbox)(process.env)))return e.browseRoot??process.cwd()};U.get(s.DIRECTORIES_API_ROUTE,async e=>{let t=_t(e),n=await de.suggestDirectories(e.req.query(`q`)??``,{limit:12,...t===void 0?{}:{root:t}});return e.json({directories:n})}),U.get(i.SESSION_FILE_ROUTE,async e=>{let t=e.req.param(`sessionId`),n=B.snapshot().find(e=>e.id===t);if(!n)return e.json({error:`Unknown session.`},404);let r=e.req.query(`path`)??``,o=await g.readSessionFile(n.cwd,r,i.MAX_SESSION_FILE_BYTES);if(o.status===`forbidden`)return e.json({error:`The path leaves the session directory.`},403);if(o.status===`not-found`)return e.json({error:`No such file.`},404);if(o.status===`too-large`)return e.json({error:`The file is too large to preview.`},413);let s={...a.sessionFileHeaders(r),[i.SESSION_FILE_SHA256_HEADER]:o.sha256,"Accept-Ranges":`bytes`},c=e.req.header(`range`);if(c===void 0)return e.body(new Uint8Array(o.body),200,s);let l=Fe(c,o.body.byteLength);if(l===void 0)return e.body(null,416,{...s,"Content-Range":`bytes */${String(o.body.byteLength)}`});let u=o.body.subarray(l.start,l.end+1);return e.body(new Uint8Array(u),206,{...s,"Content-Range":`bytes ${String(l.start)}-${String(l.end)}/${String(o.body.byteLength)}`,"Content-Length":String(u.byteLength)})});let vt=(0,w.bodyLimit)({maxSize:i.MAX_SESSION_FILE_BYTES,onError:e=>e.json({error:`The file is too large to save.`},413)});return U.put(i.SESSION_FILE_ROUTE,vt,async e=>{let t=e.req.param(`sessionId`),n=B.snapshot().find(e=>e.id===t);if(!n)return e.json({error:`Unknown session.`},404);if(B.planSaveState(t)!==`allowed`)return e.json({error:`Host saves are locked until Plan is authoritatively inactive.`},423);let r=e.req.header(i.SESSION_FILE_EXPECTED_SHA256_HEADER);if(r===void 0||!/^[a-f0-9]{64}$/.test(r))return e.json({error:`A lowercase SHA-256 ${i.SESSION_FILE_EXPECTED_SHA256_HEADER} header is required.`},400);let a=Buffer.from(await e.req.arrayBuffer()),o=await g.writeSessionFile(n.cwd,e.req.query(`path`)??``,a,r,i.MAX_SESSION_FILE_BYTES,()=>B.planSaveState(t)===`allowed`);return o.status===`ok`?e.body(null,204,{[i.SESSION_FILE_SHA256_HEADER]:o.sha256}):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 save.`},413):o.status===`locked`?e.json({error:`Host saves are locked until Plan is authoritatively inactive.`},423):e.json({error:`The file changed after it was read.`},409)}),U.get(m.SESSION_SOCKET_ROUTE,W.upgradeWebSocket(e=>{let t=Y.listenerOf(e)===`local`,n=t?void 0:J.authorize((0,C.getCookie)(e,m.DEVICE_COOKIE,`host`)),r=n===void 0?void 0:J.channelFor(n,`session`),i=new Set,a=(0,he.randomUUID)(),o=new Map,l,u,f,p,ee=e=>{for(let[t,n]of o)(e===void 0||n.sessionId===e)&&(o.delete(t),V.unsubscribe(n.sessionId,n.threadId))};return{onOpen(e,m){if(!t&&r===void 0){m.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){m.close(1008,`sealed session channel failed`);return}i=JSON.stringify(e.envelope)}try{m.send(i)}catch{}};f={post:h,local:t},H.add(f),n!==void 0&&(p=J.trackSocket(n,(e,t)=>m.close(e,t))),h(s.hubHello(B.channelTypes())),h({type:s.SESSIONS_SNAPSHOT_TYPE,sessions:B.snapshot()}),l=B.onEvent(e=>{e.kind===`upsert`?h({type:s.SESSION_UPSERT_TYPE,session:e.session}):e.kind===`removed`?(i.delete(e.sessionId),ee(e.sessionId),h({type:s.SESSION_REMOVED_TYPE,sessionId:e.sessionId})):e.kind===`channel`?(e.connectionId===a||e.connectionId===void 0&&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))}),u=V.onFrame(e=>{o.has(Le(e.sessionId,e.threadId))&&h(s.threadFrameEnvelope(e.sessionId,e.threadId,e.frame))}),d(`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(!P(n)||typeof n.sessionId!=`string`)return;let c=n.sessionId;if(typeof n.type==`string`&&B.channelTypes().includes(n.type)&&(i.has(c)||B.channelReceivesWithoutSubscription(c,n.type))){B.receiveChannel(c,n.type,n.payload,a);return}if(n.type===`subscribe`){let e=B.backlog(c);if(!e)return;i.add(c),f?.post(e);for(let e of B.channelFrames(c))f?.post(e);return}if(n.type===`unsubscribe`){i.delete(c);return}if(n.type===`history_request`){let e=B.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=Le(c,e);if(n.type===`unsubscribe_thread`){o.delete(t)&&V.unsubscribe(c,e);return}if(o.has(t)||B.backlog(c)===void 0)return;o.set(t,{sessionId:c,threadId:e}),f?.post(s.threadBacklog(c,e,V.subscribe(c,e)));return}if(n.type===`session_command`&&P(n.frame)){if(n.frame.type===`attach`)return;B.command(c,n.frame)}},onClose(){B.disconnectChannels(a),f&&H.delete(f),f=void 0,p?.(),p=void 0,l?.(),l=void 0,u?.(),u=void 0,i.clear(),ee(),d(`browser detached`)}}})),U.get(m.PROTOCOL_SOCKET_ROUTE,W.upgradeWebSocket(e=>{let t=Y.listenerOf(e)===`local`,n=t?void 0:J.authorize((0,C.getCookie)(e,m.DEVICE_COOKIE,`host`)),r=n===void 0?void 0:J.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=J.trackSocket(n,(e,t)=>o.close(e,t))),i=(t?tt:nt).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}}})),U.get(`/manifest.webmanifest`,e=>{let t=N(y.default.join(G,`manifest.webmanifest`));return t===void 0?e.text(`The PWA manifest is unavailable.`,404):e.body(new Uint8Array(t),200,{"Cache-Control":`no-store`,"Content-Type":`application/manifest+json; charset=utf-8`})}),U.get(`/sw.js`,e=>{let t=N(y.default.join(G,`sw.js`));return t===void 0?e.text(`The trusted service worker is unavailable.`,404):e.body(new Uint8Array(t),200,{"Cache-Control":`no-cache`,"Content-Type":`text/javascript; charset=utf-8`,"Service-Worker-Allowed":`/`})}),U.get(`/pwa/*`,e=>{let t=new URL(e.req.url).pathname.slice(5),n=o.resolveAssetPath(G,`/${t}`),r=n===void 0?void 0:N(n);return n===void 0||r===void 0?e.text(`PWA asset not found.`,404):e.body(new Uint8Array(r),200,{"Cache-Control":`no-store`,"Content-Type":o.contentTypeFor(n),"X-Content-Type-Options":`nosniff`})}),U.get(`${De}*`,e=>{let t=new URL(e.req.url).pathname,n=/^\/api\/web-plugins\/([a-f0-9]{64})\/([1-9][0-9]*)\/(manifest|assets(\/.*))$/u.exec(t),r=n===null?void 0:Number(n[2]),i=n===null||!Number.isSafeInteger(r)?void 0:z.get(n[1]??``,r);if(n===null||i===void 0)return e.text(`That signed plugin composition is unavailable.`,404);if(n[3]===`manifest`)return e.json(i.signed,200,{"Cache-Control":`no-store`});let a=(0,Ce.assetFor)(i.signed.manifest,n[4]??``),s=a===void 0?void 0:o.resolveAssetPath(i.assetsDir,a.path),c=s===void 0?void 0:N(s);return a===void 0||s===void 0||c===void 0?e.text(`Plugin composition asset not found.`,404):e.body(new Uint8Array(c),200,{"Cache-Control":`no-store`,"Content-Length":String(c.byteLength),"Content-Type":a.contentType,"X-Content-Type-Options":`nosniff`})}),U.get(Ce.BUNDLE_MANIFEST_ROUTE,e=>{let t=K.current();return t===void 0?e.json({error:`No signed cockpit bundle is available.`},503):e.json(t.signed,200,{"Cache-Control":`no-store`})}),U.get(`/bundle-assets/*`,e=>{let t=new URL(e.req.url).pathname,n=/^\/bundle-assets\/([1-9][0-9]*)(\/.*)$/u.exec(t),r=n===null?void 0:Number(n[1]),i=K.current();if(n===null||!Number.isSafeInteger(r)||i===void 0||i.signed.manifest.revision!==r)return e.text(`That signed bundle revision is unavailable.`,404);let a=(0,Ce.assetFor)(i.signed.manifest,n[2]??``),s=a===void 0?void 0:o.resolveAssetPath(i.assetsDir,a.path),c=s===void 0?void 0:N(s);return a===void 0||s===void 0||c===void 0?e.text(`Bundle asset not found.`,404):e.body(new Uint8Array(c),200,{"Cache-Control":`no-store`,"Content-Length":String(c.byteLength),"Content-Type":a.contentType,"X-Content-Type-Options":`nosniff`})}),U.get(`/`,e=>e.redirect(m.PAIRING_PAGE_ROUTE)),U.get(`*`,e=>{let t=new URL(e.req.url).pathname,n=e.req.header(`accept`)?.includes(`text/html`)===!0;return!t.startsWith(`/api/`)&&n?e.redirect(`/`):e.text(`Not found.`,404)}),A.watch(()=>{for(let e of B.records())T(e.cwd)===k&&B.reloadChannels(e.id)}),new Promise((t,n)=>{let r=(0,ge.serve)({fetch:U.fetch,port:e.port,hostname:l},e=>{qe=e.port,W.injectWebSocket(r);let n=`http://${l}:${e.port}`;d(`cockpit on ${n}`);let i,a=async()=>{await J.close(),await new Promise(e=>{V.close(),A.close(),it.close(),at.close(),gt(),Ye(),q?.close(),B.close(),X.close();for(let e of Z.handlers)e.close();z.close();for(let e of W.wss.clients)e.terminate();W.wss.close(),r.closeAllConnections?.(),r.close(()=>e())}),await _.shutdownWebTelemetry(f)};t({url:n,port:e.port,close:()=>i??=a()})});r.once(`error`,e=>{A.close(),V.close(),gt(),Ye(),q?.close(),B.close(),X.close(),J.close();for(let e of Z.handlers)e.close();z.close(),f.shutdown(),n(e)})})}exports.createSettingsRepositoryRegistry=ze,exports.mountHubApis=Be,exports.packagedVersion=Ne,exports.serveWeb=L;
3
3
  //# sourceMappingURL=httpServer.cjs.map