@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 +26 -57
- package/dist/src/adapters/httpServer.cjs +2 -2
- package/dist/src/adapters/httpServer.cjs.map +1 -1
- package/dist/src/adapters/httpServer.d.cts.map +1 -1
- package/dist/src/adapters/httpServer.d.mts.map +1 -1
- package/dist/src/adapters/httpServer.mjs +2 -2
- package/dist/src/adapters/httpServer.mjs.map +1 -1
- package/dist/src/adapters/sessionWebComposition.cjs +1 -1
- package/dist/src/adapters/sessionWebComposition.cjs.map +1 -1
- package/dist/src/adapters/sessionWebComposition.mjs +1 -1
- package/dist/src/adapters/sessionWebComposition.mjs.map +1 -1
- package/package.json +7 -7
- package/src/adapters/httpServer.ts +129 -24
- package/src/adapters/sessionWebComposition.ts +38 -23
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
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
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"),
|
|
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
|