@agimon-ai/doompi-web 0.0.1-alpha.4 → 0.0.1-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +11 -8
  2. package/dist/src/adapters/bundlePublication.cjs +2 -1
  3. package/dist/src/adapters/bundlePublication.cjs.map +1 -1
  4. package/dist/src/adapters/bundlePublication.mjs +2 -1
  5. package/dist/src/adapters/bundlePublication.mjs.map +1 -1
  6. package/dist/src/adapters/httpServer.cjs +2 -2
  7. package/dist/src/adapters/httpServer.cjs.map +1 -1
  8. package/dist/src/adapters/httpServer.d.cts.map +1 -1
  9. package/dist/src/adapters/httpServer.d.mts.map +1 -1
  10. package/dist/src/adapters/httpServer.mjs +2 -2
  11. package/dist/src/adapters/httpServer.mjs.map +1 -1
  12. package/dist/src/adapters/serverSpawner.cjs +1 -1
  13. package/dist/src/adapters/serverSpawner.mjs +1 -1
  14. package/dist/src/adapters/sessionFiles.cjs +1 -1
  15. package/dist/src/adapters/sessionFiles.cjs.map +1 -1
  16. package/dist/src/adapters/sessionFiles.mjs +1 -1
  17. package/dist/src/adapters/sessionFiles.mjs.map +1 -1
  18. package/dist/src/adapters/syncGuard.cjs +1 -1
  19. package/dist/src/adapters/syncGuard.cjs.map +1 -1
  20. package/dist/src/adapters/syncGuard.mjs +1 -1
  21. package/dist/src/adapters/syncGuard.mjs.map +1 -1
  22. package/dist/src/adapters/webPluginVite.cjs +1 -1
  23. package/dist/src/adapters/webPluginVite.cjs.map +1 -1
  24. package/dist/src/adapters/webPluginVite.mjs +1 -1
  25. package/dist/src/adapters/webPluginVite.mjs.map +1 -1
  26. package/dist/src/services/fileMatch.cjs +1 -1
  27. package/dist/src/services/fileMatch.cjs.map +1 -1
  28. package/dist/src/services/fileMatch.mjs +1 -1
  29. package/dist/src/services/fileMatch.mjs.map +1 -1
  30. package/dist/web/assets/{CodeEditorView-ZJ3fgCgP.js → CodeEditorView-CJoceEgU.js} +3 -3
  31. package/dist/web/assets/{CodeEditorView-ZJ3fgCgP.js.map → CodeEditorView-CJoceEgU.js.map} +1 -1
  32. package/dist/web/assets/{dist-BAbw37G4.js → dist--0dFFjHK.js} +2 -2
  33. package/dist/web/assets/{dist-BAbw37G4.js.map → dist--0dFFjHK.js.map} +1 -1
  34. package/dist/web/assets/{dist-4CpY4--O.js → dist-BEjTM7TM.js} +2 -2
  35. package/dist/web/assets/{dist-4CpY4--O.js.map → dist-BEjTM7TM.js.map} +1 -1
  36. package/dist/web/assets/{dist-DCwjnz7l.js → dist-Cc1jhITG.js} +2 -2
  37. package/dist/web/assets/{dist-DCwjnz7l.js.map → dist-Cc1jhITG.js.map} +1 -1
  38. package/dist/web/assets/{dist-Cj5gWlhe.js → dist-Cr0kQueR.js} +2 -2
  39. package/dist/web/assets/{dist-Cj5gWlhe.js.map → dist-Cr0kQueR.js.map} +1 -1
  40. package/dist/web/assets/{dist-B_rHH1XB.js → dist-Dh1ufhC2.js} +2 -2
  41. package/dist/web/assets/{dist-B_rHH1XB.js.map → dist-Dh1ufhC2.js.map} +1 -1
  42. package/dist/web/assets/{dist-BZRRewb_.js → dist-Dnjnn2hl.js} +1 -1
  43. package/dist/web/assets/{dist-BCw5Th7z.js → dist-DsQNQF8G.js} +2 -2
  44. package/dist/web/assets/{dist-BCw5Th7z.js.map → dist-DsQNQF8G.js.map} +1 -1
  45. package/dist/web/assets/{dist-DW9ZoBfU.js → dist-KD1Mlgxp.js} +2 -2
  46. package/dist/web/assets/{dist-DW9ZoBfU.js.map → dist-KD1Mlgxp.js.map} +1 -1
  47. package/dist/web/assets/{dist-C60KVFlU.js → dist-a_TARmvK.js} +2 -2
  48. package/dist/web/assets/{dist-C60KVFlU.js.map → dist-a_TARmvK.js.map} +1 -1
  49. package/dist/web/assets/{dist-BhGCoeQZ.js → dist-h0LM1rYV.js} +2 -2
  50. package/dist/web/assets/{dist-BhGCoeQZ.js.map → dist-h0LM1rYV.js.map} +1 -1
  51. package/dist/web/assets/index-Dg79RfHx.css +2 -0
  52. package/dist/web/assets/index-Dur2Nvah.js +101 -0
  53. package/dist/web/assets/index-Dur2Nvah.js.map +1 -0
  54. package/dist/web/index.html +2 -2
  55. package/package.json +15 -15
  56. package/src/adapters/bundlePublication.ts +88 -12
  57. package/src/adapters/httpServer.ts +81 -36
  58. package/src/adapters/sessionFiles.ts +26 -5
  59. package/src/adapters/syncGuard.ts +92 -33
  60. package/src/adapters/webPluginVite.ts +24 -9
  61. package/src/services/fileMatch.ts +5 -1
  62. package/src/web/app/Providers.tsx +3 -1
  63. package/src/web/features/session/Composer.tsx +48 -16
  64. package/src/web/features/session/MentionPreviews.tsx +5 -2
  65. package/src/web/features/session/MessageMarkdown.tsx +31 -0
  66. package/src/web/features/session/QueueSheet.tsx +17 -0
  67. package/src/web/features/session/ThreadView.tsx +15 -1
  68. package/src/web/features/session/Timeline.tsx +88 -33
  69. package/src/web/features/settings/ContributedSettings.tsx +26 -71
  70. package/src/web/features/settings/RepositoryWorkspace.tsx +4 -0
  71. package/src/web/lib/commands.ts +35 -0
  72. package/src/web/lib/composition.ts +53 -3
  73. package/src/web/lib/fileMentions.ts +7 -3
  74. package/src/web/lib/pluginRegistry.ts +16 -0
  75. package/src/web/lib/pluginSlotProps.ts +2 -2
  76. package/src/web/lib/protocolTimeline.ts +2 -2
  77. package/src/web/lib/sessionModel.ts +62 -9
  78. package/src/web/lib/settingsSections.ts +41 -6
  79. package/src/web/lib/threadRenderer.ts +4 -3
  80. package/src/web/routes/SettingsPage.tsx +8 -2
  81. package/src/web/stores/sessionStore.ts +46 -1
  82. package/dist/web/assets/index-DqvMDNrH.js +0 -99
  83. package/dist/web/assets/index-DqvMDNrH.js.map +0 -1
  84. package/dist/web/assets/index-Dx8DvA-4.css +0 -2
package/README.md CHANGED
@@ -368,14 +368,17 @@ package declares a `doompiWeb` block in its package.json naming a client entry (
368
368
  definition: tab, panel, badge, session data channels, tool renderers for the timeline cards of
369
369
  the tools the package registers, slots it opens for other plugins, fills into slots others open,
370
370
  and the host's own slot contributions) and an optional hub entry (`webHubChannels` plus its built
371
- dist). This package's own bundle carries no plugins; the full set is assembled on your machine:
372
- `doompi sync` discovers the installed composition's manifests, generates the import entries, and
373
- rebuilds the SPA through the `./bundler` subpath into `~/.doompi/web/current`, which the server
374
- prefers over the packaged bundle (`--assets` and `DOOMPI_WEB_DIST` override it). Server-only
375
- `webPlugins.server.json` sits beside the public `web/` directory, never inside the signed or served bundle.
376
- It names each plugin's built hub entry, imported lazily: a missing plugin package logs a notice and its tab
377
- `@agimon-ai/doompi-team` is the reference plugin; the workflows tab ships with
378
- `@agimon-ai/doompi-workflow`. Contracts come from `@agimon-ai/doompi-web-contracts`.
371
+ dist). This package's own bundle carries no plugins; the full set is assembled on your machine.
372
+ `doompi sync` discovers the installed composition's manifests, generates the import entries, and builds
373
+ the SPA through the `./bundler` subpath inside the repository/worktree's immutable sync generation.
374
+ The shared server uses the validated registration for its startup repository. `--assets` and
375
+ `DOOMPI_WEB_DIST` override registered assets, then packaged assets provide the fallback. Server-only
376
+ `webPlugins.server.json` sits beside the public `web/` directory, never inside the signed or served
377
+ bundle. It names each plugin's built hub entry, imported lazily. A missing plugin package logs a notice
378
+ and its tab is omitted. Session package API routes use the session `cwd` registration, while
379
+ `DOOMPI_API_DIR` remains the explicit API override. `@agimon-ai/doompi-team` is the reference plugin;
380
+ the workflows tab ships with `@agimon-ai/doompi-workflow`. Contracts come from
381
+ `@agimon-ai/doompi-web-contracts`.
379
382
 
380
383
  A tool call's timeline item belongs to the plugin that registers the tool: its `toolRenderers`
381
384
  entry names the tool (or claims it with `matches` when the name is only known at runtime) and
@@ -1,2 +1,3 @@
1
- const e=require("../../_virtual/_rolldown/runtime.cjs");let t=require("node:fs");t=e.__toESM(t,1);let n=require("@agimon-ai/doompi-web-security/node");function r(e){try{let n=t.default.statSync(e,{bigint:!0});return n.isDirectory()?`${String(n.dev)}:${String(n.ino)}:${String(n.mtimeNs)}`:void 0}catch{return}}function i(e){let t=e.onNotice??(()=>{}),i,a,o;try{i=(0,n.createBundleSigner)(e.stateDir,t),a=i.sign(e.assetsDir),a===void 0?t(`the cockpit bundle is empty and cannot be published`):o=r(e.assetsDir)}catch(e){t(`the cockpit bundle could not be signed: ${e instanceof Error?e.message:String(e)}`)}let s=()=>{if(i===void 0)return a;try{let n=i.refresh(e.assetsDir);if(n===void 0)return t(`the refreshed cockpit bundle is empty; keeping the last signed publication`),a;a=n,o=r(e.assetsDir)}catch(e){t(`the refreshed cockpit bundle was refused: ${e instanceof Error?e.message:String(e)}`)}return a},c=()=>{let t=r(e.assetsDir);t!==void 0&&t!==o&&s()};return{current:()=>(c(),a),trust:()=>(c(),a===void 0?void 0:{publicKey:a.publicKey,revision:a.manifest.revision}),refresh:s}}exports.createBundlePublication=i;
1
+ const e=require("../../_virtual/_rolldown/runtime.cjs");let t=require("node:fs");t=e.__toESM(t,1);let n=require("node:crypto"),r=require("@agimon-ai/doompi-web-security/node");const i=[];function a(e){let t=[...e.manifest.assets].sort((e,t)=>e.path.localeCompare(t.path)).map(e=>`${e.path}:${e.sha256}:${String(e.byteLength)}`).join(`
2
+ `);return(0,n.createHash)(`sha256`).update(t).digest(`hex`)}function o(e,t){let n=e.signed.manifest.revision,r=a(e.signed),o=i.find(e=>e.digest===r&&e.revision<n);o!==void 0&&t(`the cockpit bundle repeats content already published as revision ${String(o.revision)}; paired devices will accept it as revision ${String(n)}`),i.push({revision:n,digest:r}),i.length>32&&i.shift()}function s(e){try{let n=t.default.statSync(e,{bigint:!0});return n.isDirectory()?`${String(n.dev)}:${String(n.ino)}:${String(n.mtimeNs)}`:void 0}catch{return}}function c(e){let t=e.onNotice??(()=>{}),n,i,a,c=t=>{let n=e.assetsDir(),r=t(n);return r===void 0?void 0:{signed:r,assetsDir:n}};try{n=(0,r.createBundleSigner)(e.stateDir,t);let l=c(e=>n?.sign(e));l===void 0?t(`the cockpit bundle is empty and cannot be published`):(i=l,a=s(l.assetsDir),o(i,t))}catch(e){t(`the cockpit bundle could not be signed: ${e instanceof Error?e.message:String(e)}`)}let l=()=>{if(n===void 0)return i;try{let e=c(e=>n?.refresh(e));if(e===void 0)return t(`the refreshed cockpit bundle is empty; keeping the last signed publication`),i;i=e,a=s(e.assetsDir),o(i,t)}catch(e){t(`the refreshed cockpit bundle was refused: ${e instanceof Error?e.message:String(e)}`)}return i},u=()=>{let t=i===void 0?void 0:s(e.assetsDir());if(i!==void 0&&e.assetsDir()!==i.assetsDir){l();return}t!==void 0&&t!==a&&l()};return{current:()=>(u(),i),trust:()=>(u(),i===void 0?void 0:{publicKey:i.signed.publicKey,revision:i.signed.manifest.revision}),refresh:l}}exports.createBundlePublication=c;
2
3
  //# sourceMappingURL=bundlePublication.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"bundlePublication.cjs","names":["fs","createBundleSigner"],"sources":["../../../src/adapters/bundlePublication.ts"],"sourcesContent":["import fs from 'node:fs';\nimport type { SignedBundleManifest } from '@agimon-ai/doompi-web-security';\nimport { createBundleSigner, type BundleSigner } from '@agimon-ai/doompi-web-security/node';\n\nexport interface BundleTrustView {\n publicKey: string;\n revision: number;\n}\n\nexport interface BundlePublication {\n current(): SignedBundleManifest | undefined;\n trust(): BundleTrustView | undefined;\n refresh(): SignedBundleManifest | undefined;\n}\n\nexport interface BundlePublicationOptions {\n assetsDir: string;\n stateDir: string;\n onNotice?: (message: string) => void;\n}\n\nfunction assetRootIdentity(assetsDir: string): string | undefined {\n try {\n const stat = fs.statSync(assetsDir, { bigint: true });\n return stat.isDirectory() ? `${String(stat.dev)}:${String(stat.ino)}:${String(stat.mtimeNs)}` : undefined;\n } catch {\n return undefined;\n }\n}\n/** Holds the last complete signed publication while files at the synced path are replaced. */\nexport function createBundlePublication(options: BundlePublicationOptions): BundlePublication {\n const notice = options.onNotice ?? ((): void => {});\n let signer: BundleSigner | undefined;\n let published: SignedBundleManifest | undefined;\n let publishedRootIdentity: string | undefined;\n\n try {\n signer = createBundleSigner(options.stateDir, notice);\n published = signer.sign(options.assetsDir);\n if (published === undefined) notice('the cockpit bundle is empty and cannot be published');\n else publishedRootIdentity = assetRootIdentity(options.assetsDir);\n } catch (error) {\n notice(`the cockpit bundle could not be signed: ${error instanceof Error ? error.message : String(error)}`);\n }\n\n const refresh = (): SignedBundleManifest | undefined => {\n if (signer === undefined) return published;\n try {\n const next = signer.refresh(options.assetsDir);\n if (next === undefined) {\n notice('the refreshed cockpit bundle is empty; keeping the last signed publication');\n return published;\n }\n published = next;\n publishedRootIdentity = assetRootIdentity(options.assetsDir);\n } catch (error) {\n notice(`the refreshed cockpit bundle was refused: ${error instanceof Error ? error.message : String(error)}`);\n }\n return published;\n };\n\n const refreshAfterRootReplacement = (): void => {\n const currentRootIdentity = assetRootIdentity(options.assetsDir);\n if (currentRootIdentity !== undefined && currentRootIdentity !== publishedRootIdentity) refresh();\n };\n\n return {\n current: () => {\n refreshAfterRootReplacement();\n return published;\n },\n trust: () => {\n refreshAfterRootReplacement();\n return published === undefined\n ? undefined\n : { publicKey: published.publicKey, revision: published.manifest.revision };\n },\n refresh,\n };\n}\n"],"mappings":"uJAqBA,SAAS,EAAkB,EAAuC,CAChE,GAAI,CACF,IAAM,EAAOA,EAAAA,QAAG,SAAS,EAAW,CAAE,OAAQ,EAAK,CAAC,EACpD,OAAO,EAAK,YAAY,EAAI,GAAG,OAAO,EAAK,GAAG,EAAE,GAAG,OAAO,EAAK,GAAG,EAAE,GAAG,OAAO,EAAK,OAAO,IAAM,IAAA,EAClG,MAAQ,CACN,MACF,CACF,CAEA,SAAgB,EAAwB,EAAsD,CAC5F,IAAM,EAAS,EAAQ,eAAyB,CAAC,GAC7C,EACA,EACA,EAEJ,GAAI,CACF,GAAA,EAASC,EAAAA,mBAAAA,CAAmB,EAAQ,SAAU,CAAM,EACpD,EAAY,EAAO,KAAK,EAAQ,SAAS,EACrC,IAAc,IAAA,GAAW,EAAO,qDAAqD,EACpF,EAAwB,EAAkB,EAAQ,SAAS,CAClE,OAAS,EAAO,CACd,EAAO,2CAA2C,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,GAAG,CAC5G,CAEA,IAAM,MAAkD,CACtD,GAAI,IAAW,IAAA,GAAW,OAAO,EACjC,GAAI,CACF,IAAM,EAAO,EAAO,QAAQ,EAAQ,SAAS,EAC7C,GAAI,IAAS,IAAA,GAEX,OADA,EAAO,4EAA4E,EAC5E,EAET,EAAY,EACZ,EAAwB,EAAkB,EAAQ,SAAS,CAC7D,OAAS,EAAO,CACd,EAAO,6CAA6C,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,GAAG,CAC9G,CACA,OAAO,CACT,EAEM,MAA0C,CAC9C,IAAM,EAAsB,EAAkB,EAAQ,SAAS,EAC3D,IAAwB,IAAA,IAAa,IAAwB,GAAuB,EAAQ,CAClG,EAEA,MAAO,CACL,aACE,EAA4B,EACrB,GAET,WACE,EAA4B,EACrB,IAAc,IAAA,GACjB,IAAA,GACA,CAAE,UAAW,EAAU,UAAW,SAAU,EAAU,SAAS,QAAS,GAE9E,SACF,CACF"}
1
+ {"version":3,"file":"bundlePublication.cjs","names":["createHash","fs","createBundleSigner"],"sources":["../../../src/adapters/bundlePublication.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport fs from 'node:fs';\nimport type { SignedBundleManifest } from '@agimon-ai/doompi-web-security';\nimport { createBundleSigner, type BundleSigner } from '@agimon-ai/doompi-web-security/node';\n\nexport interface BundleTrustView {\n publicKey: string;\n revision: number;\n}\n\n/**\n * One signed manifest and the directory it was signed over.\n *\n * They travel together because a page verifies each asset it fetches against\n * the manifest it was given. If the manifest advanced to a new sync generation\n * while asset reads still resolved against the previous directory, every fetch\n * would fail its digest check and the bundle would be refused. Handing callers\n * one record removes the chance of pairing them wrongly.\n */\nexport interface PublishedBundle {\n signed: SignedBundleManifest;\n assetsDir: string;\n}\n\nexport interface BundlePublication {\n current(): PublishedBundle | undefined;\n trust(): BundleTrustView | undefined;\n refresh(): PublishedBundle | undefined;\n}\n\nexport interface BundlePublicationOptions {\n /** Read on each publication, so a new sync generation can be adopted in place. */\n assetsDir: () => string;\n stateDir: string;\n onNotice?: (message: string) => void;\n}\n\n/**\n * Content this signer has published before, newest last.\n *\n * The signer revision only ever increases, which stops a paired device being\n * handed a manifest it has already seen superseded. It does not stop the same\n * hub re-serving older cockpit code under a fresh, higher revision, which is\n * what checking out an earlier commit and syncing does. That is allowed, but it\n * should not be silent, so republished content is named in the log.\n *\n * Process-local and bounded: this is a report, not a control.\n */\nconst publishedContent: Array<{ revision: number; digest: string }> = [];\nconst PUBLICATION_HISTORY_LIMIT = 32;\n\n/**\n * Digest of what the bundle contains, ignoring which publication carried it.\n *\n * `revision` and `builtAt` change on every signing, so a digest over the whole\n * manifest would never repeat and could never spot a downgrade.\n */\nfunction assetSetDigest(manifest: SignedBundleManifest): string {\n const assets = [...manifest.manifest.assets]\n .sort((left, right) => left.path.localeCompare(right.path))\n .map((asset) => `${asset.path}:${asset.sha256}:${String(asset.byteLength)}`)\n .join('\\n');\n return createHash('sha256').update(assets).digest('hex');\n}\n\nfunction noteRepublishedContent(bundle: PublishedBundle, notice: (message: string) => void): void {\n const revision = bundle.signed.manifest.revision;\n const digest = assetSetDigest(bundle.signed);\n const earlier = publishedContent.find((entry) => entry.digest === digest && entry.revision < revision);\n if (earlier !== undefined) {\n notice(\n `the cockpit bundle repeats content already published as revision ${String(earlier.revision)}; ` +\n `paired devices will accept it as revision ${String(revision)}`,\n );\n }\n publishedContent.push({ revision, digest });\n if (publishedContent.length > PUBLICATION_HISTORY_LIMIT) publishedContent.shift();\n}\n\nfunction assetRootIdentity(assetsDir: string): string | undefined {\n try {\n const stat = fs.statSync(assetsDir, { bigint: true });\n return stat.isDirectory() ? `${String(stat.dev)}:${String(stat.ino)}:${String(stat.mtimeNs)}` : undefined;\n } catch {\n return undefined;\n }\n}\n/** Holds the last complete signed publication while files at the synced path are replaced. */\nexport function createBundlePublication(options: BundlePublicationOptions): BundlePublication {\n const notice = options.onNotice ?? ((): void => {});\n let signer: BundleSigner | undefined;\n let published: PublishedBundle | undefined;\n let publishedRootIdentity: string | undefined;\n\n const signCurrent = (sign: (assetsDir: string) => SignedBundleManifest | undefined): PublishedBundle | undefined => {\n const assetsDir = options.assetsDir();\n const signed = sign(assetsDir);\n return signed === undefined ? undefined : { signed, assetsDir };\n };\n\n try {\n signer = createBundleSigner(options.stateDir, notice);\n const first = signCurrent((assetsDir) => signer?.sign(assetsDir));\n if (first === undefined) notice('the cockpit bundle is empty and cannot be published');\n else {\n published = first;\n publishedRootIdentity = assetRootIdentity(first.assetsDir);\n noteRepublishedContent(published, notice);\n }\n } catch (error) {\n notice(`the cockpit bundle could not be signed: ${error instanceof Error ? error.message : String(error)}`);\n }\n\n const refresh = (): PublishedBundle | undefined => {\n if (signer === undefined) return published;\n try {\n const next = signCurrent((assetsDir) => signer?.refresh(assetsDir));\n if (next === undefined) {\n notice('the refreshed cockpit bundle is empty; keeping the last signed publication');\n return published;\n }\n published = next;\n publishedRootIdentity = assetRootIdentity(next.assetsDir);\n noteRepublishedContent(published, notice);\n } catch (error) {\n notice(`the refreshed cockpit bundle was refused: ${error instanceof Error ? error.message : String(error)}`);\n }\n return published;\n };\n\n const refreshAfterRootReplacement = (): void => {\n // A changed asset root means a new sync generation, or the same directory\n // replaced underneath; either way the last publication no longer describes\n // what would be served.\n const currentRootIdentity = published === undefined ? undefined : assetRootIdentity(options.assetsDir());\n if (published !== undefined && options.assetsDir() !== published.assetsDir) {\n refresh();\n return;\n }\n if (currentRootIdentity !== undefined && currentRootIdentity !== publishedRootIdentity) refresh();\n };\n\n return {\n current: () => {\n refreshAfterRootReplacement();\n return published;\n },\n trust: () => {\n refreshAfterRootReplacement();\n return published === undefined\n ? undefined\n : { publicKey: published.signed.publicKey, revision: published.signed.manifest.revision };\n },\n refresh,\n };\n}\n"],"mappings":"gLAgDA,MAAM,EAAgE,CAAC,EASvE,SAAS,EAAe,EAAwC,CAC9D,IAAM,EAAS,CAAC,GAAG,EAAS,SAAS,MAAM,CAAC,CACzC,MAAM,EAAM,IAAU,EAAK,KAAK,cAAc,EAAM,IAAI,CAAC,CAAC,CAC1D,IAAK,GAAU,GAAG,EAAM,KAAK,GAAG,EAAM,OAAO,GAAG,OAAO,EAAM,UAAU,GAAG,CAAC,CAC3E,KAAK;CAAI,EACZ,OAAA,EAAOA,EAAAA,WAAAA,CAAW,QAAQ,CAAC,CAAC,OAAO,CAAM,CAAC,CAAC,OAAO,KAAK,CACzD,CAEA,SAAS,EAAuB,EAAyB,EAAyC,CAChG,IAAM,EAAW,EAAO,OAAO,SAAS,SAClC,EAAS,EAAe,EAAO,MAAM,EACrC,EAAU,EAAiB,KAAM,GAAU,EAAM,SAAW,GAAU,EAAM,SAAW,CAAQ,EACjG,IAAY,IAAA,IACd,EACE,oEAAoE,OAAO,EAAQ,QAAQ,EAAE,8CAC9C,OAAO,CAAQ,GAChE,EAEF,EAAiB,KAAK,CAAE,WAAU,QAAO,CAAC,EACtC,EAAiB,OAAS,IAA2B,EAAiB,MAAM,CAClF,CAEA,SAAS,EAAkB,EAAuC,CAChE,GAAI,CACF,IAAM,EAAOC,EAAAA,QAAG,SAAS,EAAW,CAAE,OAAQ,EAAK,CAAC,EACpD,OAAO,EAAK,YAAY,EAAI,GAAG,OAAO,EAAK,GAAG,EAAE,GAAG,OAAO,EAAK,GAAG,EAAE,GAAG,OAAO,EAAK,OAAO,IAAM,IAAA,EAClG,MAAQ,CACN,MACF,CACF,CAEA,SAAgB,EAAwB,EAAsD,CAC5F,IAAM,EAAS,EAAQ,eAAyB,CAAC,GAC7C,EACA,EACA,EAEE,EAAe,GAA+F,CAClH,IAAM,EAAY,EAAQ,UAAU,EAC9B,EAAS,EAAK,CAAS,EAC7B,OAAO,IAAW,IAAA,GAAY,IAAA,GAAY,CAAE,SAAQ,WAAU,CAChE,EAEA,GAAI,CACF,GAAA,EAASC,EAAAA,mBAAAA,CAAmB,EAAQ,SAAU,CAAM,EACpD,IAAM,EAAQ,EAAa,GAAc,GAAQ,KAAK,CAAS,CAAC,EAC5D,IAAU,IAAA,GAAW,EAAO,qDAAqD,GAEnF,EAAY,EACZ,EAAwB,EAAkB,EAAM,SAAS,EACzD,EAAuB,EAAW,CAAM,EAE5C,OAAS,EAAO,CACd,EAAO,2CAA2C,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,GAAG,CAC5G,CAEA,IAAM,MAA6C,CACjD,GAAI,IAAW,IAAA,GAAW,OAAO,EACjC,GAAI,CACF,IAAM,EAAO,EAAa,GAAc,GAAQ,QAAQ,CAAS,CAAC,EAClE,GAAI,IAAS,IAAA,GAEX,OADA,EAAO,4EAA4E,EAC5E,EAET,EAAY,EACZ,EAAwB,EAAkB,EAAK,SAAS,EACxD,EAAuB,EAAW,CAAM,CAC1C,OAAS,EAAO,CACd,EAAO,6CAA6C,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,GAAG,CAC9G,CACA,OAAO,CACT,EAEM,MAA0C,CAI9C,IAAM,EAAsB,IAAc,IAAA,GAAY,IAAA,GAAY,EAAkB,EAAQ,UAAU,CAAC,EACvG,GAAI,IAAc,IAAA,IAAa,EAAQ,UAAU,IAAM,EAAU,UAAW,CAC1E,EAAQ,EACR,MACF,CACI,IAAwB,IAAA,IAAa,IAAwB,GAAuB,EAAQ,CAClG,EAEA,MAAO,CACL,aACE,EAA4B,EACrB,GAET,WACE,EAA4B,EACrB,IAAc,IAAA,GACjB,IAAA,GACA,CAAE,UAAW,EAAU,OAAO,UAAW,SAAU,EAAU,OAAO,SAAS,QAAS,GAE5F,SACF,CACF"}
@@ -1,2 +1,3 @@
1
- import e from"node:fs";import{createBundleSigner as t}from"@agimon-ai/doompi-web-security/node";function n(t){try{let n=e.statSync(t,{bigint:!0});return n.isDirectory()?`${String(n.dev)}:${String(n.ino)}:${String(n.mtimeNs)}`:void 0}catch{return}}function r(e){let r=e.onNotice??(()=>{}),i,a,o;try{i=t(e.stateDir,r),a=i.sign(e.assetsDir),a===void 0?r(`the cockpit bundle is empty and cannot be published`):o=n(e.assetsDir)}catch(e){r(`the cockpit bundle could not be signed: ${e instanceof Error?e.message:String(e)}`)}let s=()=>{if(i===void 0)return a;try{let t=i.refresh(e.assetsDir);if(t===void 0)return r(`the refreshed cockpit bundle is empty; keeping the last signed publication`),a;a=t,o=n(e.assetsDir)}catch(e){r(`the refreshed cockpit bundle was refused: ${e instanceof Error?e.message:String(e)}`)}return a},c=()=>{let t=n(e.assetsDir);t!==void 0&&t!==o&&s()};return{current:()=>(c(),a),trust:()=>(c(),a===void 0?void 0:{publicKey:a.publicKey,revision:a.manifest.revision}),refresh:s}}export{r as createBundlePublication};
1
+ import e from"node:fs";import{createHash as t}from"node:crypto";import{createBundleSigner as n}from"@agimon-ai/doompi-web-security/node";const r=[];function i(e){let n=[...e.manifest.assets].sort((e,t)=>e.path.localeCompare(t.path)).map(e=>`${e.path}:${e.sha256}:${String(e.byteLength)}`).join(`
2
+ `);return t(`sha256`).update(n).digest(`hex`)}function a(e,t){let n=e.signed.manifest.revision,a=i(e.signed),o=r.find(e=>e.digest===a&&e.revision<n);o!==void 0&&t(`the cockpit bundle repeats content already published as revision ${String(o.revision)}; paired devices will accept it as revision ${String(n)}`),r.push({revision:n,digest:a}),r.length>32&&r.shift()}function o(t){try{let n=e.statSync(t,{bigint:!0});return n.isDirectory()?`${String(n.dev)}:${String(n.ino)}:${String(n.mtimeNs)}`:void 0}catch{return}}function s(e){let t=e.onNotice??(()=>{}),r,i,s,c=t=>{let n=e.assetsDir(),r=t(n);return r===void 0?void 0:{signed:r,assetsDir:n}};try{r=n(e.stateDir,t);let l=c(e=>r?.sign(e));l===void 0?t(`the cockpit bundle is empty and cannot be published`):(i=l,s=o(l.assetsDir),a(i,t))}catch(e){t(`the cockpit bundle could not be signed: ${e instanceof Error?e.message:String(e)}`)}let l=()=>{if(r===void 0)return i;try{let e=c(e=>r?.refresh(e));if(e===void 0)return t(`the refreshed cockpit bundle is empty; keeping the last signed publication`),i;i=e,s=o(e.assetsDir),a(i,t)}catch(e){t(`the refreshed cockpit bundle was refused: ${e instanceof Error?e.message:String(e)}`)}return i},u=()=>{let t=i===void 0?void 0:o(e.assetsDir());if(i!==void 0&&e.assetsDir()!==i.assetsDir){l();return}t!==void 0&&t!==s&&l()};return{current:()=>(u(),i),trust:()=>(u(),i===void 0?void 0:{publicKey:i.signed.publicKey,revision:i.signed.manifest.revision}),refresh:l}}export{s as createBundlePublication};
2
3
  //# sourceMappingURL=bundlePublication.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"bundlePublication.mjs","names":[],"sources":["../../../src/adapters/bundlePublication.ts"],"sourcesContent":["import fs from 'node:fs';\nimport type { SignedBundleManifest } from '@agimon-ai/doompi-web-security';\nimport { createBundleSigner, type BundleSigner } from '@agimon-ai/doompi-web-security/node';\n\nexport interface BundleTrustView {\n publicKey: string;\n revision: number;\n}\n\nexport interface BundlePublication {\n current(): SignedBundleManifest | undefined;\n trust(): BundleTrustView | undefined;\n refresh(): SignedBundleManifest | undefined;\n}\n\nexport interface BundlePublicationOptions {\n assetsDir: string;\n stateDir: string;\n onNotice?: (message: string) => void;\n}\n\nfunction assetRootIdentity(assetsDir: string): string | undefined {\n try {\n const stat = fs.statSync(assetsDir, { bigint: true });\n return stat.isDirectory() ? `${String(stat.dev)}:${String(stat.ino)}:${String(stat.mtimeNs)}` : undefined;\n } catch {\n return undefined;\n }\n}\n/** Holds the last complete signed publication while files at the synced path are replaced. */\nexport function createBundlePublication(options: BundlePublicationOptions): BundlePublication {\n const notice = options.onNotice ?? ((): void => {});\n let signer: BundleSigner | undefined;\n let published: SignedBundleManifest | undefined;\n let publishedRootIdentity: string | undefined;\n\n try {\n signer = createBundleSigner(options.stateDir, notice);\n published = signer.sign(options.assetsDir);\n if (published === undefined) notice('the cockpit bundle is empty and cannot be published');\n else publishedRootIdentity = assetRootIdentity(options.assetsDir);\n } catch (error) {\n notice(`the cockpit bundle could not be signed: ${error instanceof Error ? error.message : String(error)}`);\n }\n\n const refresh = (): SignedBundleManifest | undefined => {\n if (signer === undefined) return published;\n try {\n const next = signer.refresh(options.assetsDir);\n if (next === undefined) {\n notice('the refreshed cockpit bundle is empty; keeping the last signed publication');\n return published;\n }\n published = next;\n publishedRootIdentity = assetRootIdentity(options.assetsDir);\n } catch (error) {\n notice(`the refreshed cockpit bundle was refused: ${error instanceof Error ? error.message : String(error)}`);\n }\n return published;\n };\n\n const refreshAfterRootReplacement = (): void => {\n const currentRootIdentity = assetRootIdentity(options.assetsDir);\n if (currentRootIdentity !== undefined && currentRootIdentity !== publishedRootIdentity) refresh();\n };\n\n return {\n current: () => {\n refreshAfterRootReplacement();\n return published;\n },\n trust: () => {\n refreshAfterRootReplacement();\n return published === undefined\n ? undefined\n : { publicKey: published.publicKey, revision: published.manifest.revision };\n },\n refresh,\n };\n}\n"],"mappings":"gGAqBA,SAAS,EAAkB,EAAuC,CAChE,GAAI,CACF,IAAM,EAAO,EAAG,SAAS,EAAW,CAAE,OAAQ,EAAK,CAAC,EACpD,OAAO,EAAK,YAAY,EAAI,GAAG,OAAO,EAAK,GAAG,EAAE,GAAG,OAAO,EAAK,GAAG,EAAE,GAAG,OAAO,EAAK,OAAO,IAAM,IAAA,EAClG,MAAQ,CACN,MACF,CACF,CAEA,SAAgB,EAAwB,EAAsD,CAC5F,IAAM,EAAS,EAAQ,eAAyB,CAAC,GAC7C,EACA,EACA,EAEJ,GAAI,CACF,EAAS,EAAmB,EAAQ,SAAU,CAAM,EACpD,EAAY,EAAO,KAAK,EAAQ,SAAS,EACrC,IAAc,IAAA,GAAW,EAAO,qDAAqD,EACpF,EAAwB,EAAkB,EAAQ,SAAS,CAClE,OAAS,EAAO,CACd,EAAO,2CAA2C,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,GAAG,CAC5G,CAEA,IAAM,MAAkD,CACtD,GAAI,IAAW,IAAA,GAAW,OAAO,EACjC,GAAI,CACF,IAAM,EAAO,EAAO,QAAQ,EAAQ,SAAS,EAC7C,GAAI,IAAS,IAAA,GAEX,OADA,EAAO,4EAA4E,EAC5E,EAET,EAAY,EACZ,EAAwB,EAAkB,EAAQ,SAAS,CAC7D,OAAS,EAAO,CACd,EAAO,6CAA6C,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,GAAG,CAC9G,CACA,OAAO,CACT,EAEM,MAA0C,CAC9C,IAAM,EAAsB,EAAkB,EAAQ,SAAS,EAC3D,IAAwB,IAAA,IAAa,IAAwB,GAAuB,EAAQ,CAClG,EAEA,MAAO,CACL,aACE,EAA4B,EACrB,GAET,WACE,EAA4B,EACrB,IAAc,IAAA,GACjB,IAAA,GACA,CAAE,UAAW,EAAU,UAAW,SAAU,EAAU,SAAS,QAAS,GAE9E,SACF,CACF"}
1
+ {"version":3,"file":"bundlePublication.mjs","names":[],"sources":["../../../src/adapters/bundlePublication.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport fs from 'node:fs';\nimport type { SignedBundleManifest } from '@agimon-ai/doompi-web-security';\nimport { createBundleSigner, type BundleSigner } from '@agimon-ai/doompi-web-security/node';\n\nexport interface BundleTrustView {\n publicKey: string;\n revision: number;\n}\n\n/**\n * One signed manifest and the directory it was signed over.\n *\n * They travel together because a page verifies each asset it fetches against\n * the manifest it was given. If the manifest advanced to a new sync generation\n * while asset reads still resolved against the previous directory, every fetch\n * would fail its digest check and the bundle would be refused. Handing callers\n * one record removes the chance of pairing them wrongly.\n */\nexport interface PublishedBundle {\n signed: SignedBundleManifest;\n assetsDir: string;\n}\n\nexport interface BundlePublication {\n current(): PublishedBundle | undefined;\n trust(): BundleTrustView | undefined;\n refresh(): PublishedBundle | undefined;\n}\n\nexport interface BundlePublicationOptions {\n /** Read on each publication, so a new sync generation can be adopted in place. */\n assetsDir: () => string;\n stateDir: string;\n onNotice?: (message: string) => void;\n}\n\n/**\n * Content this signer has published before, newest last.\n *\n * The signer revision only ever increases, which stops a paired device being\n * handed a manifest it has already seen superseded. It does not stop the same\n * hub re-serving older cockpit code under a fresh, higher revision, which is\n * what checking out an earlier commit and syncing does. That is allowed, but it\n * should not be silent, so republished content is named in the log.\n *\n * Process-local and bounded: this is a report, not a control.\n */\nconst publishedContent: Array<{ revision: number; digest: string }> = [];\nconst PUBLICATION_HISTORY_LIMIT = 32;\n\n/**\n * Digest of what the bundle contains, ignoring which publication carried it.\n *\n * `revision` and `builtAt` change on every signing, so a digest over the whole\n * manifest would never repeat and could never spot a downgrade.\n */\nfunction assetSetDigest(manifest: SignedBundleManifest): string {\n const assets = [...manifest.manifest.assets]\n .sort((left, right) => left.path.localeCompare(right.path))\n .map((asset) => `${asset.path}:${asset.sha256}:${String(asset.byteLength)}`)\n .join('\\n');\n return createHash('sha256').update(assets).digest('hex');\n}\n\nfunction noteRepublishedContent(bundle: PublishedBundle, notice: (message: string) => void): void {\n const revision = bundle.signed.manifest.revision;\n const digest = assetSetDigest(bundle.signed);\n const earlier = publishedContent.find((entry) => entry.digest === digest && entry.revision < revision);\n if (earlier !== undefined) {\n notice(\n `the cockpit bundle repeats content already published as revision ${String(earlier.revision)}; ` +\n `paired devices will accept it as revision ${String(revision)}`,\n );\n }\n publishedContent.push({ revision, digest });\n if (publishedContent.length > PUBLICATION_HISTORY_LIMIT) publishedContent.shift();\n}\n\nfunction assetRootIdentity(assetsDir: string): string | undefined {\n try {\n const stat = fs.statSync(assetsDir, { bigint: true });\n return stat.isDirectory() ? `${String(stat.dev)}:${String(stat.ino)}:${String(stat.mtimeNs)}` : undefined;\n } catch {\n return undefined;\n }\n}\n/** Holds the last complete signed publication while files at the synced path are replaced. */\nexport function createBundlePublication(options: BundlePublicationOptions): BundlePublication {\n const notice = options.onNotice ?? ((): void => {});\n let signer: BundleSigner | undefined;\n let published: PublishedBundle | undefined;\n let publishedRootIdentity: string | undefined;\n\n const signCurrent = (sign: (assetsDir: string) => SignedBundleManifest | undefined): PublishedBundle | undefined => {\n const assetsDir = options.assetsDir();\n const signed = sign(assetsDir);\n return signed === undefined ? undefined : { signed, assetsDir };\n };\n\n try {\n signer = createBundleSigner(options.stateDir, notice);\n const first = signCurrent((assetsDir) => signer?.sign(assetsDir));\n if (first === undefined) notice('the cockpit bundle is empty and cannot be published');\n else {\n published = first;\n publishedRootIdentity = assetRootIdentity(first.assetsDir);\n noteRepublishedContent(published, notice);\n }\n } catch (error) {\n notice(`the cockpit bundle could not be signed: ${error instanceof Error ? error.message : String(error)}`);\n }\n\n const refresh = (): PublishedBundle | undefined => {\n if (signer === undefined) return published;\n try {\n const next = signCurrent((assetsDir) => signer?.refresh(assetsDir));\n if (next === undefined) {\n notice('the refreshed cockpit bundle is empty; keeping the last signed publication');\n return published;\n }\n published = next;\n publishedRootIdentity = assetRootIdentity(next.assetsDir);\n noteRepublishedContent(published, notice);\n } catch (error) {\n notice(`the refreshed cockpit bundle was refused: ${error instanceof Error ? error.message : String(error)}`);\n }\n return published;\n };\n\n const refreshAfterRootReplacement = (): void => {\n // A changed asset root means a new sync generation, or the same directory\n // replaced underneath; either way the last publication no longer describes\n // what would be served.\n const currentRootIdentity = published === undefined ? undefined : assetRootIdentity(options.assetsDir());\n if (published !== undefined && options.assetsDir() !== published.assetsDir) {\n refresh();\n return;\n }\n if (currentRootIdentity !== undefined && currentRootIdentity !== publishedRootIdentity) refresh();\n };\n\n return {\n current: () => {\n refreshAfterRootReplacement();\n return published;\n },\n trust: () => {\n refreshAfterRootReplacement();\n return published === undefined\n ? undefined\n : { publicKey: published.signed.publicKey, revision: published.signed.manifest.revision };\n },\n refresh,\n };\n}\n"],"mappings":"yIAgDA,MAAM,EAAgE,CAAC,EASvE,SAAS,EAAe,EAAwC,CAC9D,IAAM,EAAS,CAAC,GAAG,EAAS,SAAS,MAAM,CAAC,CACzC,MAAM,EAAM,IAAU,EAAK,KAAK,cAAc,EAAM,IAAI,CAAC,CAAC,CAC1D,IAAK,GAAU,GAAG,EAAM,KAAK,GAAG,EAAM,OAAO,GAAG,OAAO,EAAM,UAAU,GAAG,CAAC,CAC3E,KAAK;CAAI,EACZ,OAAO,EAAW,QAAQ,CAAC,CAAC,OAAO,CAAM,CAAC,CAAC,OAAO,KAAK,CACzD,CAEA,SAAS,EAAuB,EAAyB,EAAyC,CAChG,IAAM,EAAW,EAAO,OAAO,SAAS,SAClC,EAAS,EAAe,EAAO,MAAM,EACrC,EAAU,EAAiB,KAAM,GAAU,EAAM,SAAW,GAAU,EAAM,SAAW,CAAQ,EACjG,IAAY,IAAA,IACd,EACE,oEAAoE,OAAO,EAAQ,QAAQ,EAAE,8CAC9C,OAAO,CAAQ,GAChE,EAEF,EAAiB,KAAK,CAAE,WAAU,QAAO,CAAC,EACtC,EAAiB,OAAS,IAA2B,EAAiB,MAAM,CAClF,CAEA,SAAS,EAAkB,EAAuC,CAChE,GAAI,CACF,IAAM,EAAO,EAAG,SAAS,EAAW,CAAE,OAAQ,EAAK,CAAC,EACpD,OAAO,EAAK,YAAY,EAAI,GAAG,OAAO,EAAK,GAAG,EAAE,GAAG,OAAO,EAAK,GAAG,EAAE,GAAG,OAAO,EAAK,OAAO,IAAM,IAAA,EAClG,MAAQ,CACN,MACF,CACF,CAEA,SAAgB,EAAwB,EAAsD,CAC5F,IAAM,EAAS,EAAQ,eAAyB,CAAC,GAC7C,EACA,EACA,EAEE,EAAe,GAA+F,CAClH,IAAM,EAAY,EAAQ,UAAU,EAC9B,EAAS,EAAK,CAAS,EAC7B,OAAO,IAAW,IAAA,GAAY,IAAA,GAAY,CAAE,SAAQ,WAAU,CAChE,EAEA,GAAI,CACF,EAAS,EAAmB,EAAQ,SAAU,CAAM,EACpD,IAAM,EAAQ,EAAa,GAAc,GAAQ,KAAK,CAAS,CAAC,EAC5D,IAAU,IAAA,GAAW,EAAO,qDAAqD,GAEnF,EAAY,EACZ,EAAwB,EAAkB,EAAM,SAAS,EACzD,EAAuB,EAAW,CAAM,EAE5C,OAAS,EAAO,CACd,EAAO,2CAA2C,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,GAAG,CAC5G,CAEA,IAAM,MAA6C,CACjD,GAAI,IAAW,IAAA,GAAW,OAAO,EACjC,GAAI,CACF,IAAM,EAAO,EAAa,GAAc,GAAQ,QAAQ,CAAS,CAAC,EAClE,GAAI,IAAS,IAAA,GAEX,OADA,EAAO,4EAA4E,EAC5E,EAET,EAAY,EACZ,EAAwB,EAAkB,EAAK,SAAS,EACxD,EAAuB,EAAW,CAAM,CAC1C,OAAS,EAAO,CACd,EAAO,6CAA6C,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,GAAG,CAC9G,CACA,OAAO,CACT,EAEM,MAA0C,CAI9C,IAAM,EAAsB,IAAc,IAAA,GAAY,IAAA,GAAY,EAAkB,EAAQ,UAAU,CAAC,EACvG,GAAI,IAAc,IAAA,IAAa,EAAQ,UAAU,IAAM,EAAU,UAAW,CAC1E,EAAQ,EACR,MACF,CACI,IAAwB,IAAA,IAAa,IAAwB,GAAuB,EAAQ,CAClG,EAEA,MAAO,CACL,aACE,EAA4B,EACrB,GAET,WACE,EAA4B,EACrB,IAAc,IAAA,GACjB,IAAA,GACA,CAAE,UAAW,EAAU,OAAO,UAAW,SAAU,EAAU,OAAO,SAAS,QAAS,GAE5F,SACF,CACF"}
@@ -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"),_=require("./authRoutes.cjs"),ee=require("./settingsRoutes.cjs"),te=require("./providerAuth.cjs"),ne=require("./remoteGuard.cjs"),re=require("./remoteAccess.cjs"),ie=require("./bundlePublication.cjs"),ae=require("./webPush.cjs"),oe=require("./remoteAccessStore.cjs"),se=require("./remoteRoutes.cjs"),v=require("./tunnelProcess.cjs"),ce=require("./directoryListing.cjs"),y=require("./sessionFiles.cjs"),le=require("./threadJournals.cjs"),ue=require("./webHubPluginLoader.cjs"),b=require("./webTelemetry.cjs");let x=require("node:fs");x=e.__toESM(x,1);let S=require("node:path");S=e.__toESM(S,1);let C=require("node:url"),w=require("node:crypto"),T=require("node:os");T=e.__toESM(T,1);let E=require("@hono/node-server"),de=require("@hono/node-ws"),D=require("@earendil-works/pi-coding-agent"),O=require("@earendil-works/pi-server"),fe=require("@agimon-ai/doompi/services"),pe=require("@agimon-ai/doompi/services/syncState"),k=require("@agimon-ai/doompi-web-security"),me=require("hono"),A=require("hono/cookie"),j=require("hono/body-limit"),M=require("@agimon-ai/doompi-extension-contracts/package-api"),he=require("@agimon-ai/doompi-extension-contracts/package-api-loader"),N=require("@agimon-ai/doompi-extension-contracts/sandbox-harness"),P=require("@agimon-ai/doompi/utils/repository");const F=[`.doompi`,`web`,`current`,`web`],I=8388608,L=new Set([`GET`,`HEAD`,`POST`,`PUT`,`PATCH`,`DELETE`]),R=new Set([`connection`,`content-length`,`cookie`,`forwarded`,`host`,`origin`,`transfer-encoding`,`upgrade`,`x-forwarded-for`,`x-forwarded-host`,`x-forwarded-proto`]);function ge(e){if(typeof e!=`object`||!e)return;let t=e;if(t.v!==1||typeof t.method!=`string`||!L.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 _e(e,t){try{let n=new Headers;for(let[t,r]of e.headers){let e=t.toLowerCase();R.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 ve(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 z(e){let t=S.default.dirname((0,C.fileURLToPath)(require("url").pathToFileURL(__filename).href));for(;;){if(x.default.existsSync(S.default.join(t,`package.json`)))return S.default.join(t,`dist`,e);let n=S.default.dirname(t);if(n===t)throw Error(`doompi-web could not locate its own package root.`);t=n}}function B(){return z(`web`)}function ye(){return z(`pwa`)}function be(e,t){if(e!==void 0)return e;let n=process.env.DOOMPI_WEB_DIST;if(n!==void 0&&n!==``)return n;let r=S.default.join(T.default.homedir(),...F);return x.default.existsSync(S.default.join(r,`index.html`))?(t(`serving the synced cockpit bundle from ${r}`),r):B()}function V(e){try{return x.default.statSync(e).isFile()?x.default.readFileSync(e):void 0}catch{return}}function H(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function xe(e,t){let n=e.firstMessage.split(`
2
- `,1)[0]?.trim();return(e.name?.trim()||n||S.default.basename(t)||`untitled`).slice(0,80)}function U(e){return e instanceof Error?e.message:String(e)}function Se(e,t){return`${e}\n${t}`}function Ce(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 W(e){return`repo-${(0,w.createHash)(`sha256`).update(e).digest(`base64url`).slice(0,24)}`}function G(e){let t=new Map,n=()=>{for(let[e,n]of t)t.set(e,{...n,repository:{...n.repository,active:!1}});for(let n of e.records())try{let e=x.default.realpathSync((0,P.findRepositoryRoot)(n.cwd));t.set(e,{repository:{id:W(e),path:e,name:S.default.basename(e)||e,active:!0},seenAt:Date.now()})}catch{}let n=[...t.entries()].filter(([,e])=>!e.repository.active).sort((e,t)=>t[1].seenAt-e[1].seenAt);for(let[e]of n.slice(12))t.delete(e);return[...t.values()].sort((e,t)=>Number(t.repository.active)-Number(e.repository.active)||t.seenAt-e.seenAt||e.repository.path.localeCompare(t.repository.path)).map(e=>e.repository)};return{list:n,resolve(e){return n().find(t=>t.id===e)?.path}}}function we(e,t,n,r,i){let a=[];for(let o of t){let t=`${M.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 (${U(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} (${U(t)})`),e.json({error:`The '${o.basePath}' API failed.`},500)}})}return a}function Te(e,t,n,r){e.all(`${M.DOOM_API_ROUTE_PREFIX}/*`,async e=>{let i=e.req.query(s.API_SESSION_QUERY_PARAM);if(i===void 0)return e.notFound();let a=t.snapshot().find(e=>e.id===i);if(a===void 0)return e.json({error:`No session ${i}.`},404);if(a.apiSocketPath===void 0)return e.json({error:`Session ${i} serves no package API.`},404);let o=new URL(e.req.url);o.searchParams.delete(s.API_SESSION_QUERY_PARAM);let c=b.readTraceContext(e.req.raw.headers);try{return await r.runInSpan(`web.package_proxy`,{"operation.name":`web.package_proxy`,"http.method":e.req.method},async t=>await f.proxyToSocket({socketPath:a.apiSocketPath,path:`${o.pathname}${o.search}`,method:e.req.method,headers:e.req.raw.headers,body:e.req.raw.body,trace:b.forwardedTraceContext(t,c)}),c)}catch(t){return n(`session ${i} API is unreachable (${U(t)})`),e.json({error:`Session ${i} is not answering.`},502)}})}async function K(e){let l=e.host??`127.0.0.1`,u=e.onNotice??(()=>{}),d=b.createWebTelemetry(),f=be(e.assetsDir,u),p=Ce(e,u,await ue.loadHubChannels(f,u),d),x=le.createThreadJournals({resolve:(e,t)=>p.threadJournal(e,t),onNotice:u}),C=n.createSyncGuard({cwd:process.cwd(),onNotice:u});await C.ensureSynced();let T=new Set,M=(e,t)=>{for(let n of T)t&&!n.local||n.post(e)},P=new me.Hono,F=(0,de.createNodeWebSocket)({app:P}),L,R=oe.createRemoteAccessStore({stateDir:e.remoteStateDir,onNotice:u}),z=ye(),B=ie.createBundlePublication({assetsDir:f,stateDir:R.directory,onNotice:u}),U;v.reapStaleTunnel(R.directory,u);let W=e.onHandover===void 0?void 0:t=>{let n=g.planSessionMigration(p.snapshot().map(e=>({id:e.id,cwd:e.cwd,...e.name===void 0?{}:{name:e.name}})),t.sandbox.workspaces);for(let e of g.describeStranded(n.stranded))u(e);for(let e of n.migrate){let t=p.stop(e.id);t.ok||u(`could not stop ${e.name??e.id} before the move: ${t.error}`)}e.onHandover?.({settings:t,sessions:n.migrate})},K=re.createRemoteAccess({store:R,launchTunnel:e.remoteAccess?.launchTunnel??v.createTunnelLauncher({stateDir:R.directory,...e.cloudflaredPath===void 0?{}:{cloudflaredPath:e.cloudflaredPath},onNotice:u,onExit:e=>{u(`remote access: ${e}`),K.disable()}}),bindListener:async()=>await new Promise((e,t)=>{let n=(0,E.serve)({fetch:P.fetch,port:0,hostname:`127.0.0.1`,serverOptions:{maxHeaderSize:16384,headersTimeout:5e3,requestTimeout:1e4}},t=>{F.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:()=>B.trust(),onDeviceDropped:e=>U?.remove(e),onNotice:u,...W===void 0?{}:{requestHandover:W},contained:(0,N.insideSandbox)(process.env),broadcastLocal:e=>M(e,!0),broadcastAll:e=>M(e,!1),...e.remoteAccess?.now===void 0?{}:{now:e.remoteAccess.now}});U=ae.createLiveWebPush({stateDir:R.directory,isConnected:e=>K.isDeviceConnected(e),onNotice:u});let q=p.onEvent(e=>{e.kind===`frame`&&c.parseDoomNotificationEntry(e.frame)!==void 0&&U?.notify()}),J=ne.createRemoteGuard({loopbackPort:()=>L,tunnelPolicy:()=>K.tunnelPolicy(),authorize:e=>K.authorize((0,A.getCookie)(e,m.DEVICE_COOKIE,`host`)),trustedDevice:e=>e.env?.sealedDeviceId,channelReady:(e,t)=>K.channelFor(e,t)!==void 0,stepUp:{required:e=>K.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??K.authorize((0,A.getCookie)(e,`doompi_device`,`host`)),o=a===void 0?`local`:`device:${a}`;return await K.passkeys().finishStepUp(r,o,t,i)}},extraOrigins:h.allowedOriginsFromEnv(process.env.DOOMPI_WEB_ALLOW_ORIGIN)});P.use(`*`,J.middleware),P.use(`*`,async(e,t)=>{let n=new URL(e.req.url).pathname;if(n===m.REMOTE_HTTP_ROUTE)return t();let r=b.requestOperation(n);if(r===void 0)return t();let i=performance.now();return await d.runInSpan(r,{"operation.name":r,"http.method":e.req.method},async()=>{await t(),await d.recordEvent(`web.api.request`,{"operation.name":r,"http.method":e.req.method,"http.status_code":e.res.status,duration_ms:Math.max(0,Math.round(performance.now()-i))})},b.readTraceContext(e.req.raw.headers))}),P.use(`*`,async(e,t)=>{if(J.listenerOf(e)===`local`)return t();let n=Number(e.req.header(`content-length`));return Number.isFinite(n)&&n>I?e.json({error:`The tunnel request body is too large.`},413):t()}),se.registerRemoteRoutes(P,{remote:K,listenerOf:e=>J.listenerOf(e)});let Ee=(0,j.bodyLimit)({maxSize:I,onError:e=>e.json({error:`The tunnel request body is too large.`},413)});P.use(`*`,async(e,t)=>J.listenerOf(e)===`local`?t():await Ee(e,t));let De=(0,j.bodyLimit)({maxSize:8192,onError:e=>e.json({error:`The telemetry batch is too large.`},413)}),Oe=b.createBrowserTelemetryRateLimit();P.post(b.WEB_TELEMETRY_ROUTE,De,async e=>{if(!Oe())return e.json({error:`Too many telemetry batches.`},429);let t;try{t=await e.req.json()}catch{return e.json({error:`The telemetry batch must be JSON.`},400)}let n=b.parseBrowserPerformanceBatch(t);if(n===void 0)return e.json({error:`The telemetry batch is invalid.`},400);for(let e of n)await d.recordEvent(e.name,{...e.duration_ms===void 0?{}:{duration_ms:e.duration_ms},...e.count===void 0?{}:{count:e.count}});return e.body(null,204)}),P.post(m.REMOTE_HTTP_ROUTE,async e=>{let t=K.authorize((0,A.getCookie)(e,m.DEVICE_COOKIE,`host`));if(t===void 0)return e.json({error:`This device is not paired.`},401);let n=K.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=ge(a),s=o===void 0?void 0:_e(o,e),c=o===void 0?void 0:ve(o.body);if(o===void 0||s===void 0||c===void 0)return e.json({error:`The sealed HTTP request was invalid.`},400);let l=new URL(o.target,new URL(e.req.url).origin),u=await P.request(l,{method:o.method,headers:s,...o.method===`GET`||o.method===`HEAD`?{}:{body:Uint8Array.from(c).buffer}},{...e.env,sealedDeviceId:t}),d=Buffer.from(await u.arrayBuffer()),f=n.seal(new TextEncoder().encode(JSON.stringify({v:1,status:u.status,headers:Array.from(u.headers.entries()),body:d.toString(`base64`)})));return f.ok?e.json(f.envelope):e.json({error:`The sealed response failed: ${f.failure}.`},503)});let ke=(0,j.bodyLimit)({maxSize:8192,onError:e=>e.json({error:`The Push subscription is too large.`},413)}),Y=e=>e.env?.sealedDeviceId;P.get(m.REMOTE_PUSH_KEY_ROUTE,e=>Y(e)===void 0?e.json({error:`A sealed paired device is required.`},401):e.json({publicKey:U?.publicKey()},200,{"Cache-Control":`no-store`})),P.post(m.REMOTE_PUSH_ROUTE,ke,async e=>{let t=Y(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 U?.subscribe(t,n)===!0?e.body(null,204):e.json({error:`The Push subscription is invalid.`},400)}),P.delete(m.REMOTE_PUSH_ROUTE,e=>{let t=Y(e);return t===void 0?e.json({error:`A sealed paired device is required.`},401):(U?.remove(t),e.body(null,204))});let X=r.createPiWebSocketListener({onError:e=>u(`protocol: ${e.message}`)}),Z=r.createPiWebSocketListener({onError:e=>u(`protocol: ${e.message}`)}),Q=t.createPiHubService({records:()=>p.records(),spawn:e=>p.create(e),onNotice:u}),Ae={...Q,createSession:async()=>{throw new O.PiServerError(`not_implemented`,`Remote protocol clients cannot create sessions`)}},je=new O.PiServer(Q,{listeners:[X],onError:e=>u(`protocol: ${e.message}`)}),Me=new O.PiServer(Ae,{listeners:[Z],onError:e=>u(`protocol: ${e.message}`)});await Promise.all([je.start(),Me.start()]);let $=te.createProviderAuth({runtime:e.authRuntime,onNotice:u});_.registerAuthRoutes(P,$);let Ne=G(p),Pe=()=>Ne.list(),Fe=e=>Ne.resolve(e),Ie=e=>{let t=Fe(e);if(t===void 0)return;let n=(0,fe.readSyncDrift)({repoRoot:t}),r;try{r=(0,pe.readSyncState)(t)}catch{r=void 0}return{fresh:n.fresh,reasons:[...n.reasons],...r===void 0?{}:{mcpProjection:r.fileState.mcpProjection}}};ee.registerSettingsRoutes(P,{repositories:Pe,models:()=>$.listModels()});let Le=we(P,await(0,he.loadPackageApis)(`hub`,{onNotice:u}),u,Fe,Ie);Te(P,p,u,d),P.get(`/api/health`,e=>e.json({ok:!0,role:`hub`,protocol:1,sessions:p.snapshot().length,pid:process.pid})),P.post(s.SESSIONS_API_ROUTE,async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:`The request body must be JSON.`},400)}if(!H(t)||typeof t.cwd!=`string`||t.cwd===``)return e.json({error:`A cwd string is required.`},400);let n=typeof t.name==`string`&&t.name!==``?t.name:void 0;await C.ensureSynced();let r=await p.create({cwd:t.cwd,name:n,trace:b.readTraceContext(e.req.raw.headers)});return r.ok?e.json({sessionId:r.sessionId},201):e.json({error:r.error},r.code===`invalid_request`?400:502)}),P.post(`${s.SESSIONS_API_ROUTE}/:sessionId/restart`,async e=>{let t=e.req.param(`sessionId`);await C.ensureSynced();let n=await p.restart(t,b.readTraceContext(e.req.raw.headers));return n.ok?e.json({sessionId:n.sessionId},202):e.json({error:n.error},n.code===`invalid_request`?404:502)}),P.get(`${s.SESSIONS_API_ROUTE}/:sessionId/history`,async e=>{let t=e.req.param(`sessionId`),n=p.snapshot().find(e=>e.id===t);if(!n)return e.json({error:`Unknown session.`},404);let r=await D.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}))})}),P.post(`${s.SESSIONS_API_ROUTE}/:sessionId/resume`,async e=>{let t=e.req.param(`sessionId`),n=p.snapshot().find(e=>e.id===t);if(!n)return e.json({error:`Unknown session.`},404);let r;try{r=await e.req.json()}catch{return e.json({error:`The request body must be JSON.`},400)}if(!H(r)||typeof r.targetSessionId!=`string`||r.targetSessionId===``)return e.json({error:`A targetSessionId string is required.`},400);let i=(await D.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 C.ensureSynced();let a=await p.resume(t,{sessionId:i.id,name:xe(i,n.cwd)},b.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)}),P.delete(`${s.SESSIONS_API_ROUTE}/:sessionId`,e=>{let t=e.req.param(`sessionId`),n=p.stop(t);return n.ok?e.json({sessionId:t},202):e.json({error:n.error},n.code===`unknown`?404:n.code===`self`?409:502)}),P.get(`/api/sessions/:sessionId/files`,async e=>{let t=e.req.param(`sessionId`),n=p.snapshot().find(e=>e.id===t);if(!n)return e.json({error:`Unknown session.`},404);let r=e.req.query(`q`)??``,i=await y.listSessionFiles(n.cwd,r,20);return e.json({files:i})});let Re=t=>{if(!(J.listenerOf(t)===`local`||(0,N.insideSandbox)(process.env)))return e.browseRoot??process.cwd()};return P.get(s.DIRECTORIES_API_ROUTE,async e=>{let t=Re(e),n=await ce.suggestDirectories(e.req.query(`q`)??``,{limit:12,...t===void 0?{}:{root:t}});return e.json({directories:n})}),P.get(i.SESSION_FILE_ROUTE,async e=>{let t=e.req.param(`sessionId`),n=p.snapshot().find(e=>e.id===t);if(!n)return e.json({error:`Unknown session.`},404);let r=e.req.query(`path`)??``,o=await y.readSessionFile(n.cwd,r,i.MAX_SESSION_FILE_BYTES);return o.status===`forbidden`?e.json({error:`The path leaves the session directory.`},403):o.status===`not-found`?e.json({error:`No such file.`},404):o.status===`too-large`?e.json({error:`The file is too large to preview.`},413):e.body(new Uint8Array(o.body),200,a.sessionFileHeaders(r))}),P.get(m.SESSION_SOCKET_ROUTE,F.upgradeWebSocket(e=>{let t=J.listenerOf(e)===`local`,n=t?void 0:K.authorize((0,A.getCookie)(e,m.DEVICE_COOKIE,`host`)),r=n===void 0?void 0:K.channelFor(n,`session`),i=new Set,a=(0,w.randomUUID)(),o=new Map,l,d,f,h,g=e=>{for(let[t,n]of o)(e===void 0||n.sessionId===e)&&(o.delete(t),x.unsubscribe(n.sessionId,n.threadId))};return{onOpen(e,m){if(!t&&r===void 0){m.close(1008,`sealed session channel required`);return}let _=e=>{let n=JSON.stringify(e),i=n;if(!t){if(r===void 0)return;let e=r.seal(new TextEncoder().encode(n));if(!e.ok){m.close(1008,`sealed session channel failed`);return}i=JSON.stringify(e.envelope)}try{m.send(i)}catch{}};f={post:_,local:t},T.add(f),n!==void 0&&(h=K.trackSocket(n,(e,t)=>m.close(e,t))),_(s.hubHello(p.channelTypes())),_({type:s.SESSIONS_SNAPSHOT_TYPE,sessions:p.snapshot()}),l=p.onEvent(e=>{e.kind===`upsert`?_({type:s.SESSION_UPSERT_TYPE,session:e.session}):e.kind===`removed`?(i.delete(e.sessionId),g(e.sessionId),_({type:s.SESSION_REMOVED_TYPE,sessionId:e.sessionId})):e.kind===`channel`?(e.connectionId===a||e.connectionId===void 0&&i.has(e.sessionId))&&_({type:e.frameType,sessionId:e.sessionId,payload:e.payload}):(c.parseDoomNotificationEntry(e.frame)!==void 0||i.has(e.sessionId))&&_(s.sessionFrameEnvelope(e.sessionId,e.frame))}),d=x.onFrame(e=>{o.has(Se(e.sessionId,e.threadId))&&_(s.threadFrameEnvelope(e.sessionId,e.threadId,e.frame))}),u(`browser attached`)},onMessage(e){if(typeof e.data!=`string`)return;let n;try{n=JSON.parse(e.data)}catch{return}if(!t){if(r===void 0)return;let e=r.open(n);if(!e.ok)return;try{n=JSON.parse(new TextDecoder().decode(e.plaintext))}catch{return}}if(!H(n)||typeof n.sessionId!=`string`)return;let c=n.sessionId;if(typeof n.type==`string`&&p.channelTypes().includes(n.type)&&(i.has(c)||p.channelReceivesWithoutSubscription(n.type))){p.receiveChannel(c,n.type,n.payload,a);return}if(n.type===`subscribe`){let e=p.backlog(c);if(!e)return;i.add(c),f?.post(e);for(let e of p.channelFrames(c))f?.post(e);return}if(n.type===`unsubscribe`){i.delete(c);return}if(n.type===`history_request`){let e=p.history(c,{...typeof n.before==`string`?{before:n.before}:{},...typeof n.limit==`number`?{limit:n.limit}:{}});if(!e)return;f?.post(e);return}if(n.type===`subscribe_thread`||n.type===`unsubscribe_thread`){if(typeof n.threadId!=`string`)return;let e=n.threadId,t=Se(c,e);if(n.type===`unsubscribe_thread`){o.delete(t)&&x.unsubscribe(c,e);return}if(o.has(t)||p.backlog(c)===void 0)return;o.set(t,{sessionId:c,threadId:e}),f?.post(s.threadBacklog(c,e,x.subscribe(c,e)));return}if(n.type===`session_command`&&H(n.frame)){if(n.frame.type===`attach`)return;p.command(c,n.frame)}},onClose(){p.disconnectChannels(a),f&&T.delete(f),f=void 0,h?.(),h=void 0,l?.(),l=void 0,d?.(),d=void 0,i.clear(),g(),u(`browser detached`)}}})),P.get(m.PROTOCOL_SOCKET_ROUTE,F.upgradeWebSocket(e=>{let t=J.listenerOf(e)===`local`,n=t?void 0:K.authorize((0,A.getCookie)(e,m.DEVICE_COOKIE,`host`)),r=n===void 0?void 0:K.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=K.trackSocket(n,(e,t)=>o.close(e,t))),i=(t?X:Z).accept({send:e=>{if(t){o.send(e);return}if(r===void 0)return;let n=e instanceof Uint8Array?e:new Uint8Array(e),i=r.seal(n);if(!i.ok){o.close(1008,`sealed protocol channel failed`);return}o.send(new TextEncoder().encode(JSON.stringify(i.envelope)))},close:()=>o.close(),get readyState(){return o.readyState}})},async onMessage(e){let n=e.data;if(typeof n==`string`)return;let a=n instanceof Blob?new Uint8Array(await n.arrayBuffer()):(n instanceof ArrayBuffer,new Uint8Array(n));if(t){i?.onData(a);return}if(r===void 0)return;let o;try{o=JSON.parse(new TextDecoder().decode(a))}catch{return}let s=r.open(o);s.ok&&i?.onData(s.plaintext)},onClose(){a?.(),a=void 0,i?.onClose(),i=void 0},onError(e){i?.onError(e instanceof Error?e:Error(`The protocol socket failed`)),i=void 0}}})),P.get(`/manifest.webmanifest`,e=>{let t=V(S.default.join(z,`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`})}),P.get(`/sw.js`,e=>{let t=V(S.default.join(z,`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":`/`})}),P.get(`/pwa/*`,e=>{let t=new URL(e.req.url).pathname.slice(5),n=o.resolveAssetPath(z,`/${t}`),r=n===void 0?void 0:V(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`})}),P.get(k.BUNDLE_MANIFEST_ROUTE,e=>{let t=B.current();return t===void 0?e.json({error:`No signed cockpit bundle is available.`},503):e.json(t,200,{"Cache-Control":`no-store`})}),P.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=B.current();if(n===null||!Number.isSafeInteger(r)||i===void 0||i.manifest.revision!==r)return e.text(`That signed bundle revision is unavailable.`,404);let a=(0,k.assetFor)(i.manifest,n[2]??``),s=a===void 0?void 0:o.resolveAssetPath(f,a.path),c=s===void 0?void 0:V(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`})}),P.get(`/`,e=>e.redirect(m.PAIRING_PAGE_ROUTE)),P.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)}),C.watch(()=>{B.refresh(),M({type:s.HUB_RESYNCED_TYPE},!1)}),new Promise((t,n)=>{let r=(0,E.serve)({fetch:P.fetch,port:e.port,hostname:l},e=>{L=e.port,F.injectWebSocket(r);let n=`http://${l}:${e.port}`;u(`cockpit on ${n}`);let i,a=async()=>{await K.close(),await new Promise(e=>{x.close(),C.close(),je.close(),Me.close(),q(),U?.close(),p.close(),$.close();for(let e of Le)e.close();for(let e of F.wss.clients)e.terminate();F.wss.close(),r.closeAllConnections?.(),r.close(()=>e())}),await b.shutdownWebTelemetry(d)};t({url:n,port:e.port,close:()=>i??=a()})});r.once(`error`,e=>{x.close(),q(),U?.close(),p.close(),$.close(),K.close();for(let e of Le)e.close();d.shutdown(),n(e)})})}exports.createSettingsRepositoryRegistry=G,exports.serveWeb=K;
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"),w=require("node:crypto"),fe=require("@hono/node-server"),pe=require("@hono/node-ws"),me=require("@earendil-works/pi-coding-agent"),T=require("@earendil-works/pi-server"),he=require("@agimon-ai/doompi/services/syncState"),ge=require("@agimon-ai/doompi-web-security"),_e=require("hono"),E=require("hono/cookie"),D=require("hono/body-limit"),O=require("@agimon-ai/doompi-extension-contracts/package-api"),k=require("@agimon-ai/doompi-extension-contracts/package-api-loader"),A=require("@agimon-ai/doompi-extension-contracts/sandbox-harness"),j=require("@agimon-ai/doompi/utils/repository");const M=8388608,N=new Set([`GET`,`HEAD`,`POST`,`PUT`,`PATCH`,`DELETE`]),P=new Set([`connection`,`content-length`,`cookie`,`forwarded`,`host`,`origin`,`transfer-encoding`,`upgrade`,`x-forwarded-for`,`x-forwarded-host`,`x-forwarded-proto`]);function ve(e){if(typeof e!=`object`||!e)return;let t=e;if(t.v!==1||typeof t.method!=`string`||!N.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 ye(e,t){try{let n=new Headers;for(let[t,r]of e.headers){let e=t.toLowerCase();P.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 be(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 F(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 I(){return F(`web`)}function xe(){return F(`pwa`)}function Se(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):I()}function L(e){try{return b.default.statSync(e).isFile()?b.default.readFileSync(e):void 0}catch{return}}function R(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function Ce(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 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;
3
3
  //# sourceMappingURL=httpServer.cjs.map