@agimon-ai/doompi-web 0.0.1-alpha.6 → 0.0.1-alpha.9
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 +19 -15
- package/dist/bin/serve.cjs +1 -1
- package/dist/bin/serve.cjs.map +1 -1
- package/dist/bin/serve.mjs +1 -1
- package/dist/bin/serve.mjs.map +1 -1
- package/dist/pwa/sw.js +1 -1
- package/dist/src/adapters/doomInitialization.cjs +2 -0
- package/dist/src/adapters/doomInitialization.cjs.map +1 -0
- package/dist/src/adapters/doomInitialization.mjs +2 -0
- package/dist/src/adapters/doomInitialization.mjs.map +1 -0
- 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/hubProbe.cjs +1 -1
- package/dist/src/adapters/hubProbe.cjs.map +1 -1
- package/dist/src/adapters/hubProbe.mjs +1 -1
- package/dist/src/adapters/hubProbe.mjs.map +1 -1
- package/dist/src/adapters/registryWatcher.cjs +1 -1
- package/dist/src/adapters/registryWatcher.cjs.map +1 -1
- package/dist/src/adapters/registryWatcher.mjs +1 -1
- package/dist/src/adapters/registryWatcher.mjs.map +1 -1
- package/dist/src/adapters/syncGuard.cjs +1 -1
- package/dist/src/adapters/syncGuard.cjs.map +1 -1
- package/dist/src/adapters/syncGuard.mjs +1 -1
- package/dist/src/adapters/syncGuard.mjs.map +1 -1
- package/dist/src/adapters/webBundler.cjs +1 -1
- package/dist/src/adapters/webBundler.cjs.map +1 -1
- package/dist/src/adapters/webBundler.d.cts.map +1 -1
- package/dist/src/adapters/webBundler.d.mts.map +1 -1
- package/dist/src/adapters/webBundler.mjs +1 -1
- package/dist/src/adapters/webBundler.mjs.map +1 -1
- package/dist/src/adapters/webComposition.cjs +3 -0
- package/dist/src/adapters/webComposition.cjs.map +1 -0
- package/dist/src/adapters/webComposition.mjs +3 -0
- package/dist/src/adapters/webComposition.mjs.map +1 -0
- package/dist/src/adapters/webPluginGenerate.cjs +2 -2
- package/dist/src/adapters/webPluginGenerate.cjs.map +1 -1
- package/dist/src/adapters/webPluginGenerate.mjs +2 -2
- package/dist/src/adapters/webPluginGenerate.mjs.map +1 -1
- package/dist/src/adapters/webPluginScan.cjs +1 -1
- package/dist/src/adapters/webPluginScan.cjs.map +1 -1
- package/dist/src/adapters/webPluginScan.mjs +1 -1
- package/dist/src/adapters/webPluginScan.mjs.map +1 -1
- package/dist/src/services/pairingPage.cjs +25 -1
- package/dist/src/services/pairingPage.cjs.map +1 -1
- package/dist/src/services/pairingPage.mjs +25 -1
- package/dist/src/services/pairingPage.mjs.map +1 -1
- package/dist/src/services/serveOptions.cjs +14 -1
- package/dist/src/services/serveOptions.cjs.map +1 -1
- package/dist/src/services/serveOptions.d.cts +4 -0
- package/dist/src/services/serveOptions.d.cts.map +1 -1
- package/dist/src/services/serveOptions.d.mts +4 -0
- package/dist/src/services/serveOptions.d.mts.map +1 -1
- package/dist/src/services/serveOptions.mjs +14 -1
- package/dist/src/services/serveOptions.mjs.map +1 -1
- package/dist/src/types/bridge.d.cts +2 -0
- package/dist/src/types/bridge.d.cts.map +1 -1
- package/dist/src/types/bridge.d.mts +2 -0
- package/dist/src/types/bridge.d.mts.map +1 -1
- package/dist/src/types/hub.cjs.map +1 -1
- package/dist/src/types/hub.mjs.map +1 -1
- package/dist/web/assets/{CodeEditorView-CJoceEgU.js → CodeEditorView-DZbzJOSd.js} +14 -14
- package/dist/web/assets/CodeEditorView-DZbzJOSd.js.map +1 -0
- package/dist/web/assets/{dist--0dFFjHK.js → dist-5ps7JMLd.js} +2 -2
- package/dist/web/assets/{dist--0dFFjHK.js.map → dist-5ps7JMLd.js.map} +1 -1
- package/dist/web/assets/{dist-Cr0kQueR.js → dist-BB1d6xKn.js} +2 -2
- package/dist/web/assets/{dist-Cr0kQueR.js.map → dist-BB1d6xKn.js.map} +1 -1
- package/dist/web/assets/{dist-Dnjnn2hl.js → dist-BNoSQdvj.js} +1 -1
- package/dist/web/assets/{dist-DsQNQF8G.js → dist-BxAAIyyl.js} +2 -2
- package/dist/web/assets/{dist-DsQNQF8G.js.map → dist-BxAAIyyl.js.map} +1 -1
- package/dist/web/assets/{dist-Dh1ufhC2.js → dist-CM1beAoz.js} +2 -2
- package/dist/web/assets/{dist-Dh1ufhC2.js.map → dist-CM1beAoz.js.map} +1 -1
- package/dist/web/assets/{dist-a_TARmvK.js → dist-CSe5TmKd.js} +2 -2
- package/dist/web/assets/{dist-a_TARmvK.js.map → dist-CSe5TmKd.js.map} +1 -1
- package/dist/web/assets/{dist-Cc1jhITG.js → dist-CYO6Wo80.js} +2 -2
- package/dist/web/assets/{dist-Cc1jhITG.js.map → dist-CYO6Wo80.js.map} +1 -1
- package/dist/web/assets/{dist-BEjTM7TM.js → dist-DWW6jM08.js} +2 -2
- package/dist/web/assets/{dist-BEjTM7TM.js.map → dist-DWW6jM08.js.map} +1 -1
- package/dist/web/assets/{dist-h0LM1rYV.js → dist-N1ngjnyh.js} +2 -2
- package/dist/web/assets/{dist-h0LM1rYV.js.map → dist-N1ngjnyh.js.map} +1 -1
- package/dist/web/assets/{dist-KD1Mlgxp.js → dist-jdG0WRgB.js} +2 -2
- package/dist/web/assets/{dist-KD1Mlgxp.js.map → dist-jdG0WRgB.js.map} +1 -1
- package/dist/web/assets/index-CTk349FE.css +2 -0
- package/dist/web/assets/index-CzkINgmp.js +101 -0
- package/dist/web/assets/index-CzkINgmp.js.map +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +10 -10
- package/src/adapters/doomInitialization.ts +49 -0
- package/src/adapters/httpServer.ts +111 -56
- package/src/adapters/hubProbe.ts +34 -5
- package/src/adapters/registryWatcher.ts +33 -30
- package/src/adapters/syncGuard.ts +10 -0
- package/src/adapters/webBundler.ts +3 -0
- package/src/adapters/webComposition.ts +240 -0
- package/src/adapters/webPluginGenerate.ts +16 -1
- package/src/adapters/webPluginScan.ts +6 -2
- package/src/bin/serve.ts +64 -8
- package/src/pwa/serviceWorker.ts +53 -0
- package/src/services/pairingPage.ts +25 -1
- package/src/services/serveOptions.ts +45 -2
- package/src/types/bridge.ts +2 -0
- package/src/types/bundle.ts +28 -0
- package/src/types/hub.ts +10 -0
- package/src/web/app/sessionRuntime.ts +41 -2
- package/src/web/features/dialogs/DialogOverlay.tsx +9 -3
- package/src/web/features/leader/CommandPalette.tsx +7 -3
- package/src/web/features/remote/RemoteAccessOptions.tsx +12 -6
- package/src/web/features/remote/RemoteBanner.tsx +2 -3
- package/src/web/features/session/Composer.tsx +10 -3
- package/src/web/features/session/MentionPreviews.tsx +6 -3
- package/src/web/features/session/Timeline.tsx +13 -11
- package/src/web/features/session/ToolCard.tsx +8 -2
- package/src/web/features/sessions/NewSessionDialog.tsx +14 -9
- package/src/web/features/sessions/SessionRail.tsx +42 -18
- package/src/web/features/settings/ContributedSettings.tsx +8 -3
- package/src/web/features/settings/LoginFlowDialog.tsx +7 -4
- package/src/web/features/settings/ProviderSettings.tsx +1 -0
- package/src/web/features/settings/RepositorySettings.tsx +10 -1
- package/src/web/features/settings/RepositoryWorkspace.tsx +1 -0
- package/src/web/features/settings/TunnelSettings.tsx +16 -6
- package/src/web/lib/composition.ts +1 -0
- package/src/web/lib/sessionModel.ts +22 -2
- package/src/web/routes/CockpitPage.tsx +13 -6
- package/src/web/stores/composerStore.ts +49 -0
- package/dist/web/assets/CodeEditorView-CJoceEgU.js.map +0 -1
- package/dist/web/assets/index-Dg79RfHx.css +0 -2
- package/dist/web/assets/index-Dur2Nvah.js +0 -101
- package/dist/web/assets/index-Dur2Nvah.js.map +0 -1
package/README.md
CHANGED
|
@@ -26,27 +26,31 @@ npm install -g @agimon-ai/doompi-web
|
|
|
26
26
|
doompi-web
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
Open `http://127.0.0.1:7433`. With no flags, the hub watches `~/.doompi/run`, attaches to registered sessions, and
|
|
29
|
+
Open `http://127.0.0.1:7433`. With no flags, the hub watches `~/.doompi/run`, attaches to registered sessions, and resolves the union of their synchronized cockpit compositions from `~/.pi/.doom/sync/registrations`. The directory that launches `doompi-web` does not select the bundle. Sessions started by the cockpit use the server and agent from the same installation.
|
|
30
30
|
|
|
31
|
-
If a hub already answers on the port, a second `doompi-web` process prints its URL and exits.
|
|
31
|
+
If `~/.pi/.doom` does not exist, the command runs its bundled `doompi init` before starting. If a hub already answers on the port, a second `doompi-web` process prints its URL and exits.
|
|
32
32
|
|
|
33
|
-
To run this repository's build:
|
|
33
|
+
To run this repository's build with its composition pinned and watched:
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
|
-
pnpm cockpit
|
|
36
|
+
pnpm cockpit:build
|
|
37
|
+
pnpm doompi-web --dir=<PWD>
|
|
37
38
|
```
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
| Flag | Default
|
|
42
|
-
| ----------------- |
|
|
43
|
-
| `--
|
|
44
|
-
| `--
|
|
45
|
-
| `--
|
|
46
|
-
| `--
|
|
47
|
-
| `--
|
|
48
|
-
| `--
|
|
49
|
-
| `--
|
|
40
|
+
`--dir` accepts `--dir <path>` too. It synchronizes that repository before launch and watches it for development rebuilds.
|
|
41
|
+
|
|
42
|
+
| Flag | Default | Meaning |
|
|
43
|
+
| ----------------- | ------------------------ | ------------------------------------------------ |
|
|
44
|
+
| `--dir` | live-session union | Pin, sync, and watch one repository composition |
|
|
45
|
+
| `--registry-dir` | `~/.doompi/run` | Registry to watch (also `DOOMPI_RUNTIME_DIR`) |
|
|
46
|
+
| `--spawn-command` | this install | Command launching sessions created from the page |
|
|
47
|
+
| `--port` | `7433` | HTTP port |
|
|
48
|
+
| `--host` | `127.0.0.1` | Bind address |
|
|
49
|
+
| `--assets` | synchronized or packaged | Override the built SPA directory |
|
|
50
|
+
| `--state-dir` | `~/.doompi/web` | Remote-access settings and cockpit cache |
|
|
51
|
+
| `--cloudflared` | from `PATH` | Tunnel binary (also `DOOMPI_CLOUDFLARED`) |
|
|
52
|
+
| `-h`, `--help` | | Print command help |
|
|
53
|
+
| `-v`, `--version` | | Print the installed package version |
|
|
50
54
|
|
|
51
55
|
Install [doompi-server](https://www.npmjs.com/package/@agimon-ai/doompi-server) when you also want to run a session server directly. The hub does not require `doompi-server` on `PATH` to create sessions.
|
|
52
56
|
|
package/dist/bin/serve.cjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../src/services/registryStore.cjs"),n=require("../src/adapters/remoteAccessStore.cjs"),r=require("../src/adapters/httpServer.cjs"),i=require("../src/services/serveOptions.cjs"),a=require("../src/adapters/cockpitContainer.cjs"),o=require("../src/adapters/cockpitHandoff.cjs"),s=require("../src/adapters/
|
|
2
|
+
const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../src/services/registryStore.cjs"),n=require("../src/adapters/remoteAccessStore.cjs"),r=require("../src/adapters/httpServer.cjs"),i=require("../src/services/serveOptions.cjs"),a=require("../src/adapters/cockpitContainer.cjs"),o=require("../src/adapters/cockpitHandoff.cjs"),s=require("../src/adapters/doomInitialization.cjs"),c=require("../src/adapters/hubProbe.cjs"),l=require("../src/adapters/sessionRelaunch.cjs");let u=require("node:os");u=e.__toESM(u,1);function d(e){process.stderr.write(`[doompi-web] ${e}\n`)}async function f(e,t,n){try{process.kill(e,`SIGTERM`)}catch{}let r=Date.now()+1e4;for(;Date.now()<r;)if(await new Promise(e=>setTimeout(e,100)),!await c.hubAnswers(t,n))return!0;return!1}async function p(){let e=i.parseServeOptions(process.argv.slice(2));if(e.help){process.stdout.write(i.serveHelp());return}if(e.version){process.stdout.write(`${r.packagedVersion()}\n`);return}await s.ensureDoomInitialized({homeDirectory:u.default.homedir(),onNotice:d});let o=`http://${e.host}:${String(e.port)}`,l=e.stateDir??n.defaultRemoteStateDir(),p=a.createCockpitContainer({stateDir:l,onNotice:d});await p.reapStale(),i.isLoopbackHost(e.host)||(d(`WARNING: --host ${e.host} is not loopback, so the cockpit is reachable without pairing.`),d(`WARNING: turn on remote access in settings instead, which requires a paired device.`));let h=await c.probeHub(e.host,e.port);if(h!==void 0){let t=r.packagedVersion(),n=h.version??`an older build`;if(h.version===t){d(`cockpit already running at ${o}`);return}if(h.sessions>0){d(`WARNING: ${n} is already running at ${o} with ${String(h.sessions)} live session(s).`),d(`WARNING: stop it and run again to serve ${t}; refusing to interrupt running work.`);return}if(h.pid===void 0||!i.isLoopbackHost(e.host)){d(`WARNING: ${n} is already running at ${o} and cannot be replaced automatically.`),d(`WARNING: stop it and run again to serve ${t}.`);return}if(d(`replacing ${n} at ${o} with ${t}`),!await f(h.pid,e.host,e.port)){d(`WARNING: the hub at ${o} did not stop; serve ${t} on another port with --port.`);return}}let g=async()=>{},_=async()=>{let n=await r.serveWeb({port:e.port,host:e.host,assetsDir:e.assetsDir,compositionDir:e.directory,registryDir:t.resolveRegistryDir({flagValue:e.registryDir,envValue:process.env[t.REGISTRY_DIR_ENV],homeDir:u.default.homedir()}),spawnCommand:e.spawnCommand,remoteStateDir:l,cloudflaredPath:e.cloudflaredPath,onNotice:d,onHandover:t=>{m({serving:n,restart:_,container:p,handover:t,port:e.port,host:e.host,adopt:e=>{g=e}})}});return g=async()=>{await n.close()},n};try{await _()}catch(t){if(t.code!==`EADDRINUSE`)throw t;if(await c.hubAnswers(e.host,e.port)){d(`cockpit already running at ${o}`);return}throw Error(`Port ${String(e.port)} is taken by something that is not a DoomPi cockpit; pass --port.`)}let v=()=>{g().then(()=>{process.exit(0)})};process.on(`SIGINT`,v),process.on(`SIGTERM`,v)}async function m(e){await e.serving.close();let t=await e.container.start({workspaces:e.handover.settings.sandbox.workspaces.map(e=>({path:e})),port:e.port,onProgress:d});if(!t.ok){d(`the cockpit could not move into a container: ${t.error}`),d(`serving from the host again`),await e.restart(),await l.relaunchSessions({port:e.port,host:e.host,sessions:e.handover.sessions,onNotice:d});return}e.adopt(async()=>{await t.container.stop()}),await o.handOffRemoteAccess({port:e.port,settings:e.handover.settings,onNotice:d}),await l.relaunchSessions({port:e.port,sessions:e.handover.sessions,onNotice:d}),d(`supervising the cockpit container; Ctrl-C stops it`),await t.container.supervise()}p().catch(e=>{d(e instanceof Error?e.message:String(e)),process.exitCode=1});
|
|
3
3
|
//# sourceMappingURL=serve.cjs.map
|
package/dist/bin/serve.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.cjs","names":["parseServeOptions","defaultRemoteStateDir","createCockpitContainer","isLoopbackHost","hubAnswers","serveWeb","resolveRegistryDir","REGISTRY_DIR_ENV","os","relaunchSessions","handOffRemoteAccess"],"sources":["../../src/bin/serve.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport os from 'node:os';\nimport { createCockpitContainer } from '../adapters/cockpitContainer.ts';\nimport { handOffRemoteAccess } from '../adapters/cockpitHandoff.ts';\nimport { hubAnswers } from '../adapters/hubProbe.ts';\nimport { serveWeb } from '../adapters/httpServer.ts';\nimport { defaultRemoteStateDir } from '../adapters/remoteAccessStore.ts';\nimport { relaunchSessions } from '../adapters/sessionRelaunch.ts';\nimport { REGISTRY_DIR_ENV, resolveRegistryDir } from '../services/registryStore.ts';\nimport { isLoopbackHost, parseServeOptions } from '../services/serveOptions.ts';\nimport type { WebServer } from '../types/bridge.ts';\nimport type { MigratingSession, RemoteAccessSettings } from '../types/remoteAccess.ts';\n\nconst ADDRESS_IN_USE = 'EADDRINUSE';\n\nfunction notice(message: string): void {\n process.stderr.write(`[doompi-web] ${message}\\n`);\n}\n\nasync function main(): Promise<void> {\n const options = parseServeOptions(process.argv.slice(2));\n const url = `http://${options.host}:${String(options.port)}`;\n const stateDir = options.stateDir ?? defaultRemoteStateDir();\n const container = createCockpitContainer({ stateDir, onNotice: notice });\n // A cockpit that was killed rather than closed leaves its container holding\n // the published port, so the next start would fail to bind for a reason\n // nothing explains.\n await container.reapStale();\n\n // Binding off loopback publishes an unauthenticated cockpit to whoever can\n // reach the address. Remote access exists so nobody has to do this; a warning\n // rather than a refusal, because an existing setup should not break today.\n if (!isLoopbackHost(options.host)) {\n notice(`WARNING: --host ${options.host} is not loopback, so the cockpit is reachable without pairing.`);\n notice('WARNING: turn on remote access in settings instead, which requires a paired device.');\n }\n\n // One hub serves every session, so a second start is a request to use the\n // running one, not an error worth a stack trace.\n if (await hubAnswers(options.host, options.port)) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n\n /**\n * What Ctrl-C stops, which is not the same thing throughout the run.\n *\n * A single indirection rather than swapping signal handlers, so the handover\n * never has to reach for `removeAllListeners` and take out whatever else this\n * process has registered.\n */\n let shutDown: () => Promise<void> = async () => {};\n\n const start = async (): Promise<WebServer> => {\n const server = await serveWeb({\n port: options.port,\n host: options.host,\n assetsDir: options.assetsDir,\n registryDir: resolveRegistryDir({\n flagValue: options.registryDir,\n envValue: process.env[REGISTRY_DIR_ENV],\n homeDir: os.homedir(),\n }),\n spawnCommand: options.spawnCommand,\n remoteStateDir: stateDir,\n cloudflaredPath: options.cloudflaredPath,\n onNotice: notice,\n // Already deferred past its own response by the route that asked, so by\n // the time this runs the caller has its answer and the server can go.\n onHandover: (handover) => {\n void handOver({\n serving: server,\n restart: start,\n container,\n handover,\n port: options.port,\n host: options.host,\n adopt: (next) => {\n shutDown = next;\n },\n });\n },\n });\n shutDown = async () => {\n await server.close();\n };\n return server;\n };\n\n try {\n await start();\n } catch (error) {\n // Two cockpits starting together can both find the port free; the loser\n // settles for the winner rather than reporting a clash nobody caused.\n if ((error as NodeJS.ErrnoException).code !== ADDRESS_IN_USE) throw error;\n if (await hubAnswers(options.host, options.port)) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n throw new Error(`Port ${String(options.port)} is taken by something that is not a DoomPi cockpit; pass --port.`);\n }\n\n const stop = (): void => {\n void shutDown().then(() => {\n process.exit(0);\n });\n };\n process.on('SIGINT', stop);\n process.on('SIGTERM', stop);\n}\n\n/**\n * Hands this cockpit over to one running in a container.\n *\n * The port has to be free before the container can publish onto it, so the\n * order is: stop serving, start the container, and put the host cockpit back if\n * the container does not come up. Leaving the user with nothing listening would\n * be the worst outcome of the three, which is why the rollback also recreates\n * the sessions that were stopped in preparation for a move that did not happen.\n */\nasync function handOver(input: {\n serving: WebServer;\n restart: () => Promise<WebServer>;\n container: ReturnType<typeof createCockpitContainer>;\n handover: { settings: RemoteAccessSettings; sessions: readonly MigratingSession[] };\n port: number;\n host: string;\n adopt: (shutDown: () => Promise<void>) => void;\n}): Promise<void> {\n await input.serving.close();\n const started = await input.container.start({\n workspaces: input.handover.settings.sandbox.workspaces.map((workspace) => ({ path: workspace })),\n port: input.port,\n onProgress: notice,\n });\n if (!started.ok) {\n notice(`the cockpit could not move into a container: ${started.error}`);\n notice('serving from the host again');\n await input.restart();\n await relaunchSessions({\n port: input.port,\n host: input.host,\n sessions: input.handover.sessions,\n onNotice: notice,\n });\n return;\n }\n input.adopt(async () => {\n await started.container.stop();\n });\n // The container answers its own health probe before `start` resolves, so its\n // hub is already there to take the settings and the sessions.\n await handOffRemoteAccess({ port: input.port, settings: input.handover.settings, onNotice: notice });\n await relaunchSessions({ port: input.port, sessions: input.handover.sessions, onNotice: notice });\n // Nothing else holds this process open now that its server is closed, and a\n // supervisor that exits leaves a container nobody will ever stop.\n notice('supervising the cockpit container; Ctrl-C stops it');\n await started.container.supervise();\n}\n\nmain().catch((error: unknown) => {\n notice(error instanceof Error ? error.message : String(error));\n process.exitCode = 1;\n});\n"],"mappings":";wdAgBA,SAAS,EAAO,EAAuB,CACrC,QAAQ,OAAO,MAAM,gBAAgB,EAAQ,GAAG,CAClD,CAEA,eAAe,GAAsB,CACnC,IAAM,EAAUA,EAAAA,kBAAkB,QAAQ,KAAK,MAAM,CAAC,CAAC,EACjD,EAAM,UAAU,EAAQ,KAAK,GAAG,OAAO,EAAQ,IAAI,IACnD,EAAW,EAAQ,UAAYC,EAAAA,sBAAsB,EACrD,EAAYC,EAAAA,uBAAuB,CAAE,WAAU,SAAU,CAAO,CAAC,EAgBvE,GAZA,MAAM,EAAU,UAAU,EAKrBC,EAAAA,eAAe,EAAQ,IAAI,IAC9B,EAAO,mBAAmB,EAAQ,KAAK,+DAA+D,EACtG,EAAO,qFAAqF,GAK1F,MAAMC,EAAAA,WAAW,EAAQ,KAAM,EAAQ,IAAI,EAAG,CAChD,EAAO,8BAA8B,GAAK,EAC1C,MACF,CASA,IAAI,EAAgC,SAAY,CAAC,EAE3C,EAAQ,SAAgC,CAC5C,IAAM,EAAS,MAAMC,EAAAA,SAAS,CAC5B,KAAM,EAAQ,KACd,KAAM,EAAQ,KACd,UAAW,EAAQ,UACnB,YAAaC,EAAAA,mBAAmB,CAC9B,UAAW,EAAQ,YACnB,SAAU,QAAQ,IAAIC,EAAAA,kBACtB,QAASC,EAAAA,QAAG,QAAQ,CACtB,CAAC,EACD,aAAc,EAAQ,aACtB,eAAgB,EAChB,gBAAiB,EAAQ,gBACzB,SAAU,EAGV,WAAa,GAAa,CACxB,EAAc,CACZ,QAAS,EACT,QAAS,EACT,YACA,WACA,KAAM,EAAQ,KACd,KAAM,EAAQ,KACd,MAAQ,GAAS,CACf,EAAW,CACb,CACF,CAAC,CACH,CACF,CAAC,EAID,MAHA,GAAW,SAAY,CACrB,MAAM,EAAO,MAAM,CACrB,EACO,CACT,EAEA,GAAI,CACF,MAAM,EAAM,CACd,OAAS,EAAO,CAGd,GAAK,EAAgC,OAAS,aAAgB,MAAM,EACpE,GAAI,MAAMJ,EAAAA,WAAW,EAAQ,KAAM,EAAQ,IAAI,EAAG,CAChD,EAAO,8BAA8B,GAAK,EAC1C,MACF,CACA,MAAU,MAAM,QAAQ,OAAO,EAAQ,IAAI,EAAE,kEAAkE,CACjH,CAEA,IAAM,MAAmB,CACvB,EAAc,CAAC,CAAC,SAAW,CACzB,QAAQ,KAAK,CAAC,CAChB,CAAC,CACH,EACA,QAAQ,GAAG,SAAU,CAAI,EACzB,QAAQ,GAAG,UAAW,CAAI,CAC5B,CAWA,eAAe,EAAS,EAQN,CAChB,MAAM,EAAM,QAAQ,MAAM,EAC1B,IAAM,EAAU,MAAM,EAAM,UAAU,MAAM,CAC1C,WAAY,EAAM,SAAS,SAAS,QAAQ,WAAW,IAAK,IAAe,CAAE,KAAM,CAAU,EAAE,EAC/F,KAAM,EAAM,KACZ,WAAY,CACd,CAAC,EACD,GAAI,CAAC,EAAQ,GAAI,CACf,EAAO,gDAAgD,EAAQ,OAAO,EACtE,EAAO,6BAA6B,EACpC,MAAM,EAAM,QAAQ,EACpB,MAAMK,EAAAA,iBAAiB,CACrB,KAAM,EAAM,KACZ,KAAM,EAAM,KACZ,SAAU,EAAM,SAAS,SACzB,SAAU,CACZ,CAAC,EACD,MACF,CACA,EAAM,MAAM,SAAY,CACtB,MAAM,EAAQ,UAAU,KAAK,CAC/B,CAAC,EAGD,MAAMC,EAAAA,oBAAoB,CAAE,KAAM,EAAM,KAAM,SAAU,EAAM,SAAS,SAAU,SAAU,CAAO,CAAC,EACnG,MAAMD,EAAAA,iBAAiB,CAAE,KAAM,EAAM,KAAM,SAAU,EAAM,SAAS,SAAU,SAAU,CAAO,CAAC,EAGhG,EAAO,oDAAoD,EAC3D,MAAM,EAAQ,UAAU,UAAU,CACpC,CAEA,EAAK,CAAC,CAAC,MAAO,GAAmB,CAC/B,EAAO,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,CAAC,EAC7D,QAAQ,SAAW,CACrB,CAAC"}
|
|
1
|
+
{"version":3,"file":"serve.cjs","names":["hubAnswers","parseServeOptions","serveHelp","packagedVersion","ensureDoomInitialized","os","defaultRemoteStateDir","createCockpitContainer","isLoopbackHost","probeHub","serveWeb","resolveRegistryDir","REGISTRY_DIR_ENV","relaunchSessions","handOffRemoteAccess"],"sources":["../../src/bin/serve.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport os from 'node:os';\nimport { createCockpitContainer } from '../adapters/cockpitContainer.ts';\nimport { handOffRemoteAccess } from '../adapters/cockpitHandoff.ts';\nimport { ensureDoomInitialized } from '../adapters/doomInitialization.ts';\nimport { hubAnswers, probeHub } from '../adapters/hubProbe.ts';\nimport { packagedVersion, serveWeb } from '../adapters/httpServer.ts';\nimport { defaultRemoteStateDir } from '../adapters/remoteAccessStore.ts';\nimport { relaunchSessions } from '../adapters/sessionRelaunch.ts';\nimport { REGISTRY_DIR_ENV, resolveRegistryDir } from '../services/registryStore.ts';\nimport { isLoopbackHost, parseServeOptions, serveHelp } from '../services/serveOptions.ts';\nimport type { WebServer } from '../types/bridge.ts';\nimport type { MigratingSession, RemoteAccessSettings } from '../types/remoteAccess.ts';\n\nconst ADDRESS_IN_USE = 'EADDRINUSE';\nconst STOP_POLL_MS = 100;\nconst STOP_TIMEOUT_MS = 10_000;\n\nfunction notice(message: string): void {\n process.stderr.write(`[doompi-web] ${message}\\n`);\n}\n\n/**\n * Signals the running hub and waits for the address to go quiet.\n *\n * Only ever called for a loopback address, because the pid on the other end of\n * the probe is only this machine's pid when the hub is on this machine.\n */\nasync function stopRunningHub(pid: number, host: string, port: number): Promise<boolean> {\n try {\n process.kill(pid, 'SIGTERM');\n } catch {\n // Already gone, or not ours to signal. The probe below settles it either way.\n }\n const deadline = Date.now() + STOP_TIMEOUT_MS;\n while (Date.now() < deadline) {\n await new Promise((resolve) => setTimeout(resolve, STOP_POLL_MS));\n if (!(await hubAnswers(host, port))) return true;\n }\n return false;\n}\n\nasync function main(): Promise<void> {\n const options = parseServeOptions(process.argv.slice(2));\n if (options.help) {\n process.stdout.write(serveHelp());\n return;\n }\n if (options.version) {\n process.stdout.write(`${packagedVersion()}\\n`);\n return;\n }\n await ensureDoomInitialized({ homeDirectory: os.homedir(), onNotice: notice });\n const url = `http://${options.host}:${String(options.port)}`;\n const stateDir = options.stateDir ?? defaultRemoteStateDir();\n const container = createCockpitContainer({ stateDir, onNotice: notice });\n // A cockpit that was killed rather than closed leaves its container holding\n // the published port, so the next start would fail to bind for a reason\n // nothing explains.\n await container.reapStale();\n\n // Binding off loopback publishes an unauthenticated cockpit to whoever can\n // reach the address. Remote access exists so nobody has to do this; a warning\n // rather than a refusal, because an existing setup should not break today.\n if (!isLoopbackHost(options.host)) {\n notice(`WARNING: --host ${options.host} is not loopback, so the cockpit is reachable without pairing.`);\n notice('WARNING: turn on remote access in settings instead, which requires a paired device.');\n }\n\n // One hub serves every session, so a second start is normally a request to\n // use the running one. A second start from a *different* version is not: a\n // long-lived hub signs its asset directory once, so handing back to it would\n // serve the older cockpit for as long as it lives, and a browser that already\n // verified that bundle would never be offered a newer one.\n const running = await probeHub(options.host, options.port);\n if (running !== undefined) {\n const mine = packagedVersion();\n const theirs = running.version ?? 'an older build';\n if (running.version === mine) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n if (running.sessions > 0) {\n notice(`WARNING: ${theirs} is already running at ${url} with ${String(running.sessions)} live session(s).`);\n notice(`WARNING: stop it and run again to serve ${mine}; refusing to interrupt running work.`);\n return;\n }\n if (running.pid === undefined || !isLoopbackHost(options.host)) {\n notice(`WARNING: ${theirs} is already running at ${url} and cannot be replaced automatically.`);\n notice(`WARNING: stop it and run again to serve ${mine}.`);\n return;\n }\n notice(`replacing ${theirs} at ${url} with ${mine}`);\n if (!(await stopRunningHub(running.pid, options.host, options.port))) {\n notice(`WARNING: the hub at ${url} did not stop; serve ${mine} on another port with --port.`);\n return;\n }\n }\n\n /**\n * What Ctrl-C stops, which is not the same thing throughout the run.\n *\n * A single indirection rather than swapping signal handlers, so the handover\n * never has to reach for `removeAllListeners` and take out whatever else this\n * process has registered.\n */\n let shutDown: () => Promise<void> = async () => {};\n\n const start = async (): Promise<WebServer> => {\n const server = await serveWeb({\n port: options.port,\n host: options.host,\n assetsDir: options.assetsDir,\n compositionDir: options.directory,\n registryDir: resolveRegistryDir({\n flagValue: options.registryDir,\n envValue: process.env[REGISTRY_DIR_ENV],\n homeDir: os.homedir(),\n }),\n spawnCommand: options.spawnCommand,\n remoteStateDir: stateDir,\n cloudflaredPath: options.cloudflaredPath,\n onNotice: notice,\n // Already deferred past its own response by the route that asked, so by\n // the time this runs the caller has its answer and the server can go.\n onHandover: (handover) => {\n void handOver({\n serving: server,\n restart: start,\n container,\n handover,\n port: options.port,\n host: options.host,\n adopt: (next) => {\n shutDown = next;\n },\n });\n },\n });\n shutDown = async () => {\n await server.close();\n };\n return server;\n };\n\n try {\n await start();\n } catch (error) {\n // Two cockpits starting together can both find the port free; the loser\n // settles for the winner rather than reporting a clash nobody caused.\n if ((error as NodeJS.ErrnoException).code !== ADDRESS_IN_USE) throw error;\n if (await hubAnswers(options.host, options.port)) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n throw new Error(`Port ${String(options.port)} is taken by something that is not a DoomPi cockpit; pass --port.`);\n }\n\n const stop = (): void => {\n void shutDown().then(() => {\n process.exit(0);\n });\n };\n process.on('SIGINT', stop);\n process.on('SIGTERM', stop);\n}\n\n/**\n * Hands this cockpit over to one running in a container.\n *\n * The port has to be free before the container can publish onto it, so the\n * order is: stop serving, start the container, and put the host cockpit back if\n * the container does not come up. Leaving the user with nothing listening would\n * be the worst outcome of the three, which is why the rollback also recreates\n * the sessions that were stopped in preparation for a move that did not happen.\n */\nasync function handOver(input: {\n serving: WebServer;\n restart: () => Promise<WebServer>;\n container: ReturnType<typeof createCockpitContainer>;\n handover: { settings: RemoteAccessSettings; sessions: readonly MigratingSession[] };\n port: number;\n host: string;\n adopt: (shutDown: () => Promise<void>) => void;\n}): Promise<void> {\n await input.serving.close();\n const started = await input.container.start({\n workspaces: input.handover.settings.sandbox.workspaces.map((workspace) => ({ path: workspace })),\n port: input.port,\n onProgress: notice,\n });\n if (!started.ok) {\n notice(`the cockpit could not move into a container: ${started.error}`);\n notice('serving from the host again');\n await input.restart();\n await relaunchSessions({\n port: input.port,\n host: input.host,\n sessions: input.handover.sessions,\n onNotice: notice,\n });\n return;\n }\n input.adopt(async () => {\n await started.container.stop();\n });\n // The container answers its own health probe before `start` resolves, so its\n // hub is already there to take the settings and the sessions.\n await handOffRemoteAccess({ port: input.port, settings: input.handover.settings, onNotice: notice });\n await relaunchSessions({ port: input.port, sessions: input.handover.sessions, onNotice: notice });\n // Nothing else holds this process open now that its server is closed, and a\n // supervisor that exits leaves a container nobody will ever stop.\n notice('supervising the cockpit container; Ctrl-C stops it');\n await started.container.supervise();\n}\n\nmain().catch((error: unknown) => {\n notice(error instanceof Error ? error.message : String(error));\n process.exitCode = 1;\n});\n"],"mappings":"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"}
|
package/dist/bin/serve.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{REGISTRY_DIR_ENV as e,resolveRegistryDir as t}from"../src/services/registryStore.mjs";import{defaultRemoteStateDir as n}from"../src/adapters/remoteAccessStore.mjs";import{serveWeb as
|
|
2
|
+
import{REGISTRY_DIR_ENV as e,resolveRegistryDir as t}from"../src/services/registryStore.mjs";import{defaultRemoteStateDir as n}from"../src/adapters/remoteAccessStore.mjs";import{packagedVersion as r,serveWeb as i}from"../src/adapters/httpServer.mjs";import{isLoopbackHost as a,parseServeOptions as o,serveHelp as s}from"../src/services/serveOptions.mjs";import{createCockpitContainer as c}from"../src/adapters/cockpitContainer.mjs";import{handOffRemoteAccess as l}from"../src/adapters/cockpitHandoff.mjs";import{ensureDoomInitialized as u}from"../src/adapters/doomInitialization.mjs";import{hubAnswers as d,probeHub as f}from"../src/adapters/hubProbe.mjs";import{relaunchSessions as p}from"../src/adapters/sessionRelaunch.mjs";import m from"node:os";function h(e){process.stderr.write(`[doompi-web] ${e}\n`)}async function g(e,t,n){try{process.kill(e,`SIGTERM`)}catch{}let r=Date.now()+1e4;for(;Date.now()<r;)if(await new Promise(e=>setTimeout(e,100)),!await d(t,n))return!0;return!1}async function _(){let l=o(process.argv.slice(2));if(l.help){process.stdout.write(s());return}if(l.version){process.stdout.write(`${r()}\n`);return}await u({homeDirectory:m.homedir(),onNotice:h});let p=`http://${l.host}:${String(l.port)}`,_=l.stateDir??n(),y=c({stateDir:_,onNotice:h});await y.reapStale(),a(l.host)||(h(`WARNING: --host ${l.host} is not loopback, so the cockpit is reachable without pairing.`),h(`WARNING: turn on remote access in settings instead, which requires a paired device.`));let b=await f(l.host,l.port);if(b!==void 0){let e=r(),t=b.version??`an older build`;if(b.version===e){h(`cockpit already running at ${p}`);return}if(b.sessions>0){h(`WARNING: ${t} is already running at ${p} with ${String(b.sessions)} live session(s).`),h(`WARNING: stop it and run again to serve ${e}; refusing to interrupt running work.`);return}if(b.pid===void 0||!a(l.host)){h(`WARNING: ${t} is already running at ${p} and cannot be replaced automatically.`),h(`WARNING: stop it and run again to serve ${e}.`);return}if(h(`replacing ${t} at ${p} with ${e}`),!await g(b.pid,l.host,l.port)){h(`WARNING: the hub at ${p} did not stop; serve ${e} on another port with --port.`);return}}let x=async()=>{},S=async()=>{let n=await i({port:l.port,host:l.host,assetsDir:l.assetsDir,compositionDir:l.directory,registryDir:t({flagValue:l.registryDir,envValue:process.env[e],homeDir:m.homedir()}),spawnCommand:l.spawnCommand,remoteStateDir:_,cloudflaredPath:l.cloudflaredPath,onNotice:h,onHandover:e=>{v({serving:n,restart:S,container:y,handover:e,port:l.port,host:l.host,adopt:e=>{x=e}})}});return x=async()=>{await n.close()},n};try{await S()}catch(e){if(e.code!==`EADDRINUSE`)throw e;if(await d(l.host,l.port)){h(`cockpit already running at ${p}`);return}throw Error(`Port ${String(l.port)} is taken by something that is not a DoomPi cockpit; pass --port.`)}let C=()=>{x().then(()=>{process.exit(0)})};process.on(`SIGINT`,C),process.on(`SIGTERM`,C)}async function v(e){await e.serving.close();let t=await e.container.start({workspaces:e.handover.settings.sandbox.workspaces.map(e=>({path:e})),port:e.port,onProgress:h});if(!t.ok){h(`the cockpit could not move into a container: ${t.error}`),h(`serving from the host again`),await e.restart(),await p({port:e.port,host:e.host,sessions:e.handover.sessions,onNotice:h});return}e.adopt(async()=>{await t.container.stop()}),await l({port:e.port,settings:e.handover.settings,onNotice:h}),await p({port:e.port,sessions:e.handover.sessions,onNotice:h}),h(`supervising the cockpit container; Ctrl-C stops it`),await t.container.supervise()}_().catch(e=>{h(e instanceof Error?e.message:String(e)),process.exitCode=1});export{};
|
|
3
3
|
//# sourceMappingURL=serve.mjs.map
|
package/dist/bin/serve.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.mjs","names":[],"sources":["../../src/bin/serve.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport os from 'node:os';\nimport { createCockpitContainer } from '../adapters/cockpitContainer.ts';\nimport { handOffRemoteAccess } from '../adapters/cockpitHandoff.ts';\nimport { hubAnswers } from '../adapters/hubProbe.ts';\nimport { serveWeb } from '../adapters/httpServer.ts';\nimport { defaultRemoteStateDir } from '../adapters/remoteAccessStore.ts';\nimport { relaunchSessions } from '../adapters/sessionRelaunch.ts';\nimport { REGISTRY_DIR_ENV, resolveRegistryDir } from '../services/registryStore.ts';\nimport { isLoopbackHost, parseServeOptions } from '../services/serveOptions.ts';\nimport type { WebServer } from '../types/bridge.ts';\nimport type { MigratingSession, RemoteAccessSettings } from '../types/remoteAccess.ts';\n\nconst ADDRESS_IN_USE = 'EADDRINUSE';\n\nfunction notice(message: string): void {\n process.stderr.write(`[doompi-web] ${message}\\n`);\n}\n\nasync function main(): Promise<void> {\n const options = parseServeOptions(process.argv.slice(2));\n const url = `http://${options.host}:${String(options.port)}`;\n const stateDir = options.stateDir ?? defaultRemoteStateDir();\n const container = createCockpitContainer({ stateDir, onNotice: notice });\n // A cockpit that was killed rather than closed leaves its container holding\n // the published port, so the next start would fail to bind for a reason\n // nothing explains.\n await container.reapStale();\n\n // Binding off loopback publishes an unauthenticated cockpit to whoever can\n // reach the address. Remote access exists so nobody has to do this; a warning\n // rather than a refusal, because an existing setup should not break today.\n if (!isLoopbackHost(options.host)) {\n notice(`WARNING: --host ${options.host} is not loopback, so the cockpit is reachable without pairing.`);\n notice('WARNING: turn on remote access in settings instead, which requires a paired device.');\n }\n\n // One hub serves every session, so a second start is a request to use the\n // running one, not an error worth a stack trace.\n if (await hubAnswers(options.host, options.port)) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n\n /**\n * What Ctrl-C stops, which is not the same thing throughout the run.\n *\n * A single indirection rather than swapping signal handlers, so the handover\n * never has to reach for `removeAllListeners` and take out whatever else this\n * process has registered.\n */\n let shutDown: () => Promise<void> = async () => {};\n\n const start = async (): Promise<WebServer> => {\n const server = await serveWeb({\n port: options.port,\n host: options.host,\n assetsDir: options.assetsDir,\n registryDir: resolveRegistryDir({\n flagValue: options.registryDir,\n envValue: process.env[REGISTRY_DIR_ENV],\n homeDir: os.homedir(),\n }),\n spawnCommand: options.spawnCommand,\n remoteStateDir: stateDir,\n cloudflaredPath: options.cloudflaredPath,\n onNotice: notice,\n // Already deferred past its own response by the route that asked, so by\n // the time this runs the caller has its answer and the server can go.\n onHandover: (handover) => {\n void handOver({\n serving: server,\n restart: start,\n container,\n handover,\n port: options.port,\n host: options.host,\n adopt: (next) => {\n shutDown = next;\n },\n });\n },\n });\n shutDown = async () => {\n await server.close();\n };\n return server;\n };\n\n try {\n await start();\n } catch (error) {\n // Two cockpits starting together can both find the port free; the loser\n // settles for the winner rather than reporting a clash nobody caused.\n if ((error as NodeJS.ErrnoException).code !== ADDRESS_IN_USE) throw error;\n if (await hubAnswers(options.host, options.port)) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n throw new Error(`Port ${String(options.port)} is taken by something that is not a DoomPi cockpit; pass --port.`);\n }\n\n const stop = (): void => {\n void shutDown().then(() => {\n process.exit(0);\n });\n };\n process.on('SIGINT', stop);\n process.on('SIGTERM', stop);\n}\n\n/**\n * Hands this cockpit over to one running in a container.\n *\n * The port has to be free before the container can publish onto it, so the\n * order is: stop serving, start the container, and put the host cockpit back if\n * the container does not come up. Leaving the user with nothing listening would\n * be the worst outcome of the three, which is why the rollback also recreates\n * the sessions that were stopped in preparation for a move that did not happen.\n */\nasync function handOver(input: {\n serving: WebServer;\n restart: () => Promise<WebServer>;\n container: ReturnType<typeof createCockpitContainer>;\n handover: { settings: RemoteAccessSettings; sessions: readonly MigratingSession[] };\n port: number;\n host: string;\n adopt: (shutDown: () => Promise<void>) => void;\n}): Promise<void> {\n await input.serving.close();\n const started = await input.container.start({\n workspaces: input.handover.settings.sandbox.workspaces.map((workspace) => ({ path: workspace })),\n port: input.port,\n onProgress: notice,\n });\n if (!started.ok) {\n notice(`the cockpit could not move into a container: ${started.error}`);\n notice('serving from the host again');\n await input.restart();\n await relaunchSessions({\n port: input.port,\n host: input.host,\n sessions: input.handover.sessions,\n onNotice: notice,\n });\n return;\n }\n input.adopt(async () => {\n await started.container.stop();\n });\n // The container answers its own health probe before `start` resolves, so its\n // hub is already there to take the settings and the sessions.\n await handOffRemoteAccess({ port: input.port, settings: input.handover.settings, onNotice: notice });\n await relaunchSessions({ port: input.port, sessions: input.handover.sessions, onNotice: notice });\n // Nothing else holds this process open now that its server is closed, and a\n // supervisor that exits leaves a container nobody will ever stop.\n notice('supervising the cockpit container; Ctrl-C stops it');\n await started.container.supervise();\n}\n\nmain().catch((error: unknown) => {\n notice(error instanceof Error ? error.message : String(error));\n process.exitCode = 1;\n});\n"],"mappings":";6mBAgBA,SAAS,EAAO,EAAuB,CACrC,QAAQ,OAAO,MAAM,gBAAgB,EAAQ,GAAG,CAClD,CAEA,eAAe,GAAsB,CACnC,IAAM,EAAU,EAAkB,QAAQ,KAAK,MAAM,CAAC,CAAC,EACjD,EAAM,UAAU,EAAQ,KAAK,GAAG,OAAO,EAAQ,IAAI,IACnD,EAAW,EAAQ,UAAY,EAAsB,EACrD,EAAY,EAAuB,CAAE,WAAU,SAAU,CAAO,CAAC,EAgBvE,GAZA,MAAM,EAAU,UAAU,EAKrB,EAAe,EAAQ,IAAI,IAC9B,EAAO,mBAAmB,EAAQ,KAAK,+DAA+D,EACtG,EAAO,qFAAqF,GAK1F,MAAM,EAAW,EAAQ,KAAM,EAAQ,IAAI,EAAG,CAChD,EAAO,8BAA8B,GAAK,EAC1C,MACF,CASA,IAAI,EAAgC,SAAY,CAAC,EAE3C,EAAQ,SAAgC,CAC5C,IAAM,EAAS,MAAM,EAAS,CAC5B,KAAM,EAAQ,KACd,KAAM,EAAQ,KACd,UAAW,EAAQ,UACnB,YAAa,EAAmB,CAC9B,UAAW,EAAQ,YACnB,SAAU,QAAQ,IAAI,GACtB,QAAS,EAAG,QAAQ,CACtB,CAAC,EACD,aAAc,EAAQ,aACtB,eAAgB,EAChB,gBAAiB,EAAQ,gBACzB,SAAU,EAGV,WAAa,GAAa,CACxB,EAAc,CACZ,QAAS,EACT,QAAS,EACT,YACA,WACA,KAAM,EAAQ,KACd,KAAM,EAAQ,KACd,MAAQ,GAAS,CACf,EAAW,CACb,CACF,CAAC,CACH,CACF,CAAC,EAID,MAHA,GAAW,SAAY,CACrB,MAAM,EAAO,MAAM,CACrB,EACO,CACT,EAEA,GAAI,CACF,MAAM,EAAM,CACd,OAAS,EAAO,CAGd,GAAK,EAAgC,OAAS,aAAgB,MAAM,EACpE,GAAI,MAAM,EAAW,EAAQ,KAAM,EAAQ,IAAI,EAAG,CAChD,EAAO,8BAA8B,GAAK,EAC1C,MACF,CACA,MAAU,MAAM,QAAQ,OAAO,EAAQ,IAAI,EAAE,kEAAkE,CACjH,CAEA,IAAM,MAAmB,CACvB,EAAc,CAAC,CAAC,SAAW,CACzB,QAAQ,KAAK,CAAC,CAChB,CAAC,CACH,EACA,QAAQ,GAAG,SAAU,CAAI,EACzB,QAAQ,GAAG,UAAW,CAAI,CAC5B,CAWA,eAAe,EAAS,EAQN,CAChB,MAAM,EAAM,QAAQ,MAAM,EAC1B,IAAM,EAAU,MAAM,EAAM,UAAU,MAAM,CAC1C,WAAY,EAAM,SAAS,SAAS,QAAQ,WAAW,IAAK,IAAe,CAAE,KAAM,CAAU,EAAE,EAC/F,KAAM,EAAM,KACZ,WAAY,CACd,CAAC,EACD,GAAI,CAAC,EAAQ,GAAI,CACf,EAAO,gDAAgD,EAAQ,OAAO,EACtE,EAAO,6BAA6B,EACpC,MAAM,EAAM,QAAQ,EACpB,MAAM,EAAiB,CACrB,KAAM,EAAM,KACZ,KAAM,EAAM,KACZ,SAAU,EAAM,SAAS,SACzB,SAAU,CACZ,CAAC,EACD,MACF,CACA,EAAM,MAAM,SAAY,CACtB,MAAM,EAAQ,UAAU,KAAK,CAC/B,CAAC,EAGD,MAAM,EAAoB,CAAE,KAAM,EAAM,KAAM,SAAU,EAAM,SAAS,SAAU,SAAU,CAAO,CAAC,EACnG,MAAM,EAAiB,CAAE,KAAM,EAAM,KAAM,SAAU,EAAM,SAAS,SAAU,SAAU,CAAO,CAAC,EAGhG,EAAO,oDAAoD,EAC3D,MAAM,EAAQ,UAAU,UAAU,CACpC,CAEA,EAAK,CAAC,CAAC,MAAO,GAAmB,CAC/B,EAAO,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,CAAC,EAC7D,QAAQ,SAAW,CACrB,CAAC"}
|
|
1
|
+
{"version":3,"file":"serve.mjs","names":[],"sources":["../../src/bin/serve.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport os from 'node:os';\nimport { createCockpitContainer } from '../adapters/cockpitContainer.ts';\nimport { handOffRemoteAccess } from '../adapters/cockpitHandoff.ts';\nimport { ensureDoomInitialized } from '../adapters/doomInitialization.ts';\nimport { hubAnswers, probeHub } from '../adapters/hubProbe.ts';\nimport { packagedVersion, serveWeb } from '../adapters/httpServer.ts';\nimport { defaultRemoteStateDir } from '../adapters/remoteAccessStore.ts';\nimport { relaunchSessions } from '../adapters/sessionRelaunch.ts';\nimport { REGISTRY_DIR_ENV, resolveRegistryDir } from '../services/registryStore.ts';\nimport { isLoopbackHost, parseServeOptions, serveHelp } from '../services/serveOptions.ts';\nimport type { WebServer } from '../types/bridge.ts';\nimport type { MigratingSession, RemoteAccessSettings } from '../types/remoteAccess.ts';\n\nconst ADDRESS_IN_USE = 'EADDRINUSE';\nconst STOP_POLL_MS = 100;\nconst STOP_TIMEOUT_MS = 10_000;\n\nfunction notice(message: string): void {\n process.stderr.write(`[doompi-web] ${message}\\n`);\n}\n\n/**\n * Signals the running hub and waits for the address to go quiet.\n *\n * Only ever called for a loopback address, because the pid on the other end of\n * the probe is only this machine's pid when the hub is on this machine.\n */\nasync function stopRunningHub(pid: number, host: string, port: number): Promise<boolean> {\n try {\n process.kill(pid, 'SIGTERM');\n } catch {\n // Already gone, or not ours to signal. The probe below settles it either way.\n }\n const deadline = Date.now() + STOP_TIMEOUT_MS;\n while (Date.now() < deadline) {\n await new Promise((resolve) => setTimeout(resolve, STOP_POLL_MS));\n if (!(await hubAnswers(host, port))) return true;\n }\n return false;\n}\n\nasync function main(): Promise<void> {\n const options = parseServeOptions(process.argv.slice(2));\n if (options.help) {\n process.stdout.write(serveHelp());\n return;\n }\n if (options.version) {\n process.stdout.write(`${packagedVersion()}\\n`);\n return;\n }\n await ensureDoomInitialized({ homeDirectory: os.homedir(), onNotice: notice });\n const url = `http://${options.host}:${String(options.port)}`;\n const stateDir = options.stateDir ?? defaultRemoteStateDir();\n const container = createCockpitContainer({ stateDir, onNotice: notice });\n // A cockpit that was killed rather than closed leaves its container holding\n // the published port, so the next start would fail to bind for a reason\n // nothing explains.\n await container.reapStale();\n\n // Binding off loopback publishes an unauthenticated cockpit to whoever can\n // reach the address. Remote access exists so nobody has to do this; a warning\n // rather than a refusal, because an existing setup should not break today.\n if (!isLoopbackHost(options.host)) {\n notice(`WARNING: --host ${options.host} is not loopback, so the cockpit is reachable without pairing.`);\n notice('WARNING: turn on remote access in settings instead, which requires a paired device.');\n }\n\n // One hub serves every session, so a second start is normally a request to\n // use the running one. A second start from a *different* version is not: a\n // long-lived hub signs its asset directory once, so handing back to it would\n // serve the older cockpit for as long as it lives, and a browser that already\n // verified that bundle would never be offered a newer one.\n const running = await probeHub(options.host, options.port);\n if (running !== undefined) {\n const mine = packagedVersion();\n const theirs = running.version ?? 'an older build';\n if (running.version === mine) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n if (running.sessions > 0) {\n notice(`WARNING: ${theirs} is already running at ${url} with ${String(running.sessions)} live session(s).`);\n notice(`WARNING: stop it and run again to serve ${mine}; refusing to interrupt running work.`);\n return;\n }\n if (running.pid === undefined || !isLoopbackHost(options.host)) {\n notice(`WARNING: ${theirs} is already running at ${url} and cannot be replaced automatically.`);\n notice(`WARNING: stop it and run again to serve ${mine}.`);\n return;\n }\n notice(`replacing ${theirs} at ${url} with ${mine}`);\n if (!(await stopRunningHub(running.pid, options.host, options.port))) {\n notice(`WARNING: the hub at ${url} did not stop; serve ${mine} on another port with --port.`);\n return;\n }\n }\n\n /**\n * What Ctrl-C stops, which is not the same thing throughout the run.\n *\n * A single indirection rather than swapping signal handlers, so the handover\n * never has to reach for `removeAllListeners` and take out whatever else this\n * process has registered.\n */\n let shutDown: () => Promise<void> = async () => {};\n\n const start = async (): Promise<WebServer> => {\n const server = await serveWeb({\n port: options.port,\n host: options.host,\n assetsDir: options.assetsDir,\n compositionDir: options.directory,\n registryDir: resolveRegistryDir({\n flagValue: options.registryDir,\n envValue: process.env[REGISTRY_DIR_ENV],\n homeDir: os.homedir(),\n }),\n spawnCommand: options.spawnCommand,\n remoteStateDir: stateDir,\n cloudflaredPath: options.cloudflaredPath,\n onNotice: notice,\n // Already deferred past its own response by the route that asked, so by\n // the time this runs the caller has its answer and the server can go.\n onHandover: (handover) => {\n void handOver({\n serving: server,\n restart: start,\n container,\n handover,\n port: options.port,\n host: options.host,\n adopt: (next) => {\n shutDown = next;\n },\n });\n },\n });\n shutDown = async () => {\n await server.close();\n };\n return server;\n };\n\n try {\n await start();\n } catch (error) {\n // Two cockpits starting together can both find the port free; the loser\n // settles for the winner rather than reporting a clash nobody caused.\n if ((error as NodeJS.ErrnoException).code !== ADDRESS_IN_USE) throw error;\n if (await hubAnswers(options.host, options.port)) {\n notice(`cockpit already running at ${url}`);\n return;\n }\n throw new Error(`Port ${String(options.port)} is taken by something that is not a DoomPi cockpit; pass --port.`);\n }\n\n const stop = (): void => {\n void shutDown().then(() => {\n process.exit(0);\n });\n };\n process.on('SIGINT', stop);\n process.on('SIGTERM', stop);\n}\n\n/**\n * Hands this cockpit over to one running in a container.\n *\n * The port has to be free before the container can publish onto it, so the\n * order is: stop serving, start the container, and put the host cockpit back if\n * the container does not come up. Leaving the user with nothing listening would\n * be the worst outcome of the three, which is why the rollback also recreates\n * the sessions that were stopped in preparation for a move that did not happen.\n */\nasync function handOver(input: {\n serving: WebServer;\n restart: () => Promise<WebServer>;\n container: ReturnType<typeof createCockpitContainer>;\n handover: { settings: RemoteAccessSettings; sessions: readonly MigratingSession[] };\n port: number;\n host: string;\n adopt: (shutDown: () => Promise<void>) => void;\n}): Promise<void> {\n await input.serving.close();\n const started = await input.container.start({\n workspaces: input.handover.settings.sandbox.workspaces.map((workspace) => ({ path: workspace })),\n port: input.port,\n onProgress: notice,\n });\n if (!started.ok) {\n notice(`the cockpit could not move into a container: ${started.error}`);\n notice('serving from the host again');\n await input.restart();\n await relaunchSessions({\n port: input.port,\n host: input.host,\n sessions: input.handover.sessions,\n onNotice: notice,\n });\n return;\n }\n input.adopt(async () => {\n await started.container.stop();\n });\n // The container answers its own health probe before `start` resolves, so its\n // hub is already there to take the settings and the sessions.\n await handOffRemoteAccess({ port: input.port, settings: input.handover.settings, onNotice: notice });\n await relaunchSessions({ port: input.port, sessions: input.handover.sessions, onNotice: notice });\n // Nothing else holds this process open now that its server is closed, and a\n // supervisor that exits leaves a container nobody will ever stop.\n notice('supervising the cockpit container; Ctrl-C stops it');\n await started.container.supervise();\n}\n\nmain().catch((error: unknown) => {\n notice(error instanceof Error ? error.message : String(error));\n process.exitCode = 1;\n});\n"],"mappings":"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"}
|
package/dist/pwa/sw.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
(function(){var e=`/bundle-manifest.json`,t=/^[0-9a-f]{64}$/u,n=/^[A-Za-z0-9_-]+$/u,r=/^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/u;function i(e,t){let n=Object.keys(e).sort();return n.length===t.length&&n.every((e,n)=>e===t[n])}function a(e,t){return typeof e==`number`&&Number.isSafeInteger(e)&&e>=t}function o(e){return typeof e!=`string`||e.length<2||!e.startsWith(`/`)||e.includes(`\\`)||e.includes(`\0`)||e.includes(`?`)||e.includes(`#`)||/[^\x21-\x7e]/u.test(e)||e.includes(`//`)||/%(?![0-9a-f]{2})/iu.test(e)||/%(?:2e|2f|5c)/iu.test(e)?!1:e.split(`/`).slice(1).every(e=>e!==``&&e!==`.`&&e!==`..`)}function s(e){let t=[...e.assets].sort((e,t)=>e.path<t.path?-1:+(e.path>t.path)).map(e=>`${e.path}\0${e.sha256}\0${String(e.byteLength)}\0${e.contentType}`);return[String(e.version),String(e.revision),String(e.builtAt),...t].join(`
|
|
2
|
-
`)}function c(e){if(typeof e!=`object`||!e||!i(e,[`assets`,`builtAt`,`revision`,`version`]))return!1;let n=e;if(n.version!==2||!a(n.revision,1)||!a(n.builtAt,0)||!Array.isArray(n.assets)||n.assets.length===0)return!1;let s=new Set;for(let e of n.assets){if(typeof e!=`object`||!e||!i(e,[`byteLength`,`contentType`,`path`,`sha256`]))return!1;let n=e;if(!o(n.path)||s.has(n.path)||typeof n.sha256!=`string`||!t.test(n.sha256)||!a(n.byteLength,0)||typeof n.contentType!=`string`||!r.test(n.contentType))return!1;s.add(n.path)}return s.has(`/index.html`)}function l(e){if(typeof e!=`object`||!e||!i(e,[`manifest`,`publicKey`,`signature`]))return!1;let t=e;return c(t.manifest)&&typeof t.signature==`string`&&t.signature.length===86&&n.test(t.signature)&&typeof t.publicKey==`string`&&t.publicKey.length<=512&&n.test(t.publicKey)}function u(e,t){return e.assets.find(e=>e.path===t)}function d(e){let t=e.replaceAll(`-`,`+`).replaceAll(`_`,`/`),n=t.padEnd(Math.ceil(t.length/4)*4,`=`),r=atob(n),i=new Uint8Array(r.length);for(let e=0;e<r.length;e+=1)i[e]=r.charCodeAt(e);return i.buffer}function f(e){return[...new Uint8Array(e)].map(e=>e.toString(16).padStart(2,`0`)).join(``)}async function p(e,t,n=0){if(!l(e))return{ok:!1,failure:{code:`invalid-envelope`}};let r=e;if(r.publicKey!==t)return{ok:!1,failure:{code:`untrusted-public-key`}};if(!Number.isSafeInteger(n)||n<0||r.manifest.revision<n)return{ok:!1,failure:{code:`stale-revision`,minimumRevision:n,actualRevision:r.manifest.revision}};let i;try{i=await crypto.subtle.importKey(`spki`,d(t),{name:`ECDSA`,namedCurve:`P-256`},!1,[`verify`])}catch{return{ok:!1,failure:{code:`invalid-public-key`}}}try{return await crypto.subtle.verify({name:`ECDSA`,hash:`SHA-256`},i,d(r.signature),new TextEncoder().encode(s(r.manifest)))?{ok:!0,manifest:r.manifest}:{ok:!1,failure:{code:`invalid-signature`}}}catch{return{ok:!1,failure:{code:`invalid-signature`}}}}async function m(e,t,n){let r=u(e,t);return r===void 0?{ok:!1,failure:{code:`asset-not-listed`,path:t}}:n.byteLength===r.byteLength?f(await crypto.subtle.digest(`SHA-256`,n))===r.sha256?{ok:!0}:{ok:!1,failure:{code:`digest-mismatch`}}:{ok:!1,failure:{code:`byte-length-mismatch`,expected:r.byteLength,actual:n.byteLength}}}var h=2**32,g=`doompi/sealed/c2s/v1`,_=`doompi/sealed/s2c/v1`;function v(e){return e===`c2s`?g:_}function y(e){if(typeof e!=`object`||!e)return!1;let t=e;return t.v===1&&typeof t.n==`string`&&typeof t.c==`string`}function b(e){switch(e){case`version`:return`The other end is speaking a different envelope version.`;case`malformed`:return`A sealed message was not shaped like one.`;case`replay`:return`A sealed message arrived out of order or twice.`;case`auth`:return`A sealed message failed its authentication tag, so it was altered in transit.`;case`exhausted`:return`This channel reached its message limit and must be re-established.`}}var x={name:`ECDH`,namedCurve:`P-256`},S=`AES-GCM`;function C(e){let t;try{t=atob(e.replaceAll(`-`,`+`).replaceAll(`_`,`/`))}catch{return}let n=new Uint8Array(new ArrayBuffer(t.length));for(let e=0;e<t.length;e+=1)n[e]=t.charCodeAt(e);return n}function w(e){let t=``;for(let n of e)t+=String.fromCharCode(n);return btoa(t).replaceAll(`+`,`-`).replaceAll(`/`,`_`).replaceAll(`=`,``)}function T(e){let t=new Uint8Array(new ArrayBuffer(e.length));return t.set(e),t.buffer}async function E(e,t){let n=await crypto.subtle.importKey(`raw`,e,`HKDF`,!1,[`deriveKey`]);return await crypto.subtle.deriveKey({name:`HKDF`,hash:`SHA-256`,salt:T(new TextEncoder().encode(`doompi/sealed/v1`)),info:T(new TextEncoder().encode(v(t)))},n,{name:S,length:256},!1,[`encrypt`,`decrypt`])}function D(e,t){let n=new Uint8Array(12);return n.set(e.subarray(0,4),0),new DataView(n.buffer).setBigUint64(4,t,!1),n}function O(e,t,n){let r=0n,i=-1n;return{async seal(t){if(r>=BigInt(h))return{ok:!1,failure:`exhausted`};let i=D(n,r),a=await crypto.subtle.encrypt({name:S,iv:T(i),tagLength:128},e,T(t));return r+=1n,{ok:!0,envelope:{v:1,n:w(i),c:w(new Uint8Array(a))}}},async open(e){let n=e;if(typeof n?.n!=`string`||typeof n.c!=`string`)return{ok:!1,failure:`malformed`};if(n.v!==1)return{ok:!1,failure:`version`};if(!y(e))return{ok:!1,failure:`malformed`};let r=C(e.n),a=C(e.c);if(r===void 0||a===void 0||r.length!==12||a.length<16)return{ok:!1,failure:`malformed`};let o=new DataView(T(r)).getBigUint64(4,!1);if(o<=i)return{ok:!1,failure:`replay`};let s;try{s=await crypto.subtle.decrypt({name:S,iv:T(r),tagLength:128},t,T(a))}catch{return{ok:!1,failure:`auth`}}return i=o,{ok:!0,plaintext:new Uint8Array(s)}}}}async function k(e){if(globalThis.crypto?.subtle!==void 0)try{let t=await crypto.subtle.generateKey(x,!1,[`deriveBits`]),n=C(e);if(n===void 0)return;let r=await crypto.subtle.importKey(`raw`,T(n),x,!1,[]),i=await crypto.subtle.deriveBits({name:`ECDH`,public:r},t.privateKey,256),a=w(new Uint8Array(await crypto.subtle.exportKey(`raw`,t.publicKey)));return{channel:O(await E(i,`c2s`),await E(i,`s2c`),new Uint8Array(4)),clientPublicKey:a}}catch{return}}function A(){let e=Promise.resolve();return{run(t){let n=e.then(t,t);return e=n.catch(()=>void 0),n}}}var j=new TextEncoder,M=new TextDecoder;function N(e){let t=``;for(let n of e)t+=String.fromCharCode(n);return btoa(t)}function P(e){let t=atob(e);return Uint8Array.from(t,e=>e.charCodeAt(0))}async function ee(e){if(e!=null){if(typeof e==`string`)return j.encode(e);if(e instanceof URLSearchParams)return j.encode(e.toString());if(e instanceof Blob)return new Uint8Array(await e.arrayBuffer());if(e instanceof ArrayBuffer)return new Uint8Array(e);if(ArrayBuffer.isView(e))return new Uint8Array(e.buffer,e.byteOffset,e.byteLength);throw Error(`The sealed HTTP transport does not support this request body type.`)}}function te(e){if(!e.startsWith(`/`))throw Error(`A sealed HTTP request must use a root-relative URL.`);return e}function F(e){if(typeof e!=`object`||!e)return!1;let t=e;return t.v===1&&typeof t.status==`number`&&Array.isArray(t.headers)&&t.headers.every(e=>Array.isArray(e)&&e.length===2&&e.every(e=>typeof e==`string`))&&typeof t.body==`string`}function I(){let e,t,n=A(),r=A(),i=A(),a,o=e=>{t=b(e)},s=async t=>{if(e===void 0)return;let n=await e.seal(t);return n.ok?JSON.stringify(n.envelope):o(n.failure)},c=async t=>{if(e===void 0)return;let n;try{n=JSON.parse(t)}catch{return o(`malformed`)}let r=await e.open(n);return r.ok?r.plaintext:o(r.failure)};return{active:()=>e!==void 0,lastFailure:()=>t,async connect(n){let r=await k(n);if(r===void 0){t=`This browser could not establish a sealed channel.`;return}return e=r.channel,t=void 0,r.clientPublicKey},relayRequestsThrough(e){a=e},reset(){e=void 0,t=void 0},async sealText(r){return e===void 0?r:await n.run(async()=>{let e=await s(j.encode(r));if(e===void 0)throw Error(t??`The active sealed channel could not encrypt a message.`);return e})},async openText(t){return e===void 0?t:await r.run(async()=>{let e=await c(t);return e===void 0?void 0:M.decode(e)})},async sealBinary(r){return e===void 0?r:await n.run(async()=>{let e=await s(r);if(e===void 0)throw Error(t??`The active sealed channel could not encrypt a message.`);return j.encode(e)})},async openBinary(t){return e===void 0?t:await r.run(async()=>await c(M.decode(t)))},async fetch(n,r){let o=a;return e===void 0||o===void 0?await fetch(n,r):await i.run(async()=>{let e=await ee(r?.body),i={v:1,method:(r?.method??`GET`).toUpperCase(),target:te(n),headers:Array.from(new Headers(r?.headers).entries()),...e===void 0?{}:{body:N(e)}},a=await s(j.encode(JSON.stringify(i)));if(a===void 0)return new Response(JSON.stringify({error:t}),{status:502});let l=await fetch(o,{method:`POST`,credentials:`same-origin`,headers:{"content-type":`application/json`},body:a}),u=await c(await l.text());if(u===void 0)return new Response(JSON.stringify({error:t}),{status:502});let d;try{d=JSON.parse(M.decode(u))}catch{d=void 0}if(!F(d))return t=`The sealed HTTP response was malformed.`,new Response(JSON.stringify({error:t}),{status:502});let f=P(d.body);return new Response(d.status===204||d.status===205||d.status===304?null:f,{status:d.status,headers:d.headers})})}}}I();var L=`doompi-pwa`,R=`state`,z=`active-bundle`;function B(e){return new Promise((t,n)=>{e.addEventListener(`success`,()=>t(e.result),{once:!0}),e.addEventListener(`error`,()=>n(e.error??Error(`The PWA database request failed.`)),{once:!0})})}function V(e){return new Promise((t,n)=>{e.addEventListener(`complete`,()=>t(),{once:!0}),e.addEventListener(`abort`,()=>n(e.error??Error(`The PWA transaction aborted.`)),{once:!0}),e.addEventListener(`error`,()=>n(e.error??Error(`The PWA transaction failed.`)),{once:!0})})}async function H(){let e=indexedDB.open(L,1);return e.addEventListener(`upgradeneeded`,()=>{let t=e.result;t.objectStoreNames.contains(`state`)||t.createObjectStore(R)}),await B(e)}async function U(){let e=await H();try{let t=e.transaction(R,`readonly`),n=await B(t.objectStore(R).get(z));return await V(t),n}finally{e.close()}}async function W(e){let t=await H();try{let n=t.transaction(R,`readwrite`,{durability:`strict`});n.objectStore(R).put(e,z),await V(n)}finally{t.close()}}async function G(){let e=await H();try{let t=e.transaction(R,`readwrite`,{durability:`strict`});t.objectStore(R).delete(z),await V(t)}finally{e.close()}}var K=self,q=`/bundle-assets/`,J=`doompi-bundle-`,Y=`doompi:activate-bundle`,X=`doompi:refresh-bundle`,Z=`doompi:reset-bundle-trust`;function Q(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function ne(e){if(!(!Q(e)||typeof e.type!=`string`)){if(e.type===X||e.type===Z)return{type:e.type};if(!(e.type!==Y||typeof e.publicKey!=`string`||!Number.isSafeInteger(e.minimumRevision)||Number(e.minimumRevision)<1))return{type:Y,publicKey:e.publicKey,minimumRevision:Number(e.minimumRevision)}}}function re(e){return[...new Uint8Array(e)].map(e=>e.toString(16).padStart(2,`0`)).join(``)}async function ie(e){let t=new TextEncoder().encode(s(e));return re(await crypto.subtle.digest(`SHA-256`,t))}function ae(t){return t===`/pair`||t===`/sw.js`||t===`/manifest.webmanifest`||t.startsWith(`/pwa/`)||t.startsWith(`/api/`)||t===e||t.startsWith(q)}async function oe(e,t){let n=await caches.open(e.cacheName),r=new URL(t.url),i=t.mode===`navigate`?`/index.html`:r.pathname;return await n.match(i)??new Response(`That verified cockpit asset is unavailable.`,{status:404})}async function se(){let t=await fetch(e,{credentials:`include`,cache:`no-store`,redirect:`error`});if(!t.ok)throw Error(`The signed manifest request failed (${String(t.status)}).`);return await t.json()}async function $(e,t){let n=await U();if(n!==void 0&&n.signerPublicKey!==e)return{ok:!1,code:`signer-mismatch`,message:`The host signing key does not match the pinned key.`};let r;try{r=await se()}catch(e){return{ok:!1,code:`manifest-fetch`,message:e instanceof Error?e.message:String(e)}}let i=Math.max(t,n?.revision??0),a=await p(r,e,i);if(!a.ok)return{ok:!1,code:a.failure.code,message:`The signed bundle manifest was refused.`};let o=await ie(a.manifest);if(n?.revision===a.manifest.revision){if(n.manifestDigest!==o)return{ok:!1,code:`revision-conflict`,message:`The host reused a revision for different bundle bytes.`};if(await caches.has(n.cacheName))return{ok:!0,revision:n.revision}}let s=`${J}${String(a.manifest.revision)}-${o.slice(0,16)}`;await caches.delete(s);let c=await caches.open(s);try{for(let e of a.manifest.assets){let t=`${q}${String(a.manifest.revision)}${e.path}`,n=await fetch(t,{credentials:`include`,cache:`no-store`,redirect:`error`});if(!n.ok||n.redirected||new URL(n.url).origin!==K.location.origin)throw Error(`The raw bundle asset ${e.path} was unavailable.`);let r=await n.arrayBuffer(),i=await m(a.manifest,e.path,r);if(!i.ok)throw Error(`The raw bundle asset ${e.path} failed ${i.failure.code}.`);await c.put(e.path,new Response(r,{status:200,headers:{"Cache-Control":`no-store`,"Content-Length":String(e.byteLength),"Content-Type":e.contentType,"X-Content-Type-Options":`nosniff`}}))}let t={signerPublicKey:e,manifestDigest:o,revision:a.manifest.revision,cacheName:s};await W(t);for(let e of await caches.keys())e.startsWith(J)&&e!==t.cacheName&&e!==n?.cacheName&&await caches.delete(e);return{ok:!0,revision:t.revision}}catch(e){return await caches.delete(s),{ok:!1,code:`asset-verification`,message:e instanceof Error?e.message:String(e)}}}async function ce(e){if(e.type===Z){for(let e of await caches.keys())e.startsWith(J)&&await caches.delete(e);return await G(),{ok:!0}}if(e.type===Y)return await $(e.publicKey,e.minimumRevision);let t=await U();return t===void 0?{ok:!1,code:`no-pin`,message:`No host signing key is pinned.`}:await $(t.signerPublicKey,t.revision)}K.addEventListener(`install`,e=>{e.waitUntil(K.skipWaiting())}),K.addEventListener(`activate`,e=>{e.waitUntil(K.clients.claim())}),K.addEventListener(`message`,e=>{let t=ne(e.data),n=e.ports[0];if(n!==void 0){if(t===void 0){n.postMessage({ok:!1,code:`invalid-request`,message:`The worker request was malformed.`});return}e.waitUntil(ce(t).then(e=>n.postMessage(e)).catch(e=>n.postMessage({ok:!1,code:`worker-failure`,message:e instanceof Error?e.message:String(e)})))}}),K.addEventListener(`fetch`,e=>{let t=new URL(e.request.url);t.origin!==K.location.origin||ae(t.pathname)||e.respondWith(U().then(async t=>t===void 0?await fetch(e.request):await oe(t,e.request)))}),K.addEventListener(`push`,e=>{let t=`A live session needs your attention.`;try{let n=e.data?.json();Q(n)&&n.body===t&&(t=n.body)}catch{}e.waitUntil(K.registration.showNotification(`DoomPi`,{body:t,tag:`doompi-live`,icon:`/pwa/icon-192.png`,badge:`/pwa/icon-192.png`,data:{url:`/`}}))}),K.addEventListener(`notificationclick`,e=>{e.notification.close(),e.waitUntil(K.clients.matchAll({type:`window`,includeUncontrolled:!0}).then(async e=>{let t=e.find(e=>new URL(e.url).origin===K.location.origin);if(t!==void 0){await t.focus();return}await K.clients.openWindow(`/`)}))})})();
|
|
2
|
+
`)}function c(e){if(typeof e!=`object`||!e||!i(e,[`assets`,`builtAt`,`revision`,`version`]))return!1;let n=e;if(n.version!==2||!a(n.revision,1)||!a(n.builtAt,0)||!Array.isArray(n.assets)||n.assets.length===0)return!1;let s=new Set;for(let e of n.assets){if(typeof e!=`object`||!e||!i(e,[`byteLength`,`contentType`,`path`,`sha256`]))return!1;let n=e;if(!o(n.path)||s.has(n.path)||typeof n.sha256!=`string`||!t.test(n.sha256)||!a(n.byteLength,0)||typeof n.contentType!=`string`||!r.test(n.contentType))return!1;s.add(n.path)}return s.has(`/index.html`)}function l(e){if(typeof e!=`object`||!e||!i(e,[`manifest`,`publicKey`,`signature`]))return!1;let t=e;return c(t.manifest)&&typeof t.signature==`string`&&t.signature.length===86&&n.test(t.signature)&&typeof t.publicKey==`string`&&t.publicKey.length<=512&&n.test(t.publicKey)}function u(e,t){return e.assets.find(e=>e.path===t)}function d(e){let t=e.replaceAll(`-`,`+`).replaceAll(`_`,`/`),n=t.padEnd(Math.ceil(t.length/4)*4,`=`),r=atob(n),i=new Uint8Array(r.length);for(let e=0;e<r.length;e+=1)i[e]=r.charCodeAt(e);return i.buffer}function f(e){return[...new Uint8Array(e)].map(e=>e.toString(16).padStart(2,`0`)).join(``)}async function ee(e,t,n=0){if(!l(e))return{ok:!1,failure:{code:`invalid-envelope`}};let r=e;if(r.publicKey!==t)return{ok:!1,failure:{code:`untrusted-public-key`}};if(!Number.isSafeInteger(n)||n<0||r.manifest.revision<n)return{ok:!1,failure:{code:`stale-revision`,minimumRevision:n,actualRevision:r.manifest.revision}};let i;try{i=await crypto.subtle.importKey(`spki`,d(t),{name:`ECDSA`,namedCurve:`P-256`},!1,[`verify`])}catch{return{ok:!1,failure:{code:`invalid-public-key`}}}try{return await crypto.subtle.verify({name:`ECDSA`,hash:`SHA-256`},i,d(r.signature),new TextEncoder().encode(s(r.manifest)))?{ok:!0,manifest:r.manifest}:{ok:!1,failure:{code:`invalid-signature`}}}catch{return{ok:!1,failure:{code:`invalid-signature`}}}}async function p(e,t,n){let r=u(e,t);return r===void 0?{ok:!1,failure:{code:`asset-not-listed`,path:t}}:n.byteLength===r.byteLength?f(await crypto.subtle.digest(`SHA-256`,n))===r.sha256?{ok:!0}:{ok:!1,failure:{code:`digest-mismatch`}}:{ok:!1,failure:{code:`byte-length-mismatch`,expected:r.byteLength,actual:n.byteLength}}}var m=2**32,h=`doompi/sealed/c2s/v1`,g=`doompi/sealed/s2c/v1`;function _(e){return e===`c2s`?h:g}function v(e){if(typeof e!=`object`||!e)return!1;let t=e;return t.v===1&&typeof t.n==`string`&&typeof t.c==`string`}function y(e){switch(e){case`version`:return`The other end is speaking a different envelope version.`;case`malformed`:return`A sealed message was not shaped like one.`;case`replay`:return`A sealed message arrived out of order or twice.`;case`auth`:return`A sealed message failed its authentication tag, so it was altered in transit.`;case`exhausted`:return`This channel reached its message limit and must be re-established.`}}var b={name:`ECDH`,namedCurve:`P-256`},x=`AES-GCM`;function S(e){let t;try{t=atob(e.replaceAll(`-`,`+`).replaceAll(`_`,`/`))}catch{return}let n=new Uint8Array(new ArrayBuffer(t.length));for(let e=0;e<t.length;e+=1)n[e]=t.charCodeAt(e);return n}function C(e){let t=``;for(let n of e)t+=String.fromCharCode(n);return btoa(t).replaceAll(`+`,`-`).replaceAll(`/`,`_`).replaceAll(`=`,``)}function w(e){let t=new Uint8Array(new ArrayBuffer(e.length));return t.set(e),t.buffer}async function T(e,t){let n=await crypto.subtle.importKey(`raw`,e,`HKDF`,!1,[`deriveKey`]);return await crypto.subtle.deriveKey({name:`HKDF`,hash:`SHA-256`,salt:w(new TextEncoder().encode(`doompi/sealed/v1`)),info:w(new TextEncoder().encode(_(t)))},n,{name:x,length:256},!1,[`encrypt`,`decrypt`])}function E(e,t){let n=new Uint8Array(12);return n.set(e.subarray(0,4),0),new DataView(n.buffer).setBigUint64(4,t,!1),n}function D(e,t,n){let r=0n,i=-1n;return{async seal(t){if(r>=BigInt(m))return{ok:!1,failure:`exhausted`};let i=E(n,r),a=await crypto.subtle.encrypt({name:x,iv:w(i),tagLength:128},e,w(t));return r+=1n,{ok:!0,envelope:{v:1,n:C(i),c:C(new Uint8Array(a))}}},async open(e){let n=e;if(typeof n?.n!=`string`||typeof n.c!=`string`)return{ok:!1,failure:`malformed`};if(n.v!==1)return{ok:!1,failure:`version`};if(!v(e))return{ok:!1,failure:`malformed`};let r=S(e.n),a=S(e.c);if(r===void 0||a===void 0||r.length!==12||a.length<16)return{ok:!1,failure:`malformed`};let o=new DataView(w(r)).getBigUint64(4,!1);if(o<=i)return{ok:!1,failure:`replay`};let s;try{s=await crypto.subtle.decrypt({name:x,iv:w(r),tagLength:128},t,w(a))}catch{return{ok:!1,failure:`auth`}}return i=o,{ok:!0,plaintext:new Uint8Array(s)}}}}async function O(e){if(globalThis.crypto?.subtle!==void 0)try{let t=await crypto.subtle.generateKey(b,!1,[`deriveBits`]),n=S(e);if(n===void 0)return;let r=await crypto.subtle.importKey(`raw`,w(n),b,!1,[]),i=await crypto.subtle.deriveBits({name:`ECDH`,public:r},t.privateKey,256),a=C(new Uint8Array(await crypto.subtle.exportKey(`raw`,t.publicKey)));return{channel:D(await T(i,`c2s`),await T(i,`s2c`),new Uint8Array(4)),clientPublicKey:a}}catch{return}}function k(){let e=Promise.resolve();return{run(t){let n=e.then(t,t);return e=n.catch(()=>void 0),n}}}var A=new TextEncoder,j=new TextDecoder;function te(e){let t=``;for(let n of e)t+=String.fromCharCode(n);return btoa(t)}function ne(e){let t=atob(e);return Uint8Array.from(t,e=>e.charCodeAt(0))}async function re(e){if(e!=null){if(typeof e==`string`)return A.encode(e);if(e instanceof URLSearchParams)return A.encode(e.toString());if(e instanceof Blob)return new Uint8Array(await e.arrayBuffer());if(e instanceof ArrayBuffer)return new Uint8Array(e);if(ArrayBuffer.isView(e))return new Uint8Array(e.buffer,e.byteOffset,e.byteLength);throw Error(`The sealed HTTP transport does not support this request body type.`)}}function ie(e){if(!e.startsWith(`/`))throw Error(`A sealed HTTP request must use a root-relative URL.`);return e}function M(e){if(typeof e!=`object`||!e)return!1;let t=e;return t.v===1&&typeof t.status==`number`&&Array.isArray(t.headers)&&t.headers.every(e=>Array.isArray(e)&&e.length===2&&e.every(e=>typeof e==`string`))&&typeof t.body==`string`}function N(){let e,t,n=k(),r=k(),i=k(),a,o=e=>{t=y(e)},s=async t=>{if(e===void 0)return;let n=await e.seal(t);return n.ok?JSON.stringify(n.envelope):o(n.failure)},c=async t=>{if(e===void 0)return;let n;try{n=JSON.parse(t)}catch{return o(`malformed`)}let r=await e.open(n);return r.ok?r.plaintext:o(r.failure)};return{active:()=>e!==void 0,lastFailure:()=>t,async connect(n){let r=await O(n);if(r===void 0){t=`This browser could not establish a sealed channel.`;return}return e=r.channel,t=void 0,r.clientPublicKey},relayRequestsThrough(e){a=e},reset(){e=void 0,t=void 0},async sealText(r){return e===void 0?r:await n.run(async()=>{let e=await s(A.encode(r));if(e===void 0)throw Error(t??`The active sealed channel could not encrypt a message.`);return e})},async openText(t){return e===void 0?t:await r.run(async()=>{let e=await c(t);return e===void 0?void 0:j.decode(e)})},async sealBinary(r){return e===void 0?r:await n.run(async()=>{let e=await s(r);if(e===void 0)throw Error(t??`The active sealed channel could not encrypt a message.`);return A.encode(e)})},async openBinary(t){return e===void 0?t:await r.run(async()=>await c(j.decode(t)))},async fetch(n,r){let o=a;return e===void 0||o===void 0?await fetch(n,r):await i.run(async()=>{let e=await re(r?.body),i={v:1,method:(r?.method??`GET`).toUpperCase(),target:ie(n),headers:Array.from(new Headers(r?.headers).entries()),...e===void 0?{}:{body:te(e)}},a=await s(A.encode(JSON.stringify(i)));if(a===void 0)return new Response(JSON.stringify({error:t}),{status:502});let l=await fetch(o,{method:`POST`,credentials:`same-origin`,headers:{"content-type":`application/json`},body:a}),u=await c(await l.text());if(u===void 0)return new Response(JSON.stringify({error:t}),{status:502});let d;try{d=JSON.parse(j.decode(u))}catch{d=void 0}if(!M(d))return t=`The sealed HTTP response was malformed.`,new Response(JSON.stringify({error:t}),{status:502});let f=ne(d.body);return new Response(d.status===204||d.status===205||d.status===304?null:f,{status:d.status,headers:d.headers})})}}}N();var P=`doompi:bundle-updated`,F=`doompi-pwa`,I=`state`,L=`active-bundle`;function R(e){return new Promise((t,n)=>{e.addEventListener(`success`,()=>t(e.result),{once:!0}),e.addEventListener(`error`,()=>n(e.error??Error(`The PWA database request failed.`)),{once:!0})})}function z(e){return new Promise((t,n)=>{e.addEventListener(`complete`,()=>t(),{once:!0}),e.addEventListener(`abort`,()=>n(e.error??Error(`The PWA transaction aborted.`)),{once:!0}),e.addEventListener(`error`,()=>n(e.error??Error(`The PWA transaction failed.`)),{once:!0})})}async function B(){let e=indexedDB.open(F,1);return e.addEventListener(`upgradeneeded`,()=>{let t=e.result;t.objectStoreNames.contains(`state`)||t.createObjectStore(I)}),await R(e)}async function V(){let e=await B();try{let t=e.transaction(I,`readonly`),n=await R(t.objectStore(I).get(L));return await z(t),n}finally{e.close()}}async function H(e){let t=await B();try{let n=t.transaction(I,`readwrite`,{durability:`strict`});n.objectStore(I).put(e,L),await z(n)}finally{t.close()}}async function U(){let e=await B();try{let t=e.transaction(I,`readwrite`,{durability:`strict`});t.objectStore(I).delete(L),await z(t)}finally{e.close()}}var W=self,G=`/bundle-assets/`,K=`doompi-bundle-`,q=`doompi:activate-bundle`,J=`doompi:refresh-bundle`,Y=`doompi:reset-bundle-trust`;function X(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function ae(e){if(!(!X(e)||typeof e.type!=`string`)){if(e.type===J||e.type===Y)return{type:e.type};if(!(e.type!==q||typeof e.publicKey!=`string`||!Number.isSafeInteger(e.minimumRevision)||Number(e.minimumRevision)<1))return{type:q,publicKey:e.publicKey,minimumRevision:Number(e.minimumRevision)}}}function oe(e){return[...new Uint8Array(e)].map(e=>e.toString(16).padStart(2,`0`)).join(``)}async function se(e){let t=new TextEncoder().encode(s(e));return oe(await crypto.subtle.digest(`SHA-256`,t))}function ce(t){return t===`/pair`||t===`/sw.js`||t===`/manifest.webmanifest`||t.startsWith(`/pwa/`)||t.startsWith(`/api/`)||t===e||t.startsWith(G)}async function Z(e,t){let n=await caches.open(e.cacheName),r=new URL(t.url),i=t.mode===`navigate`?`/index.html`:r.pathname;return await n.match(i)??new Response(`That verified cockpit asset is unavailable.`,{status:404})}async function le(){let t=await fetch(e,{credentials:`include`,cache:`no-store`,redirect:`error`});if(!t.ok)throw Error(`The signed manifest request failed (${String(t.status)}).`);return await t.json()}async function Q(e,t){let n=await V();if(n!==void 0&&n.signerPublicKey!==e)return{ok:!1,code:`signer-mismatch`,message:`The host signing key does not match the pinned key.`};let r;try{r=await le()}catch(e){return{ok:!1,code:`manifest-fetch`,message:e instanceof Error?e.message:String(e)}}let i=Math.max(t,n?.revision??0),a=await ee(r,e,i);if(!a.ok)return{ok:!1,code:a.failure.code,message:`The signed bundle manifest was refused.`};let o=await se(a.manifest);if(n?.revision===a.manifest.revision){if(n.manifestDigest!==o)return{ok:!1,code:`revision-conflict`,message:`The host reused a revision for different bundle bytes.`};if(await caches.has(n.cacheName))return{ok:!0,revision:n.revision}}let s=`${K}${String(a.manifest.revision)}-${o.slice(0,16)}`;await caches.delete(s);let c=await caches.open(s);try{for(let e of a.manifest.assets){let t=`${G}${String(a.manifest.revision)}${e.path}`,n=await fetch(t,{credentials:`include`,cache:`no-store`,redirect:`error`});if(!n.ok||n.redirected||new URL(n.url).origin!==W.location.origin)throw Error(`The raw bundle asset ${e.path} was unavailable.`);let r=await n.arrayBuffer(),i=await p(a.manifest,e.path,r);if(!i.ok)throw Error(`The raw bundle asset ${e.path} failed ${i.failure.code}.`);await c.put(e.path,new Response(r,{status:200,headers:{"Cache-Control":`no-store`,"Content-Length":String(e.byteLength),"Content-Type":e.contentType,"X-Content-Type-Options":`nosniff`}}))}let t={signerPublicKey:e,manifestDigest:o,revision:a.manifest.revision,cacheName:s};await H(t);for(let e of await caches.keys())e.startsWith(K)&&e!==t.cacheName&&e!==n?.cacheName&&await caches.delete(e);return{ok:!0,revision:t.revision}}catch(e){return await caches.delete(s),{ok:!1,code:`asset-verification`,message:e instanceof Error?e.message:String(e)}}}async function ue(e){if(e.type===Y){for(let e of await caches.keys())e.startsWith(K)&&await caches.delete(e);return await U(),{ok:!0}}if(e.type===q)return await Q(e.publicKey,e.minimumRevision);let t=await V();return t===void 0?{ok:!1,code:`no-pin`,message:`No host signing key is pinned.`}:await Q(t.signerPublicKey,t.revision)}W.addEventListener(`install`,e=>{e.waitUntil(W.skipWaiting())}),W.addEventListener(`activate`,e=>{e.waitUntil(W.clients.claim())}),W.addEventListener(`message`,e=>{let t=ae(e.data),n=e.ports[0];if(n!==void 0){if(t===void 0){n.postMessage({ok:!1,code:`invalid-request`,message:`The worker request was malformed.`});return}e.waitUntil(ue(t).then(e=>n.postMessage(e)).catch(e=>n.postMessage({ok:!1,code:`worker-failure`,message:e instanceof Error?e.message:String(e)})))}});var $;async function de(e){let t=await W.clients.matchAll({type:`window`});for(let n of t)n.postMessage({type:P,revision:e})}function fe(e){return $??=(async()=>{try{let t=await Q(e.signerPublicKey,e.revision);t.ok&&t.revision!==void 0&&t.revision>e.revision&&await de(t.revision)}catch{}finally{$=void 0}})(),$}async function pe(){let e=await V();e!==void 0&&await fe(e)}W.addEventListener(`fetch`,e=>{let t=new URL(e.request.url);t.origin!==W.location.origin||ce(t.pathname)||(e.request.mode===`navigate`&&e.waitUntil(pe()),e.respondWith(V().then(async t=>t===void 0?await fetch(e.request):await Z(t,e.request))))}),W.addEventListener(`push`,e=>{let t=`A live session needs your attention.`;try{let n=e.data?.json();X(n)&&n.body===t&&(t=n.body)}catch{}e.waitUntil(W.registration.showNotification(`DoomPi`,{body:t,tag:`doompi-live`,icon:`/pwa/icon-192.png`,badge:`/pwa/icon-192.png`,data:{url:`/`}}))}),W.addEventListener(`notificationclick`,e=>{e.notification.close(),e.waitUntil(W.clients.matchAll({type:`window`,includeUncontrolled:!0}).then(async e=>{let t=e.find(e=>new URL(e.url).origin===W.location.origin);if(t!==void 0){await t.focus();return}await W.clients.openWindow(`/`)}))})})();
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../../_virtual/_rolldown/runtime.cjs");let t=require("node:fs");t=e.__toESM(t,1);let n=require("node:path");n=e.__toESM(n,1);let r=require("@agimon-ai/doompi-config"),i=require("node:child_process"),a=require("node:module"),o=require("node:os");o=e.__toESM(o,1);const s=[`dist`,`bin`,`cli.mjs`];function c(){let e=n.default.dirname((0,a.createRequire)(require("url").pathToFileURL(__filename).href).resolve(`@agimon-ai/doompi/package.json`));return n.default.join(e,...s)}function l(){return new Promise((e,t)=>{let n=(0,i.spawn)(process.execPath,[c(),`init`],{cwd:o.default.homedir(),stdio:[`ignore`,`inherit`,`inherit`]});n.once(`error`,e=>t(Error(`doompi init could not start: ${e.message}`,{cause:e}))),n.once(`exit`,n=>{n===0?e():t(Error(`doompi init exited with code ${String(n)}`))})})}async function u(e={}){let n=e.homeDirectory??o.default.homedir(),i=(0,r.globalDoomConfigDirectory)(n);return!(e.exists??t.default.existsSync)(i)&&(e.onNotice?.(`${i} is missing; running doompi init`),await(e.runInit??l)(),!0)}exports.ensureDoomInitialized=u;
|
|
2
|
+
//# sourceMappingURL=doomInitialization.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doomInitialization.cjs","names":["path","createRequire","spawn","os","globalDoomConfigDirectory","fs"],"sources":["../../../src/adapters/doomInitialization.ts"],"sourcesContent":["import { spawn } from 'node:child_process';\nimport fs from 'node:fs';\nimport { createRequire } from 'node:module';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { globalDoomConfigDirectory } from '@agimon-ai/doompi-config';\n\nconst DOOMPI_PACKAGE = '@agimon-ai/doompi';\nconst CLI_SEGMENTS = ['dist', 'bin', 'cli.mjs'];\n\nexport interface DoomInitializationOptions {\n homeDirectory?: string;\n onNotice?: (message: string) => void;\n /** Test seam over the global configuration directory check. */\n exists?: (directory: string) => boolean;\n /** Test seam over the child command. */\n runInit?: () => Promise<void>;\n}\n\nfunction bundledDoomPiCli(): string {\n const packageRoot = path.dirname(createRequire(import.meta.url).resolve(`${DOOMPI_PACKAGE}/package.json`));\n return path.join(packageRoot, ...CLI_SEGMENTS);\n}\n\nfunction runBundledInit(): Promise<void> {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [bundledDoomPiCli(), 'init'], {\n cwd: os.homedir(),\n stdio: ['ignore', 'inherit', 'inherit'],\n });\n child.once('error', (error) =>\n reject(new Error(`doompi init could not start: ${error.message}`, { cause: error })),\n );\n child.once('exit', (code) => {\n if (code === 0) resolve();\n else reject(new Error(`doompi init exited with code ${String(code)}`));\n });\n });\n}\n\n/** Ensures the global configuration and managed Pi integration exist before the web hub starts. */\nexport async function ensureDoomInitialized(options: DoomInitializationOptions = {}): Promise<boolean> {\n const homeDirectory = options.homeDirectory ?? os.homedir();\n const configDirectory = globalDoomConfigDirectory(homeDirectory);\n if ((options.exists ?? fs.existsSync)(configDirectory)) return false;\n options.onNotice?.(`${configDirectory} is missing; running doompi init`);\n await (options.runInit ?? runBundledInit)();\n return true;\n}\n"],"mappings":"uRAOA,MACM,EAAe,CAAC,OAAQ,MAAO,SAAS,EAW9C,SAAS,GAA2B,CAClC,IAAM,EAAcA,EAAAA,QAAK,SAAA,EAAQC,EAAAA,cAAAA,CAAAA,QAAAA,KAAAA,CAAAA,CAAAA,cAAAA,UAAAA,CAAAA,CAAAA,IAA6B,CAAC,CAAC,QAAQ,gCAAgC,CAAC,EACzG,OAAOD,EAAAA,QAAK,KAAK,EAAa,GAAG,CAAY,CAC/C,CAEA,SAAS,GAAgC,CACvC,OAAO,IAAI,SAAS,EAAS,IAAW,CACtC,IAAM,GAAA,EAAQE,EAAAA,MAAAA,CAAM,QAAQ,SAAU,CAAC,EAAiB,EAAG,MAAM,EAAG,CAClE,IAAKC,EAAAA,QAAG,QAAQ,EAChB,MAAO,CAAC,SAAU,UAAW,SAAS,CACxC,CAAC,EACD,EAAM,KAAK,QAAU,GACnB,EAAW,MAAM,gCAAgC,EAAM,UAAW,CAAE,MAAO,CAAM,CAAC,CAAC,CACrF,EACA,EAAM,KAAK,OAAS,GAAS,CACvB,IAAS,EAAG,EAAQ,EACnB,EAAW,MAAM,gCAAgC,OAAO,CAAI,GAAG,CAAC,CACvE,CAAC,CACH,CAAC,CACH,CAGA,eAAsB,EAAsB,EAAqC,CAAC,EAAqB,CACrG,IAAM,EAAgB,EAAQ,eAAiBA,EAAAA,QAAG,QAAQ,EACpD,GAAA,EAAkBC,EAAAA,0BAAAA,CAA0B,CAAa,EAI/D,MAHA,EAAK,EAAQ,QAAUC,EAAAA,QAAG,WAAA,CAAY,CAAe,IACrD,EAAQ,WAAW,GAAG,EAAgB,iCAAiC,EACvE,MAAO,EAAQ,SAAW,EAAA,CAAgB,EACnC,GACT"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{createRequire as e}from"node:module";import t from"node:fs";import n from"node:path";import{globalDoomConfigDirectory as r}from"@agimon-ai/doompi-config";import{spawn as i}from"node:child_process";import a from"node:os";const o=[`dist`,`bin`,`cli.mjs`];function s(){let t=n.dirname(e(import.meta.url).resolve(`@agimon-ai/doompi/package.json`));return n.join(t,...o)}function c(){return new Promise((e,t)=>{let n=i(process.execPath,[s(),`init`],{cwd:a.homedir(),stdio:[`ignore`,`inherit`,`inherit`]});n.once(`error`,e=>t(Error(`doompi init could not start: ${e.message}`,{cause:e}))),n.once(`exit`,n=>{n===0?e():t(Error(`doompi init exited with code ${String(n)}`))})})}async function l(e={}){let n=e.homeDirectory??a.homedir(),i=r(n);return!(e.exists??t.existsSync)(i)&&(e.onNotice?.(`${i} is missing; running doompi init`),await(e.runInit??c)(),!0)}export{l as ensureDoomInitialized};
|
|
2
|
+
//# sourceMappingURL=doomInitialization.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doomInitialization.mjs","names":[],"sources":["../../../src/adapters/doomInitialization.ts"],"sourcesContent":["import { spawn } from 'node:child_process';\nimport fs from 'node:fs';\nimport { createRequire } from 'node:module';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { globalDoomConfigDirectory } from '@agimon-ai/doompi-config';\n\nconst DOOMPI_PACKAGE = '@agimon-ai/doompi';\nconst CLI_SEGMENTS = ['dist', 'bin', 'cli.mjs'];\n\nexport interface DoomInitializationOptions {\n homeDirectory?: string;\n onNotice?: (message: string) => void;\n /** Test seam over the global configuration directory check. */\n exists?: (directory: string) => boolean;\n /** Test seam over the child command. */\n runInit?: () => Promise<void>;\n}\n\nfunction bundledDoomPiCli(): string {\n const packageRoot = path.dirname(createRequire(import.meta.url).resolve(`${DOOMPI_PACKAGE}/package.json`));\n return path.join(packageRoot, ...CLI_SEGMENTS);\n}\n\nfunction runBundledInit(): Promise<void> {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [bundledDoomPiCli(), 'init'], {\n cwd: os.homedir(),\n stdio: ['ignore', 'inherit', 'inherit'],\n });\n child.once('error', (error) =>\n reject(new Error(`doompi init could not start: ${error.message}`, { cause: error })),\n );\n child.once('exit', (code) => {\n if (code === 0) resolve();\n else reject(new Error(`doompi init exited with code ${String(code)}`));\n });\n });\n}\n\n/** Ensures the global configuration and managed Pi integration exist before the web hub starts. */\nexport async function ensureDoomInitialized(options: DoomInitializationOptions = {}): Promise<boolean> {\n const homeDirectory = options.homeDirectory ?? os.homedir();\n const configDirectory = globalDoomConfigDirectory(homeDirectory);\n if ((options.exists ?? fs.existsSync)(configDirectory)) return false;\n options.onNotice?.(`${configDirectory} is missing; running doompi init`);\n await (options.runInit ?? runBundledInit)();\n return true;\n}\n"],"mappings":"mOAOA,MACM,EAAe,CAAC,OAAQ,MAAO,SAAS,EAW9C,SAAS,GAA2B,CAClC,IAAM,EAAc,EAAK,QAAQ,EAAc,YAAY,GAAG,CAAC,CAAC,QAAQ,gCAAgC,CAAC,EACzG,OAAO,EAAK,KAAK,EAAa,GAAG,CAAY,CAC/C,CAEA,SAAS,GAAgC,CACvC,OAAO,IAAI,SAAS,EAAS,IAAW,CACtC,IAAM,EAAQ,EAAM,QAAQ,SAAU,CAAC,EAAiB,EAAG,MAAM,EAAG,CAClE,IAAK,EAAG,QAAQ,EAChB,MAAO,CAAC,SAAU,UAAW,SAAS,CACxC,CAAC,EACD,EAAM,KAAK,QAAU,GACnB,EAAW,MAAM,gCAAgC,EAAM,UAAW,CAAE,MAAO,CAAM,CAAC,CAAC,CACrF,EACA,EAAM,KAAK,OAAS,GAAS,CACvB,IAAS,EAAG,EAAQ,EACnB,EAAW,MAAM,gCAAgC,OAAO,CAAI,GAAG,CAAC,CACvE,CAAC,CACH,CAAC,CACH,CAGA,eAAsB,EAAsB,EAAqC,CAAC,EAAqB,CACrG,IAAM,EAAgB,EAAQ,eAAiB,EAAG,QAAQ,EACpD,EAAkB,EAA0B,CAAa,EAI/D,MAHA,EAAK,EAAQ,QAAU,EAAG,WAAA,CAAY,CAAe,IACrD,EAAQ,WAAW,GAAG,EAAgB,iCAAiC,EACvE,MAAO,EAAQ,SAAW,EAAA,CAAgB,EACnC,GACT"}
|
|
@@ -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"),_=require("./tunnelProcess.cjs"),le=require("./directoryListing.cjs"),v=require("./sessionFiles.cjs"),ue=require("./threadJournals.cjs"),de=require("./webHubPluginLoader.cjs"),y=require("./webTelemetry.cjs");let b=require("node:fs");b=e.__toESM(b,1);let x=require("node:path");x=e.__toESM(x,1);let S=require("node:url"),C=require("@agimon-ai/doompi/services"),
|
|
2
|
-
`,1)[0]?.trim();return(e.name?.trim()||n||x.default.basename(t)||`untitled`).slice(0,80)}function z(e){return e instanceof Error?e.message:String(e)}function B(e,t){return`${e}\n${t}`}function we(e,t,n,r){return p.createSessionHub({source:u.watchRegistry(e.registryDir,t),spawner:d.createServerSpawner({registryDir:e.registryDir,command:e.spawnCommand,onNotice:t}),readGit:l.readGitStatus,channels:n,telemetry:r,onNotice:t})}function V(e){return`repo-${(0,w.createHash)(`sha256`).update(e).digest(`base64url`).slice(0,24)}`}function H(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=b.default.realpathSync((0,j.findRepositoryRoot)(n.cwd));t.set(e,{repository:{id:V(e),path:e,name:x.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 Te(e,t,n,r,i){let a=[];for(let o of t){let t=`${O.DOOM_API_ROUTE_PREFIX}/${o.basePath}`,s;try{s=o.start({scope:`hub`,onNotice:n,resolveRepository:r,readRepositorySync:i})}catch(e){n(`hub API '${o.basePath}' did not start (${z(e)}); its routes stay unmounted`);continue}a.push(s),e.all(`${t}/*`,async e=>{if(e.req.query(`session`)!==void 0)return e.notFound();let r=new URL(e.req.url);r.pathname=r.pathname.slice(t.length)||`/`;try{return await s.fetch(new Request(r,e.req.raw))}catch(t){return n(`hub API '${o.basePath}' failed on ${r.pathname} (${z(t)})`),e.json({error:`The '${o.basePath}' API failed.`},500)}})}return a}function Ee(e,t,n,r){e.all(`${O.DOOM_API_ROUTE_PREFIX}/*`,async e=>{let i=e.req.query(s.API_SESSION_QUERY_PARAM);if(i===void 0)return e.notFound();let a=t.snapshot().find(e=>e.id===i);if(a===void 0)return e.json({error:`No session ${i}.`},404);if(a.apiSocketPath===void 0)return e.json({error:`Session ${i} serves no package API.`},404);let o=new URL(e.req.url);o.searchParams.delete(s.API_SESSION_QUERY_PARAM);let c=y.readTraceContext(e.req.raw.headers);try{return await r.runInSpan(`web.package_proxy`,{"operation.name":`web.package_proxy`,"http.method":e.req.method},async t=>await f.proxyToSocket({socketPath:a.apiSocketPath,path:`${o.pathname}${o.search}`,method:e.req.method,headers:e.req.raw.headers,body:e.req.raw.body,trace:y.forwardedTraceContext(t,c)}),c)}catch(t){return n(`session ${i} API is unreachable (${z(t)})`),e.json({error:`Session ${i} is not answering.`},502)}})}async function U(e){let l=e.host??`127.0.0.1`,u=e.onNotice??(()=>{}),d=y.createWebTelemetry(),f;try{f=(0,j.findRepositoryRoot)(process.cwd())}catch{f=void 0}let p=n.createSyncGuard({...f===void 0?{cwd:process.cwd()}:{repoRoot:f},onNotice:u});await p.ensureSynced();let b;if(f!==void 0)try{b=(0,C.readSyncRegistration)(f)}catch(e){u(`synced registration is unavailable (${z(e)})`)}let S={assetsDir:Se(e.assetsDir,b,u),apiDirectory:b?.apiDirectory},O=we(e,u,await de.loadHubChannels(S.assetsDir,u),d),N=ue.createThreadJournals({resolve:(e,t)=>O.threadJournal(e,t),onNotice:u}),P=new Set,F=(e,t)=>{for(let n of P)t&&!n.local||n.post(e)},I=new _e.Hono,V=(0,pe.createNodeWebSocket)({app:I}),U,W=se.createRemoteAccessStore({stateDir:e.remoteStateDir,onNotice:u}),G=xe(),K=ae.createBundlePublication({assetsDir:()=>S.assetsDir,stateDir:W.directory,onNotice:u}),q;_.reapStaleTunnel(W.directory,u);let De=e.onHandover===void 0?void 0:t=>{let n=g.planSessionMigration(O.snapshot().map(e=>({id:e.id,cwd:e.cwd,...e.name===void 0?{}:{name:e.name}})),t.sandbox.workspaces);for(let e of g.describeStranded(n.stranded))u(e);for(let e of n.migrate){let t=O.stop(e.id);t.ok||u(`could not stop ${e.name??e.id} before the move: ${t.error}`)}e.onHandover?.({settings:t,sessions:n.migrate})},J=ie.createRemoteAccess({store:W,launchTunnel:e.remoteAccess?.launchTunnel??_.createTunnelLauncher({stateDir:W.directory,...e.cloudflaredPath===void 0?{}:{cloudflaredPath:e.cloudflaredPath},onNotice:u,onExit:e=>{u(`remote access: ${e}`),J.disable()}}),bindListener:async()=>await new Promise((e,t)=>{let n=(0,fe.serve)({fetch:I.fetch,port:0,hostname:`127.0.0.1`,serverOptions:{maxHeaderSize:16384,headersTimeout:5e3,requestTimeout:1e4}},t=>{V.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:u,...De===void 0?{}:{requestHandover:De},contained:(0,A.insideSandbox)(process.env),broadcastLocal:e=>F(e,!0),broadcastAll:e=>F(e,!1),...e.remoteAccess?.now===void 0?{}:{now:e.remoteAccess.now}});q=oe.createLiveWebPush({stateDir:W.directory,isConnected:e=>J.isDeviceConnected(e),onNotice:u});let Oe=O.onEvent(e=>{e.kind===`frame`&&c.parseDoomNotificationEntry(e.frame)!==void 0&&q?.notify()}),Y=re.createRemoteGuard({loopbackPort:()=>U,tunnelPolicy:()=>J.tunnelPolicy(),authorize:e=>J.authorize((0,E.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,E.getCookie)(e,`doompi_device`,`host`)),o=a===void 0?`local`:`device:${a}`;return await J.passkeys().finishStepUp(r,o,t,i)}},extraOrigins:h.allowedOriginsFromEnv(process.env.DOOMPI_WEB_ALLOW_ORIGIN)});I.use(`*`,Y.middleware),I.use(`*`,async(e,t)=>{let n=new URL(e.req.url).pathname;if(n===m.REMOTE_HTTP_ROUTE)return t();let r=y.requestOperation(n);if(r===void 0)return t();let i=performance.now();return await d.runInSpan(r,{"operation.name":r,"http.method":e.req.method},async()=>{await t(),await d.recordEvent(`web.api.request`,{"operation.name":r,"http.method":e.req.method,"http.status_code":e.res.status,duration_ms:Math.max(0,Math.round(performance.now()-i))})},y.readTraceContext(e.req.raw.headers))}),I.use(`*`,async(e,t)=>{if(Y.listenerOf(e)===`local`)return t();let n=Number(e.req.header(`content-length`));return Number.isFinite(n)&&n>M?e.json({error:`The tunnel request body is too large.`},413):t()}),ce.registerRemoteRoutes(I,{remote:J,listenerOf:e=>Y.listenerOf(e)});let ke=(0,D.bodyLimit)({maxSize:M,onError:e=>e.json({error:`The tunnel request body is too large.`},413)});I.use(`*`,async(e,t)=>Y.listenerOf(e)===`local`?t():await ke(e,t));let Ae=(0,D.bodyLimit)({maxSize:8192,onError:e=>e.json({error:`The telemetry batch is too large.`},413)}),je=y.createBrowserTelemetryRateLimit();I.post(y.WEB_TELEMETRY_ROUTE,Ae,async e=>{if(!je())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=y.parseBrowserPerformanceBatch(t);if(n===void 0)return e.json({error:`The telemetry batch is invalid.`},400);for(let e of n)await d.recordEvent(e.name,{...e.duration_ms===void 0?{}:{duration_ms:e.duration_ms},...e.count===void 0?{}:{count:e.count}});return e.body(null,204)}),I.post(m.REMOTE_HTTP_ROUTE,async e=>{let t=J.authorize((0,E.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=ve(a),s=o===void 0?void 0:ye(o,e),c=o===void 0?void 0:be(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 I.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 Me=(0,D.bodyLimit)({maxSize:8192,onError:e=>e.json({error:`The Push subscription is too large.`},413)}),X=e=>e.env?.sealedDeviceId;I.get(m.REMOTE_PUSH_KEY_ROUTE,e=>X(e)===void 0?e.json({error:`A sealed paired device is required.`},401):e.json({publicKey:q?.publicKey()},200,{"Cache-Control":`no-store`})),I.post(m.REMOTE_PUSH_ROUTE,Me,async e=>{let t=X(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)}),I.delete(m.REMOTE_PUSH_ROUTE,e=>{let t=X(e);return t===void 0?e.json({error:`A sealed paired device is required.`},401):(q?.remove(t),e.body(null,204))});let Ne=r.createPiWebSocketListener({onError:e=>u(`protocol: ${e.message}`)}),Pe=r.createPiWebSocketListener({onError:e=>u(`protocol: ${e.message}`)}),Fe=t.createPiHubService({records:()=>O.records(),spawn:e=>O.create(e),onNotice:u}),Ie={...Fe,createSession:async()=>{throw new T.PiServerError(`not_implemented`,`Remote protocol clients cannot create sessions`)}},Z=new T.PiServer(Fe,{listeners:[Ne],onError:e=>u(`protocol: ${e.message}`)}),Le=new T.PiServer(Ie,{listeners:[Pe],onError:e=>u(`protocol: ${e.message}`)});await Promise.all([Z.start(),Le.start()]);let Q=ne.createProviderAuth({runtime:e.authRuntime,onNotice:u});ee.registerAuthRoutes(I,Q);let Re=H(O),ze=()=>Re.list(),Be=e=>Re.resolve(e),Ve=e=>{let t=Be(e);if(t===void 0)return;let n=(0,C.readSyncDrift)({repoRoot:t}),r;try{r=(0,C.readSyncRegistration)(t)?(0,he.readSyncState)(t):void 0}catch{r=void 0}return{fresh:n.fresh,reasons:[...n.reasons],...r===void 0?{}:{mcpProjection:r.fileState.mcpProjection}}};te.registerSettingsRoutes(I,{repositories:ze,models:()=>Q.listModels()});let He=S.apiDirectory,$=Te(I,He===void 0&&!process.env[k.PACKAGE_API_DIR_ENV]?[]:await(0,k.loadPackageApis)(`hub`,{apiDirectory:He,onNotice:u}),u,Be,Ve);Ee(I,O,u,d),I.get(`/api/health`,e=>e.json({ok:!0,role:`hub`,protocol:1,sessions:O.snapshot().length,pid:process.pid})),I.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(!R(t)||typeof t.cwd!=`string`||t.cwd===``)return e.json({error:`A cwd string is required.`},400);let n=typeof t.name==`string`&&t.name!==``?t.name:void 0;await p.ensureSynced();let r=await O.create({cwd:t.cwd,name:n,trace:y.readTraceContext(e.req.raw.headers)});return r.ok?e.json({sessionId:r.sessionId},201):e.json({error:r.error},r.code===`invalid_request`?400:502)}),I.post(`${s.SESSIONS_API_ROUTE}/:sessionId/restart`,async e=>{let t=e.req.param(`sessionId`);await p.ensureSynced();let n=await O.restart(t,y.readTraceContext(e.req.raw.headers));return n.ok?e.json({sessionId:n.sessionId},202):e.json({error:n.error},n.code===`invalid_request`?404:502)}),I.get(`${s.SESSIONS_API_ROUTE}/:sessionId/history`,async e=>{let t=e.req.param(`sessionId`),n=O.snapshot().find(e=>e.id===t);if(!n)return e.json({error:`Unknown session.`},404);let r=await me.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}))})}),I.post(`${s.SESSIONS_API_ROUTE}/:sessionId/resume`,async e=>{let t=e.req.param(`sessionId`),n=O.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(!R(r)||typeof r.targetSessionId!=`string`||r.targetSessionId===``)return e.json({error:`A targetSessionId string is required.`},400);let i=(await me.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 p.ensureSynced();let a=await O.resume(t,{sessionId:i.id,name:Ce(i,n.cwd)},y.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)}),I.delete(`${s.SESSIONS_API_ROUTE}/:sessionId`,e=>{let t=e.req.param(`sessionId`),n=O.stop(t);return n.ok?e.json({sessionId:t},202):e.json({error:n.error},n.code===`unknown`?404:n.code===`self`?409:502)}),I.get(`/api/sessions/:sessionId/files`,async e=>{let t=e.req.param(`sessionId`),n=O.snapshot().find(e=>e.id===t);if(!n)return e.json({error:`Unknown session.`},404);let r=e.req.query(`q`)??``,i=await v.listSessionFiles(n.cwd,r,20);return e.json({files:i})});let Ue=t=>{if(!(Y.listenerOf(t)===`local`||(0,A.insideSandbox)(process.env)))return e.browseRoot??process.cwd()};return I.get(s.DIRECTORIES_API_ROUTE,async e=>{let t=Ue(e),n=await le.suggestDirectories(e.req.query(`q`)??``,{limit:12,...t===void 0?{}:{root:t}});return e.json({directories:n})}),I.get(i.SESSION_FILE_ROUTE,async e=>{let t=e.req.param(`sessionId`),n=O.snapshot().find(e=>e.id===t);if(!n)return e.json({error:`Unknown session.`},404);let r=e.req.query(`path`)??``,o=await v.readSessionFile(n.cwd,r,i.MAX_SESSION_FILE_BYTES);return o.status===`forbidden`?e.json({error:`The path leaves the session directory.`},403):o.status===`not-found`?e.json({error:`No such file.`},404):o.status===`too-large`?e.json({error:`The file is too large to preview.`},413):e.body(new Uint8Array(o.body),200,a.sessionFileHeaders(r))}),I.get(m.SESSION_SOCKET_ROUTE,V.upgradeWebSocket(e=>{let t=Y.listenerOf(e)===`local`,n=t?void 0:J.authorize((0,E.getCookie)(e,m.DEVICE_COOKIE,`host`)),r=n===void 0?void 0:J.channelFor(n,`session`),i=new Set,a=(0,w.randomUUID)(),o=new Map,l,d,f,p,h=e=>{for(let[t,n]of o)(e===void 0||n.sessionId===e)&&(o.delete(t),N.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},P.add(f),n!==void 0&&(p=J.trackSocket(n,(e,t)=>m.close(e,t))),g(s.hubHello(O.channelTypes())),g({type:s.SESSIONS_SNAPSHOT_TYPE,sessions:O.snapshot()}),l=O.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))}),d=N.onFrame(e=>{o.has(B(e.sessionId,e.threadId))&&g(s.threadFrameEnvelope(e.sessionId,e.threadId,e.frame))}),u(`browser attached`)},onMessage(e){if(typeof e.data!=`string`)return;let n;try{n=JSON.parse(e.data)}catch{return}if(!t){if(r===void 0)return;let e=r.open(n);if(!e.ok)return;try{n=JSON.parse(new TextDecoder().decode(e.plaintext))}catch{return}}if(!R(n)||typeof n.sessionId!=`string`)return;let c=n.sessionId;if(typeof n.type==`string`&&O.channelTypes().includes(n.type)&&(i.has(c)||O.channelReceivesWithoutSubscription(n.type))){O.receiveChannel(c,n.type,n.payload,a);return}if(n.type===`subscribe`){let e=O.backlog(c);if(!e)return;i.add(c),f?.post(e);for(let e of O.channelFrames(c))f?.post(e);return}if(n.type===`unsubscribe`){i.delete(c);return}if(n.type===`history_request`){let e=O.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=B(c,e);if(n.type===`unsubscribe_thread`){o.delete(t)&&N.unsubscribe(c,e);return}if(o.has(t)||O.backlog(c)===void 0)return;o.set(t,{sessionId:c,threadId:e}),f?.post(s.threadBacklog(c,e,N.subscribe(c,e)));return}if(n.type===`session_command`&&R(n.frame)){if(n.frame.type===`attach`)return;O.command(c,n.frame)}},onClose(){O.disconnectChannels(a),f&&P.delete(f),f=void 0,p?.(),p=void 0,l?.(),l=void 0,d?.(),d=void 0,i.clear(),h(),u(`browser detached`)}}})),I.get(m.PROTOCOL_SOCKET_ROUTE,V.upgradeWebSocket(e=>{let t=Y.listenerOf(e)===`local`,n=t?void 0:J.authorize((0,E.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?Ne:Pe).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}}})),I.get(`/manifest.webmanifest`,e=>{let t=L(x.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`})}),I.get(`/sw.js`,e=>{let t=L(x.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":`/`})}),I.get(`/pwa/*`,e=>{let t=new URL(e.req.url).pathname.slice(5),n=o.resolveAssetPath(G,`/${t}`),r=n===void 0?void 0:L(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`})}),I.get(ge.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`})}),I.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,ge.assetFor)(i.signed.manifest,n[2]??``),s=a===void 0?void 0:o.resolveAssetPath(i.assetsDir,a.path),c=s===void 0?void 0:L(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`})}),I.get(`/`,e=>e.redirect(m.PAIRING_PAGE_ROUTE)),I.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)}),p.watch(()=>{let e;if(f!==void 0)try{e=(0,C.readSyncRegistration)(f)}catch(e){u(`the rebuilt registration is unavailable (${z(e)}); keeping the served bundle`)}e?.webDirectory!=null&&e.webDirectory!==S.assetsDir&&(S={assetsDir:e.webDirectory,apiDirectory:e.apiDirectory},u(`serving the rebuilt cockpit bundle from ${S.assetsDir}`)),K.refresh(),F({type:s.HUB_RESYNCED_TYPE},!1)}),new Promise((t,n)=>{let r=(0,fe.serve)({fetch:I.fetch,port:e.port,hostname:l},e=>{U=e.port,V.injectWebSocket(r);let n=`http://${l}:${e.port}`;u(`cockpit on ${n}`);let i,a=async()=>{await J.close(),await new Promise(e=>{N.close(),p.close(),Z.close(),Le.close(),Oe(),q?.close(),O.close(),Q.close();for(let e of $)e.close();for(let e of V.wss.clients)e.terminate();V.wss.close(),r.closeAllConnections?.(),r.close(()=>e())}),await y.shutdownWebTelemetry(d)};t({url:n,port:e.port,close:()=>i??=a()})});r.once(`error`,e=>{N.close(),Oe(),q?.close(),O.close(),Q.close(),J.close();for(let e of $)e.close();d.shutdown(),n(e)})})}exports.createSettingsRepositoryRegistry=H,exports.serveWeb=U;
|
|
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"),_=require("./tunnelProcess.cjs"),le=require("./directoryListing.cjs"),v=require("./sessionFiles.cjs"),ue=require("./threadJournals.cjs"),de=require("./webHubPluginLoader.cjs"),y=require("./webTelemetry.cjs"),fe=require("./webComposition.cjs");let b=require("node:fs");b=e.__toESM(b,1);let x=require("node:path");x=e.__toESM(x,1);let S=require("node:url"),C=require("@agimon-ai/doompi/services"),pe=require("node:crypto"),me=require("@hono/node-server"),he=require("@hono/node-ws"),ge=require("@earendil-works/pi-coding-agent"),w=require("@earendil-works/pi-server"),_e=require("@agimon-ai/doompi-config"),ve=require("@agimon-ai/doompi/services/syncState"),ye=require("@agimon-ai/doompi-web-security"),be=require("hono"),T=require("hono/cookie"),E=require("hono/body-limit"),D=require("@agimon-ai/doompi-extension-contracts/package-api"),xe=require("@agimon-ai/doompi-extension-contracts/package-api-loader"),Se=require("@agimon-ai/doompi-extension-contracts/sandbox-harness"),O=require("@agimon-ai/doompi/utils/repository");const k=8388608,A=new Set([`GET`,`HEAD`,`POST`,`PUT`,`PATCH`,`DELETE`]),j=new Set([`connection`,`content-length`,`cookie`,`forwarded`,`host`,`origin`,`transfer-encoding`,`upgrade`,`x-forwarded-for`,`x-forwarded-host`,`x-forwarded-proto`]);function Ce(e){if(typeof e!=`object`||!e)return;let t=e;if(t.v!==1||typeof t.method!=`string`||!A.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 we(e,t){try{let n=new Headers;for(let[t,r]of e.headers){let e=t.toLowerCase();j.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 Te(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 M(e){let t=x.default.dirname((0,S.fileURLToPath)(require("url").pathToFileURL(__filename).href));for(;;){if(b.default.existsSync(x.default.join(t,`package.json`)))return x.default.join(t,`dist`,e);let n=x.default.dirname(t);if(n===t)throw Error(`doompi-web could not locate its own package root.`);t=n}}function N(){return M(`web`)}function Ee(){return M(`pwa`)}function De(){let e=x.default.dirname((0,S.fileURLToPath)(require("url").pathToFileURL(__filename).href));for(;;){let t=x.default.join(e,`package.json`);if(b.default.existsSync(t))try{let e=JSON.parse(b.default.readFileSync(t,`utf8`)),n=F(e)?e.version:void 0;return typeof n==`string`?n:`unknown`}catch{return`unknown`}let n=x.default.dirname(e);if(n===e)return`unknown`;e=n}}function Oe(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&&b.default.existsSync(x.default.join(i,`index.html`))?(n(`serving the synced cockpit bundle from ${i}`),i):N()}function P(e){try{return b.default.statSync(e).isFile()?b.default.readFileSync(e):void 0}catch{return}}function F(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function ke(e,t){let n=e.firstMessage.split(`
|
|
2
|
+
`,1)[0]?.trim();return(e.name?.trim()||n||x.default.basename(t)||`untitled`).slice(0,80)}function I(e){return e instanceof Error?e.message:String(e)}function Ae(e,t){return`${e}\n${t}`}function je(e,t,n,r){return p.createSessionHub({source:u.watchRegistry(e.registryDir,t),spawner:d.createServerSpawner({registryDir:e.registryDir,command:e.spawnCommand,onNotice:t}),readGit:l.readGitStatus,channels:n,telemetry:r,onNotice:t})}function L(e){return`repo-${(0,pe.createHash)(`sha256`).update(e).digest(`base64url`).slice(0,24)}`}function Me(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=b.default.realpathSync((0,O.findRepositoryRoot)(n.cwd));t.set(e,{repository:{id:L(e),path:e,name:x.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 Ne(e,t,n,r,i){let a=[];for(let o of t){let t=`${D.DOOM_API_ROUTE_PREFIX}/${o.basePath}`,s;try{s=o.start({scope:`hub`,onNotice:n,resolveRepository:r,readRepositorySync:i})}catch(e){n(`hub API '${o.basePath}' did not start (${I(e)}); its routes stay unmounted`);continue}a.push(s),e.all(`${t}/*`,async e=>{if(e.req.query(`session`)!==void 0)return e.notFound();let r=new URL(e.req.url);r.pathname=r.pathname.slice(t.length)||`/`;try{return await s.fetch(new Request(r,e.req.raw))}catch(t){return n(`hub API '${o.basePath}' failed on ${r.pathname} (${I(t)})`),e.json({error:`The '${o.basePath}' API failed.`},500)}})}return a}function Pe(e,t,n,r){e.all(`${D.DOOM_API_ROUTE_PREFIX}/*`,async e=>{let i=e.req.query(s.API_SESSION_QUERY_PARAM);if(i===void 0)return e.notFound();let a=t.snapshot().find(e=>e.id===i);if(a===void 0)return e.json({error:`No session ${i}.`},404);if(a.apiSocketPath===void 0)return e.json({error:`Session ${i} serves no package API.`},404);let o=new URL(e.req.url);o.searchParams.delete(s.API_SESSION_QUERY_PARAM);let c=y.readTraceContext(e.req.raw.headers);try{return await r.runInSpan(`web.package_proxy`,{"operation.name":`web.package_proxy`,"http.method":e.req.method},async t=>await f.proxyToSocket({socketPath:a.apiSocketPath,path:`${o.pathname}${o.search}`,method:e.req.method,headers:e.req.raw.headers,body:e.req.raw.body,trace:y.forwardedTraceContext(t,c)}),c)}catch(t){return n(`session ${i} API is unreachable (${I(t)})`),e.json({error:`Session ${i} is not answering.`},502)}})}async function R(e){let l=e.host??`127.0.0.1`,d=e.onNotice??(()=>{}),f=y.createWebTelemetry(),p=se.createRemoteAccessStore({stateDir:e.remoteStateDir,onNotice:d}),S=e=>{try{return b.default.realpathSync((0,O.findRepositoryRoot)(e))}catch{return}},D=e=>{let t=S(e);if(t!==void 0)return t;let n=(0,_e.globalDoomConfigDirectory)();try{return(0,C.readSyncRegistration)(n)?.root}catch(e){d(`global cockpit composition is unreadable (${I(e)})`);return}},A=e.compositionDir===void 0?void 0:S(e.compositionDir);if(e.compositionDir!==void 0&&A===void 0)throw Error(`Cockpit composition directory is not inside a repository: ${e.compositionDir}`);let j=u.readRegistryRecords(e.registryDir,d).map(e=>D(e.cwd)).filter(e=>e!==void 0),M=[...new Set(A===void 0?j:[A])].sort((e,t)=>e.localeCompare(t)),N=new Map;for(let e of M)N.set(e,n.createSyncGuard({repoRoot:e,onNotice:d}));await Promise.all([...N.values()].map(async e=>await e.ensureSynced()));let L=async()=>await fe.resolveWebComposition({repositoryRoots:M,stateDirectory:p.directory,onNotice:d}),R=await L(),z={assetsDir:Oe(e.assetsDir,R,d),apiDirectory:R?.apiDirectory},B=async e=>{let t=D(e);if(t===void 0)return;let r=N.get(t);if(r!==void 0){await r.ensureSynced();return}let i=n.createSyncGuard({repoRoot:t,onNotice:d});try{await i.ensureSynced()}finally{i.close()}},V=je(e,d,await de.loadHubChannels(z.assetsDir,d),f),H=ue.createThreadJournals({resolve:(e,t)=>V.threadJournal(e,t),onNotice:d}),U=new Set,W=(e,t)=>{for(let n of U)t&&!n.local||n.post(e)},G=new be.Hono,K=(0,he.createNodeWebSocket)({app:G}),Fe,q=Ee(),J=ae.createBundlePublication({assetsDir:()=>z.assetsDir,stateDir:p.directory,onNotice:d}),Y;_.reapStaleTunnel(p.directory,d);let Ie=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})},X=ie.createRemoteAccess({store:p,launchTunnel:e.remoteAccess?.launchTunnel??_.createTunnelLauncher({stateDir:p.directory,...e.cloudflaredPath===void 0?{}:{cloudflaredPath:e.cloudflaredPath},onNotice:d,onExit:e=>{d(`remote access: ${e}`),X.disable()}}),bindListener:async()=>await new Promise((e,t)=>{let n=(0,me.serve)({fetch:G.fetch,port:0,hostname:`127.0.0.1`,serverOptions:{maxHeaderSize:16384,headersTimeout:5e3,requestTimeout:1e4}},t=>{K.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:()=>J.trust(),onDeviceDropped:e=>Y?.remove(e),onNotice:d,...Ie===void 0?{}:{requestHandover:Ie},contained:(0,Se.insideSandbox)(process.env),broadcastLocal:e=>W(e,!0),broadcastAll:e=>W(e,!1),...e.remoteAccess?.now===void 0?{}:{now:e.remoteAccess.now}});Y=oe.createLiveWebPush({stateDir:p.directory,isConnected:e=>X.isDeviceConnected(e),onNotice:d});let Le=V.onEvent(e=>{e.kind===`frame`&&c.parseDoomNotificationEntry(e.frame)!==void 0&&Y?.notify()}),Z=re.createRemoteGuard({loopbackPort:()=>Fe,tunnelPolicy:()=>X.tunnelPolicy(),authorize:e=>X.authorize((0,T.getCookie)(e,m.DEVICE_COOKIE,`host`)),trustedDevice:e=>e.env?.sealedDeviceId,channelReady:(e,t)=>X.channelFor(e,t)!==void 0,stepUp:{required:e=>X.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??X.authorize((0,T.getCookie)(e,`doompi_device`,`host`)),o=a===void 0?`local`:`device:${a}`;return await X.passkeys().finishStepUp(r,o,t,i)}},extraOrigins:h.allowedOriginsFromEnv(process.env.DOOMPI_WEB_ALLOW_ORIGIN)});G.use(`*`,Z.middleware),G.use(`*`,async(e,t)=>{let n=new URL(e.req.url).pathname;if(n===m.REMOTE_HTTP_ROUTE)return t();let r=y.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))})},y.readTraceContext(e.req.raw.headers))}),G.use(`*`,async(e,t)=>{if(Z.listenerOf(e)===`local`)return t();let n=Number(e.req.header(`content-length`));return Number.isFinite(n)&&n>k?e.json({error:`The tunnel request body is too large.`},413):t()}),ce.registerRemoteRoutes(G,{remote:X,listenerOf:e=>Z.listenerOf(e)});let Re=(0,E.bodyLimit)({maxSize:k,onError:e=>e.json({error:`The tunnel request body is too large.`},413)});G.use(`*`,async(e,t)=>Z.listenerOf(e)===`local`?t():await Re(e,t));let ze=(0,E.bodyLimit)({maxSize:8192,onError:e=>e.json({error:`The telemetry batch is too large.`},413)}),Be=y.createBrowserTelemetryRateLimit();G.post(y.WEB_TELEMETRY_ROUTE,ze,async e=>{if(!Be())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=y.parseBrowserPerformanceBatch(t);if(n===void 0)return e.json({error:`The telemetry batch is invalid.`},400);for(let e of n)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)}),G.post(m.REMOTE_HTTP_ROUTE,async e=>{let t=X.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=X.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=Ce(a),s=o===void 0?void 0:we(o,e),c=o===void 0?void 0:Te(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 G.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 Ve=(0,E.bodyLimit)({maxSize:8192,onError:e=>e.json({error:`The Push subscription is too large.`},413)}),Q=e=>e.env?.sealedDeviceId;G.get(m.REMOTE_PUSH_KEY_ROUTE,e=>Q(e)===void 0?e.json({error:`A sealed paired device is required.`},401):e.json({publicKey:Y?.publicKey()},200,{"Cache-Control":`no-store`})),G.post(m.REMOTE_PUSH_ROUTE,Ve,async e=>{let t=Q(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 Y?.subscribe(t,n)===!0?e.body(null,204):e.json({error:`The Push subscription is invalid.`},400)}),G.delete(m.REMOTE_PUSH_ROUTE,e=>{let t=Q(e);return t===void 0?e.json({error:`A sealed paired device is required.`},401):(Y?.remove(t),e.body(null,204))});let He=r.createPiWebSocketListener({onError:e=>d(`protocol: ${e.message}`)}),Ue=r.createPiWebSocketListener({onError:e=>d(`protocol: ${e.message}`)}),We=t.createPiHubService({records:()=>V.records(),spawn:e=>V.create(e),onNotice:d}),Ge={...We,createSession:async()=>{throw new w.PiServerError(`not_implemented`,`Remote protocol clients cannot create sessions`)}},Ke=new w.PiServer(We,{listeners:[He],onError:e=>d(`protocol: ${e.message}`)}),qe=new w.PiServer(Ge,{listeners:[Ue],onError:e=>d(`protocol: ${e.message}`)});await Promise.all([Ke.start(),qe.start()]);let $=ne.createProviderAuth({runtime:e.authRuntime,onNotice:d});ee.registerAuthRoutes(G,$);let Je=Me(V),Ye=()=>Je.list(),Xe=e=>Je.resolve(e),Ze=e=>{let t=Xe(e);if(t===void 0)return;let n=(0,C.readSyncDrift)({repoRoot:t}),r;try{r=(0,C.readSyncRegistration)(t)?(0,ve.readSyncState)(t):void 0}catch{r=void 0}return{fresh:n.fresh,reasons:[...n.reasons],...r===void 0?{}:{mcpProjection:r.fileState.mcpProjection}}};te.registerSettingsRoutes(G,{repositories:Ye,models:()=>$.listModels()});let Qe=z.apiDirectory,$e=Ne(G,Qe===void 0&&!process.env[xe.PACKAGE_API_DIR_ENV]?[]:await(0,xe.loadPackageApis)(`hub`,{apiDirectory:Qe,onNotice:d}),d,Xe,Ze);Pe(G,V,d,f),G.get(`/api/health`,e=>e.json({ok:!0,role:`hub`,protocol:1,sessions:V.snapshot().length,pid:process.pid,version:De()})),G.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(!F(t)||typeof t.cwd!=`string`||t.cwd===``)return e.json({error:`A cwd string is required.`},400);let n=typeof t.name==`string`&&t.name!==``?t.name:void 0;await B(t.cwd);let r=await V.create({cwd:t.cwd,name:n,trace:y.readTraceContext(e.req.raw.headers)});return r.ok?e.json({sessionId:r.sessionId},201):e.json({error:r.error},r.code===`invalid_request`?400:502)}),G.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 B(n.cwd);let r=await V.restart(t,y.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)}),G.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 ge.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}))})}),G.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(!F(r)||typeof r.targetSessionId!=`string`||r.targetSessionId===``)return e.json({error:`A targetSessionId string is required.`},400);let i=(await ge.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 B(n.cwd);let a=await V.resume(t,{sessionId:i.id,name:ke(i,n.cwd)},y.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)}),G.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)}),G.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 v.listSessionFiles(n.cwd,r,20);return e.json({files:i})});let et=t=>{if(!(Z.listenerOf(t)===`local`||(0,Se.insideSandbox)(process.env)))return e.browseRoot??process.cwd()};G.get(s.DIRECTORIES_API_ROUTE,async e=>{let t=et(e),n=await le.suggestDirectories(e.req.query(`q`)??``,{limit:12,...t===void 0?{}:{root:t}});return e.json({directories:n})}),G.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 v.readSessionFile(n.cwd,r,i.MAX_SESSION_FILE_BYTES);return o.status===`forbidden`?e.json({error:`The path leaves the session directory.`},403):o.status===`not-found`?e.json({error:`No such file.`},404):o.status===`too-large`?e.json({error:`The file is too large to preview.`},413):e.body(new Uint8Array(o.body),200,a.sessionFileHeaders(r))}),G.get(m.SESSION_SOCKET_ROUTE,K.upgradeWebSocket(e=>{let t=Z.listenerOf(e)===`local`,n=t?void 0:X.authorize((0,T.getCookie)(e,m.DEVICE_COOKIE,`host`)),r=n===void 0?void 0:X.channelFor(n,`session`),i=new Set,a=(0,pe.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=X.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(Ae(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(!F(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(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=Ae(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`&&F(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`)}}})),G.get(m.PROTOCOL_SOCKET_ROUTE,K.upgradeWebSocket(e=>{let t=Z.listenerOf(e)===`local`,n=t?void 0:X.authorize((0,T.getCookie)(e,m.DEVICE_COOKIE,`host`)),r=n===void 0?void 0:X.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=X.trackSocket(n,(e,t)=>o.close(e,t))),i=(t?He:Ue).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}}})),G.get(`/manifest.webmanifest`,e=>{let t=P(x.default.join(q,`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`})}),G.get(`/sw.js`,e=>{let t=P(x.default.join(q,`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":`/`})}),G.get(`/pwa/*`,e=>{let t=new URL(e.req.url).pathname.slice(5),n=o.resolveAssetPath(q,`/${t}`),r=n===void 0?void 0:P(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`})}),G.get(ye.BUNDLE_MANIFEST_ROUTE,e=>{let t=J.current();return t===void 0?e.json({error:`No signed cockpit bundle is available.`},503):e.json(t.signed,200,{"Cache-Control":`no-store`})}),G.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=J.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,ye.assetFor)(i.signed.manifest,n[2]??``),s=a===void 0?void 0:o.resolveAssetPath(i.assetsDir,a.path),c=s===void 0?void 0:P(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`})}),G.get(`/`,e=>e.redirect(m.PAIRING_PAGE_ROUTE)),G.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)});let tt=async()=>{try{let e=await L();e!==void 0&&e.webDirectory!==z.assetsDir&&(z={assetsDir:e.webDirectory,apiDirectory:e.apiDirectory},d(`serving the rebuilt cockpit bundle from ${z.assetsDir}`)),J.refresh(),W({type:s.HUB_RESYNCED_TYPE},!1)}catch(e){d(`the rebuilt cockpit composition is unavailable (${I(e)}); keeping the served bundle`)}};for(let e of N.values())e.watch(()=>void tt());return new Promise((t,n)=>{let r=(0,me.serve)({fetch:G.fetch,port:e.port,hostname:l},e=>{Fe=e.port,K.injectWebSocket(r);let n=`http://${l}:${e.port}`;d(`cockpit on ${n}`);let i,a=async()=>{await X.close(),await new Promise(e=>{H.close();for(let e of N.values())e.close();Ke.close(),qe.close(),Le(),Y?.close(),V.close(),$.close();for(let e of $e)e.close();for(let e of K.wss.clients)e.terminate();K.wss.close(),r.closeAllConnections?.(),r.close(()=>e())}),await y.shutdownWebTelemetry(f)};t({url:n,port:e.port,close:()=>i??=a()})});r.once(`error`,e=>{H.close(),Le(),Y?.close(),V.close(),$.close(),X.close();for(let e of $e)e.close();f.shutdown(),n(e)})})}exports.createSettingsRepositoryRegistry=Me,exports.packagedVersion=De,exports.serveWeb=R;
|
|
3
3
|
//# sourceMappingURL=httpServer.cjs.map
|