borgmcp 1.1.15 → 2.0.1
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/CONTRIBUTING.md +15 -0
- package/LICENSE +201 -33
- package/NOTICE +2 -0
- package/README.md +85 -36
- package/SECURITY.md +7 -0
- package/dist/agent-runtime.d.ts.map +1 -0
- package/dist/agent-runtime.js +63 -1
- package/dist/agent-runtime.js.map +1 -0
- package/dist/assimilate-cmd.d.ts +96 -17
- package/dist/assimilate-cmd.d.ts.map +1 -0
- package/dist/assimilate-cmd.js +1396 -48
- package/dist/assimilate-cmd.js.map +1 -0
- package/dist/assimilate-deps.d.ts.map +1 -0
- package/dist/assimilate-deps.js +384 -3
- package/dist/assimilate-deps.js.map +1 -0
- package/dist/assimilate-guard.d.ts +5 -7
- package/dist/assimilate-guard.d.ts.map +1 -0
- package/dist/assimilate-guard.js +60 -2
- package/dist/assimilate-guard.js.map +1 -0
- package/dist/assimilate-welcome.d.ts +1 -1
- package/dist/assimilate-welcome.d.ts.map +1 -0
- package/dist/assimilate-welcome.js +32 -2
- package/dist/assimilate-welcome.js.map +1 -0
- package/dist/auth-env.d.ts +7 -50
- package/dist/auth-env.d.ts.map +1 -0
- package/dist/auth-env.js +22 -1
- package/dist/auth-env.js.map +1 -0
- package/dist/backends/launch-all-pastelist.d.ts.map +1 -0
- package/dist/backends/launch-all-pastelist.js +11 -4
- package/dist/backends/launch-all-pastelist.js.map +1 -0
- package/dist/backends/launch-all-tmux.d.ts.map +1 -0
- package/dist/backends/launch-all-tmux.js +49 -1
- package/dist/backends/launch-all-tmux.js.map +1 -0
- package/dist/backends/launch-all-windows.d.ts.map +1 -0
- package/dist/backends/launch-all-windows.js +86 -12
- package/dist/backends/launch-all-windows.js.map +1 -0
- package/dist/bare-launch-menu.d.ts.map +1 -0
- package/dist/bare-launch-menu.js +83 -6
- package/dist/bare-launch-menu.js.map +1 -0
- package/dist/claude-launch-args.d.ts.map +1 -0
- package/dist/claude-launch-args.js +34 -1
- package/dist/claude-launch-args.js.map +1 -0
- package/dist/claude.d.ts.map +1 -0
- package/dist/claude.js +486 -14
- package/dist/claude.js.map +1 -0
- package/dist/cleanup-cmd.d.ts +5 -5
- package/dist/cleanup-cmd.d.ts.map +1 -0
- package/dist/cleanup-cmd.js +453 -14
- package/dist/cleanup-cmd.js.map +1 -0
- package/dist/clear-rewake-core.d.ts.map +1 -0
- package/dist/clear-rewake-core.js +23 -2
- package/dist/clear-rewake-core.js.map +1 -0
- package/dist/clear-rewake.d.ts.map +1 -0
- package/dist/clear-rewake.js +19 -1
- package/dist/clear-rewake.js.map +1 -0
- package/dist/cli-help.d.ts +5 -0
- package/dist/cli-help.d.ts.map +1 -0
- package/dist/cli-help.js +104 -56
- package/dist/cli-help.js.map +1 -0
- package/dist/cli-platform.d.ts.map +1 -0
- package/dist/cli-platform.js +106 -4
- package/dist/cli-platform.js.map +1 -0
- package/dist/cli-tool-approval.d.ts +84 -0
- package/dist/cli-tool-approval.d.ts.map +1 -0
- package/dist/cli-tool-approval.js +583 -0
- package/dist/cli-tool-approval.js.map +1 -0
- package/dist/codex-app-server.d.ts.map +1 -0
- package/dist/codex-app-server.js +228 -4
- package/dist/codex-app-server.js.map +1 -0
- package/dist/codex-app-wake.d.ts.map +1 -0
- package/dist/codex-app-wake.js +432 -2
- package/dist/codex-app-wake.js.map +1 -0
- package/dist/codex-launch.d.ts.map +1 -0
- package/dist/codex-launch.js +102 -2
- package/dist/codex-launch.js.map +1 -0
- package/dist/codex-remote.d.ts +6 -1
- package/dist/codex-remote.d.ts.map +1 -0
- package/dist/codex-remote.js +394 -1
- package/dist/codex-remote.js.map +1 -0
- package/dist/codex-wake-resolve.d.ts.map +1 -0
- package/dist/codex-wake-resolve.js +127 -1
- package/dist/codex-wake-resolve.js.map +1 -0
- package/dist/config-utils.d.ts +2 -2
- package/dist/config-utils.d.ts.map +1 -0
- package/dist/config-utils.js +766 -3
- package/dist/config-utils.js.map +1 -0
- package/dist/config.d.ts +98 -61
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +426 -1
- package/dist/config.js.map +1 -0
- package/dist/console-prefix.d.ts.map +1 -0
- package/dist/console-prefix.js +96 -1
- package/dist/console-prefix.js.map +1 -0
- package/dist/cube-name.d.ts.map +1 -0
- package/dist/cube-name.js +67 -1
- package/dist/cube-name.js.map +1 -0
- package/dist/cubes.d.ts +126 -18
- package/dist/cubes.d.ts.map +1 -0
- package/dist/cubes.js +412 -4
- package/dist/cubes.js.map +1 -0
- package/dist/debug.d.ts.map +1 -0
- package/dist/debug.js +71 -1
- package/dist/debug.js.map +1 -0
- package/dist/direct-log.d.ts.map +1 -0
- package/dist/direct-log.js +11 -1
- package/dist/direct-log.js.map +1 -0
- package/dist/docs-sections.d.ts +11 -10
- package/dist/docs-sections.d.ts.map +1 -0
- package/dist/docs-sections.js +113 -3
- package/dist/docs-sections.js.map +1 -0
- package/dist/drone-lifecycle.d.ts +5 -21
- package/dist/drone-lifecycle.d.ts.map +1 -0
- package/dist/drone-lifecycle.js +57 -8
- package/dist/drone-lifecycle.js.map +1 -0
- package/dist/drone-management.d.ts +29 -0
- package/dist/drone-management.d.ts.map +1 -0
- package/dist/drone-management.js +109 -0
- package/dist/drone-management.js.map +1 -0
- package/dist/ensure-mcp-config.d.ts.map +1 -0
- package/dist/ensure-mcp-config.js +36 -1
- package/dist/ensure-mcp-config.js.map +1 -0
- package/dist/evict-drone.d.ts +10 -2
- package/dist/evict-drone.d.ts.map +1 -0
- package/dist/evict-drone.js +60 -1
- package/dist/evict-drone.js.map +1 -0
- package/dist/gc-orphan-inboxes.d.ts.map +1 -0
- package/dist/gc-orphan-inboxes.js +177 -1
- package/dist/gc-orphan-inboxes.js.map +1 -0
- package/dist/inbox-monitor.d.ts +1 -1
- package/dist/inbox-monitor.d.ts.map +1 -0
- package/dist/inbox-monitor.js +856 -3
- package/dist/inbox-monitor.js.map +1 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1034 -37
- package/dist/index.js.map +1 -0
- package/dist/launch-all-cmd.d.ts.map +1 -0
- package/dist/launch-all-cmd.js +377 -37
- package/dist/launch-all-cmd.js.map +1 -0
- package/dist/launch-all-command.d.ts.map +1 -0
- package/dist/launch-all-command.js +24 -1
- package/dist/launch-all-command.js.map +1 -0
- package/dist/launch-all-deps.d.ts +3 -8
- package/dist/launch-all-deps.d.ts.map +1 -0
- package/dist/launch-all-deps.js +99 -1
- package/dist/launch-all-deps.js.map +1 -0
- package/dist/launch-all-discovery.d.ts +1 -0
- package/dist/launch-all-discovery.d.ts.map +1 -0
- package/dist/launch-all-discovery.js +90 -4
- package/dist/launch-all-discovery.js.map +1 -0
- package/dist/launch-all-locks.d.ts.map +1 -0
- package/dist/launch-all-locks.js +54 -1
- package/dist/launch-all-locks.js.map +1 -0
- package/dist/launch-gate.d.ts +2 -2
- package/dist/launch-gate.d.ts.map +1 -0
- package/dist/launch-gate.js +73 -1
- package/dist/launch-gate.js.map +1 -0
- package/dist/lifecycle-log-guard.d.ts.map +1 -0
- package/dist/lifecycle-log-guard.js +93 -2
- package/dist/lifecycle-log-guard.js.map +1 -0
- package/dist/list-roles-render.d.ts.map +1 -0
- package/dist/list-roles-render.js +40 -6
- package/dist/list-roles-render.js.map +1 -0
- package/dist/local-log-routing.d.ts +19 -0
- package/dist/local-log-routing.d.ts.map +1 -0
- package/dist/local-log-routing.js +67 -0
- package/dist/local-log-routing.js.map +1 -0
- package/dist/local-manage-tool-result.d.ts +8 -0
- package/dist/local-manage-tool-result.d.ts.map +1 -0
- package/dist/local-manage-tool-result.js +10 -0
- package/dist/local-manage-tool-result.js.map +1 -0
- package/dist/local-server-cursor.d.ts +34 -0
- package/dist/local-server-cursor.d.ts.map +1 -0
- package/dist/local-server-cursor.js +151 -0
- package/dist/local-server-cursor.js.map +1 -0
- package/dist/log-audit.d.ts.map +1 -0
- package/dist/log-audit.js +194 -3
- package/dist/log-audit.js.map +1 -0
- package/dist/log-stream.d.ts +31 -18
- package/dist/log-stream.d.ts.map +1 -0
- package/dist/log-stream.js +1214 -12
- package/dist/log-stream.js.map +1 -0
- package/dist/model-presets.d.ts.map +1 -0
- package/dist/model-presets.js +19 -1
- package/dist/model-presets.js.map +1 -0
- package/dist/name-validator.d.ts.map +1 -0
- package/dist/name-validator.js +22 -1
- package/dist/name-validator.js.map +1 -0
- package/dist/opencode-drone.d.ts.map +1 -0
- package/dist/opencode-drone.js +438 -4
- package/dist/opencode-drone.js.map +1 -0
- package/dist/opencode-plugin.d.ts +1 -1
- package/dist/opencode-plugin.d.ts.map +1 -0
- package/dist/opencode-plugin.js +29 -4
- package/dist/opencode-plugin.js.map +1 -0
- package/dist/parse-assimilate-args.d.ts +1 -1
- package/dist/parse-assimilate-args.d.ts.map +1 -0
- package/dist/parse-assimilate-args.js +125 -1
- package/dist/parse-assimilate-args.js.map +1 -0
- package/dist/parse-launch-all-args.d.ts.map +1 -0
- package/dist/parse-launch-all-args.js +70 -1
- package/dist/parse-launch-all-args.js.map +1 -0
- package/dist/postinstall-banner.d.ts.map +1 -0
- package/dist/postinstall-banner.js +27 -2
- package/dist/postinstall-banner.js.map +1 -0
- package/dist/postinstall.d.ts.map +1 -0
- package/dist/postinstall.js +32 -5
- package/dist/postinstall.js.map +1 -0
- package/dist/readiness-probe.d.ts.map +1 -0
- package/dist/readiness-probe.js +9 -1
- package/dist/readiness-probe.js.map +1 -0
- package/dist/regen-format.d.ts.map +1 -0
- package/dist/regen-format.js +545 -13
- package/dist/regen-format.js.map +1 -0
- package/dist/regen.d.ts.map +1 -0
- package/dist/regen.js +141 -5
- package/dist/regen.js.map +1 -0
- package/dist/remote-client.d.ts +65 -175
- package/dist/remote-client.d.ts.map +1 -0
- package/dist/remote-client.js +942 -1
- package/dist/remote-client.js.map +1 -0
- package/dist/reset-local-seat-cmd.d.ts +50 -0
- package/dist/reset-local-seat-cmd.d.ts.map +1 -0
- package/dist/reset-local-seat-cmd.js +169 -0
- package/dist/reset-local-seat-cmd.js.map +1 -0
- package/dist/role-match.d.ts.map +1 -0
- package/dist/role-match.js +23 -1
- package/dist/role-match.js.map +1 -0
- package/dist/role-resolver.d.ts.map +1 -0
- package/dist/role-resolver.js +60 -1
- package/dist/role-resolver.js.map +1 -0
- package/dist/roster-render.d.ts.map +1 -0
- package/dist/roster-render.js +121 -3
- package/dist/roster-render.js.map +1 -0
- package/dist/seat-probe.d.ts +32 -13
- package/dist/seat-probe.d.ts.map +1 -0
- package/dist/seat-probe.js +78 -1
- package/dist/seat-probe.js.map +1 -0
- package/dist/seat-store.d.ts +76 -0
- package/dist/seat-store.d.ts.map +1 -0
- package/dist/seat-store.js +309 -0
- package/dist/seat-store.js.map +1 -0
- package/dist/seats.d.ts +303 -0
- package/dist/seats.d.ts.map +1 -0
- package/dist/seats.js +551 -0
- package/dist/seats.js.map +1 -0
- package/dist/self-path.d.ts +27 -0
- package/dist/self-path.d.ts.map +1 -0
- package/dist/self-path.js +43 -0
- package/dist/self-path.js.map +1 -0
- package/dist/server-endpoint.d.ts +9 -0
- package/dist/server-endpoint.d.ts.map +1 -0
- package/dist/server-endpoint.js +38 -0
- package/dist/server-endpoint.js.map +1 -0
- package/dist/server-errors.d.ts +50 -0
- package/dist/server-errors.d.ts.map +1 -0
- package/dist/server-errors.js +79 -0
- package/dist/server-errors.js.map +1 -0
- package/dist/server-handshake.d.ts +197 -0
- package/dist/server-handshake.d.ts.map +1 -0
- package/dist/server-handshake.js +505 -0
- package/dist/server-handshake.js.map +1 -0
- package/dist/server-response.d.ts +3 -0
- package/dist/server-response.d.ts.map +1 -0
- package/dist/server-response.js +67 -0
- package/dist/server-response.js.map +1 -0
- package/dist/server-trust.d.ts +14 -0
- package/dist/server-trust.d.ts.map +1 -0
- package/dist/server-trust.js +225 -0
- package/dist/server-trust.js.map +1 -0
- package/dist/setup-confirm.d.ts +3 -3
- package/dist/setup-confirm.d.ts.map +1 -0
- package/dist/setup-confirm.js +126 -2
- package/dist/setup-confirm.js.map +1 -0
- package/dist/setup.d.ts +5 -3
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +169 -46
- package/dist/setup.js.map +1 -0
- package/dist/shell-escape.d.ts.map +1 -0
- package/dist/shell-escape.js +22 -1
- package/dist/shell-escape.js.map +1 -0
- package/dist/spawn.d.ts +2 -2
- package/dist/spawn.d.ts.map +1 -0
- package/dist/spawn.js +29 -10
- package/dist/spawn.js.map +1 -0
- package/dist/startup-services.d.ts +0 -1
- package/dist/startup-services.d.ts.map +1 -0
- package/dist/startup-services.js +26 -1
- package/dist/startup-services.js.map +1 -0
- package/dist/stream-owner.d.ts +6 -2
- package/dist/stream-owner.d.ts.map +1 -0
- package/dist/stream-owner.js +547 -2
- package/dist/stream-owner.js.map +1 -0
- package/dist/stream-status.d.ts +1 -1
- package/dist/stream-status.d.ts.map +1 -0
- package/dist/stream-status.js +268 -3
- package/dist/stream-status.js.map +1 -0
- package/dist/sync-roles-render.d.ts.map +1 -0
- package/dist/sync-roles-render.js +118 -3
- package/dist/sync-roles-render.js.map +1 -0
- package/dist/sync.d.ts +1 -1
- package/dist/sync.d.ts.map +1 -0
- package/dist/sync.js +286 -22
- package/dist/sync.js.map +1 -0
- package/dist/terminal-title.d.ts.map +1 -0
- package/dist/terminal-title.js +68 -1
- package/dist/terminal-title.js.map +1 -0
- package/dist/token-store.d.ts +17 -111
- package/dist/token-store.d.ts.map +1 -0
- package/dist/token-store.js +81 -1
- package/dist/token-store.js.map +1 -0
- package/dist/tool-manifest.d.ts +4 -8
- package/dist/tool-manifest.d.ts.map +1 -0
- package/dist/tool-manifest.js +554 -1
- package/dist/tool-manifest.js.map +1 -0
- package/dist/tool-scope.d.ts +8 -12
- package/dist/tool-scope.d.ts.map +1 -0
- package/dist/tool-scope.js +97 -1
- package/dist/tool-scope.js.map +1 -0
- package/dist/types.d.ts +1 -5
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/unknown-subcommand.d.ts +1 -1
- package/dist/unknown-subcommand.d.ts.map +1 -0
- package/dist/unknown-subcommand.js +37 -1
- package/dist/unknown-subcommand.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +78 -2
- package/dist/version.js.map +1 -0
- package/dist/working-repo.d.ts.map +1 -0
- package/dist/working-repo.js +81 -1
- package/dist/working-repo.js.map +1 -0
- package/dist/worktree-lifecycle.d.ts.map +1 -0
- package/dist/worktree-lifecycle.js +205 -2
- package/dist/worktree-lifecycle.js.map +1 -0
- package/docs/EXTRACTION_PROVENANCE.md +40 -0
- package/docs/LOCAL_SERVER.md +103 -0
- package/docs/RELEASING.md +120 -0
- package/package.json +52 -34
- package/src/agent-runtime.ts +69 -0
- package/src/assimilate-cmd.ts +1864 -0
- package/src/assimilate-deps.ts +444 -0
- package/src/assimilate-guard.ts +84 -0
- package/src/assimilate-welcome.ts +39 -0
- package/src/auth-env.ts +21 -0
- package/src/backends/launch-all-pastelist.ts +18 -0
- package/src/backends/launch-all-tmux.ts +75 -0
- package/src/backends/launch-all-windows.ts +110 -0
- package/src/bare-launch-menu.ts +148 -0
- package/src/claude-launch-args.ts +35 -0
- package/src/claude.ts +561 -0
- package/src/cleanup-cmd.ts +581 -0
- package/src/clear-rewake-core.ts +34 -0
- package/src/clear-rewake.ts +18 -0
- package/src/cli-help.ts +116 -0
- package/src/cli-platform.ts +127 -0
- package/src/cli-tool-approval.ts +716 -0
- package/src/codex-app-server.ts +238 -0
- package/src/codex-app-wake.ts +512 -0
- package/src/codex-launch.ts +140 -0
- package/src/codex-remote.ts +500 -0
- package/src/codex-wake-resolve.ts +155 -0
- package/src/config-utils.ts +811 -0
- package/src/config.ts +595 -0
- package/src/console-prefix.ts +99 -0
- package/src/cube-name.ts +69 -0
- package/src/cubes.ts +583 -0
- package/src/debug.ts +73 -0
- package/src/direct-log.ts +9 -0
- package/src/docs-sections.ts +129 -0
- package/src/drone-lifecycle.ts +62 -0
- package/src/drone-management.ts +162 -0
- package/src/ensure-mcp-config.ts +54 -0
- package/src/evict-drone.ts +82 -0
- package/src/gc-orphan-inboxes.ts +229 -0
- package/src/inbox-monitor.ts +1005 -0
- package/src/index.ts +1201 -0
- package/src/launch-all-cmd.ts +450 -0
- package/src/launch-all-command.ts +30 -0
- package/src/launch-all-deps.ts +188 -0
- package/src/launch-all-discovery.ts +118 -0
- package/src/launch-all-locks.ts +74 -0
- package/src/launch-gate.ts +80 -0
- package/src/lifecycle-log-guard.ts +144 -0
- package/src/list-roles-render.ts +52 -0
- package/src/local-log-routing.ts +106 -0
- package/src/local-manage-tool-result.ts +10 -0
- package/src/local-server-cursor.ts +188 -0
- package/src/log-audit.ts +207 -0
- package/src/log-stream.ts +1505 -0
- package/src/model-presets.ts +22 -0
- package/src/name-validator.ts +23 -0
- package/src/opencode-drone.ts +528 -0
- package/src/opencode-plugin.ts +38 -0
- package/src/parse-assimilate-args.ts +125 -0
- package/src/parse-launch-all-args.ts +100 -0
- package/src/postinstall-banner.ts +37 -0
- package/src/postinstall.ts +36 -0
- package/src/readiness-probe.ts +12 -0
- package/src/regen-format.ts +651 -0
- package/src/regen.ts +150 -0
- package/src/remote-client.ts +1379 -0
- package/src/reset-local-seat-cmd.ts +227 -0
- package/src/role-match.ts +29 -0
- package/src/role-resolver.ts +93 -0
- package/src/roster-render.ts +196 -0
- package/src/seat-probe.ts +115 -0
- package/src/seat-store.ts +333 -0
- package/src/seats.ts +741 -0
- package/src/self-path.ts +51 -0
- package/src/server-endpoint.ts +38 -0
- package/src/server-errors.ts +93 -0
- package/src/server-handshake.ts +783 -0
- package/src/server-response.ts +76 -0
- package/src/server-trust.ts +245 -0
- package/src/setup-confirm.ts +171 -0
- package/src/setup.ts +186 -0
- package/src/shell-escape.ts +22 -0
- package/src/spawn.ts +31 -0
- package/src/startup-services.ts +36 -0
- package/src/stream-owner.ts +686 -0
- package/src/stream-status.ts +337 -0
- package/src/sync-roles-render.ts +163 -0
- package/src/sync.ts +363 -0
- package/src/terminal-title.ts +76 -0
- package/src/token-store.ts +102 -0
- package/src/tool-manifest.ts +610 -0
- package/src/tool-scope.ts +110 -0
- package/src/types.ts +5 -0
- package/src/unknown-subcommand.ts +35 -0
- package/src/version.ts +82 -0
- package/src/working-repo.ts +99 -0
- package/src/worktree-lifecycle.ts +254 -0
- package/dist/auth-recovery.d.ts +0 -33
- package/dist/auth-recovery.js +0 -1
- package/dist/auth.d.ts +0 -103
- package/dist/auth.js +0 -39
- package/dist/device-auth.d.ts +0 -75
- package/dist/device-auth.js +0 -1
- package/dist/drone-address.d.ts +0 -26
- package/dist/drone-address.js +0 -1
- package/dist/get-started.d.ts +0 -20
- package/dist/get-started.js +0 -2
- package/dist/health-beat.d.ts +0 -134
- package/dist/health-beat.js +0 -1
- package/dist/role-section.d.ts +0 -110
- package/dist/role-section.js +0 -8
- package/dist/setup-action.d.ts +0 -20
- package/dist/setup-action.js +0 -1
- package/dist/stale-version-check.d.ts +0 -40
- package/dist/stale-version-check.js +0 -1
- package/dist/subscription-retry.d.ts +0 -40
- package/dist/subscription-retry.js +0 -1
- package/dist/templates.d.ts +0 -107
- package/dist/templates.js +0 -359
- package/dist/token-crypto.d.ts +0 -50
- package/dist/token-crypto.js +0 -1
package/src/config.ts
ADDED
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secure local-server credential storage.
|
|
3
|
+
*
|
|
4
|
+
* The self-hosted-server credential group (enrollment credentials, pending
|
|
5
|
+
* enrollment/cube-creation records, and the per-seat drone-session bearers)
|
|
6
|
+
* rests ONLY in the 0600 credential file store (Queen rescope) — parity with the
|
|
7
|
+
* server's own TLS private keys. The OS keychain is gone; the raw secret never
|
|
8
|
+
* leaves the 0600 file, and a single store flock serializes every read-compare-write.
|
|
9
|
+
*/
|
|
10
|
+
import os from 'os';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { createHash, randomBytes, randomUUID } from 'crypto';
|
|
13
|
+
import type { ServerCapability } from 'borgmcp-shared/protocol';
|
|
14
|
+
import { withStoreLock } from './seat-store.js';
|
|
15
|
+
import {
|
|
16
|
+
makeFileBackend,
|
|
17
|
+
type TokenBackend,
|
|
18
|
+
} from './token-store.js';
|
|
19
|
+
|
|
20
|
+
const SERVER_CREDENTIAL_RECORD_VERSION = 2 as const;
|
|
21
|
+
const SERVER_PENDING_ENROLLMENT_RECORD_VERSION = 1 as const;
|
|
22
|
+
const SERVER_CUBE_RETRY_RECORD_VERSION = 1 as const;
|
|
23
|
+
// The 0600 credential store (Queen rescope: replaces the OS keychain). A single
|
|
24
|
+
// file holds every server credential/session/enrollment record; a single flock
|
|
25
|
+
// serializes every mutator + observer that must (SR-seven #4).
|
|
26
|
+
const CREDENTIALS_FILE = path.join(os.homedir(), '.config', 'borgmcp', 'credentials.json');
|
|
27
|
+
const CREDENTIALS_LOCK = `${CREDENTIALS_FILE}.lock`;
|
|
28
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
29
|
+
|
|
30
|
+
export interface ServerCredentialRecord {
|
|
31
|
+
origin: string;
|
|
32
|
+
trustIdentity: string;
|
|
33
|
+
credential: string;
|
|
34
|
+
clientId?: string | null;
|
|
35
|
+
serverCapabilities?: ServerCapability[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ActiveServerCredentialRecord {
|
|
39
|
+
origin: string;
|
|
40
|
+
trustIdentity: string;
|
|
41
|
+
credential: string;
|
|
42
|
+
clientId: string | null;
|
|
43
|
+
serverCapabilities: ServerCapability[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface PendingServerEnrollmentRecord {
|
|
47
|
+
origin: string;
|
|
48
|
+
trustIdentity: string;
|
|
49
|
+
invitation: string;
|
|
50
|
+
retryKey: string;
|
|
51
|
+
credential: string;
|
|
52
|
+
clientName?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface PendingServerCubeCreationRecord {
|
|
56
|
+
origin: string;
|
|
57
|
+
trustIdentity: string;
|
|
58
|
+
clientId: string;
|
|
59
|
+
repositoryBinding: string;
|
|
60
|
+
retryKey: string;
|
|
61
|
+
name: string;
|
|
62
|
+
template: 'default';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* S1 clean-slate local drone-session record. The client CSPRNG-generates the
|
|
67
|
+
* bearer and persists it PENDING (keyed by the stable per-seat attach identity
|
|
68
|
+
* origin+trustIdentity+cube+role — no drone id yet on first attach) BEFORE the
|
|
69
|
+
* attach request. The bearer digest is the sole server correlator, so a lost
|
|
70
|
+
* response is recovered by re-sending the exact same bearer. After a verified
|
|
71
|
+
* `created`/`reused` response the SAME record is enriched in place with the
|
|
72
|
+
* server-assigned drone/session identity — no generation, no rotation.
|
|
73
|
+
*/
|
|
74
|
+
/**
|
|
75
|
+
* The seat/sibling operation dimension for a pending session. Because the client
|
|
76
|
+
* bearer digest is the SOLE server correlator, distinct seats require distinct
|
|
77
|
+
* bearers; a deliberate sibling attach must therefore namespace its bearer apart
|
|
78
|
+
* from the durable in-place seat for the same (origin,trust,cube,role). Ported
|
|
79
|
+
* from the retired local-attach `operationBindingKey`. projectRoot is captured
|
|
80
|
+
* before a successful sibling attach changes cwd, so it is stable across the
|
|
81
|
+
* whole prepare→activate lifecycle.
|
|
82
|
+
*/
|
|
83
|
+
export interface ServerSessionOperation {
|
|
84
|
+
projectRoot: string;
|
|
85
|
+
kind: 'seat' | 'sibling';
|
|
86
|
+
operationKey: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
function validateServerCredentialBinding(origin: string, trustIdentity: string): void {
|
|
91
|
+
let parsed: URL;
|
|
92
|
+
try {
|
|
93
|
+
parsed = new URL(origin);
|
|
94
|
+
} catch {
|
|
95
|
+
throw new Error('invalid Borg server credential origin');
|
|
96
|
+
}
|
|
97
|
+
if (parsed.origin !== origin || parsed.protocol !== 'https:') {
|
|
98
|
+
throw new Error('Borg server credentials require a canonical HTTPS origin');
|
|
99
|
+
}
|
|
100
|
+
if (
|
|
101
|
+
trustIdentity.length < 1 ||
|
|
102
|
+
trustIdentity.length > 512 ||
|
|
103
|
+
/[\u0000-\u001f\u007f]/.test(trustIdentity)
|
|
104
|
+
) {
|
|
105
|
+
throw new Error('invalid Borg server trust identity');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function serverCredentialAccount(origin: string, trustIdentity: string): string {
|
|
110
|
+
validateServerCredentialBinding(origin, trustIdentity);
|
|
111
|
+
const binding = createHash('sha256')
|
|
112
|
+
.update(origin)
|
|
113
|
+
.update('\0')
|
|
114
|
+
.update(trustIdentity)
|
|
115
|
+
.digest('hex');
|
|
116
|
+
return `borg-server-credential:${binding}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// The whole 0600 credential store is serialized by the SINGLE store lock
|
|
120
|
+
// (seat-store.withStoreLock over CREDENTIALS_LOCK). Every mutator AND every
|
|
121
|
+
// observer that must serialize runs its entire read-compare-write inside one
|
|
122
|
+
// continuous hold, released on EVERY path incl throw (SR-seven #4). There is no
|
|
123
|
+
// per-account lock and no compat shim — call sites acquire the store lock directly.
|
|
124
|
+
|
|
125
|
+
function serverPendingEnrollmentAccount(origin: string, trustIdentity: string): string {
|
|
126
|
+
validateServerCredentialBinding(origin, trustIdentity);
|
|
127
|
+
const binding = createHash('sha256')
|
|
128
|
+
.update(origin)
|
|
129
|
+
.update('\0')
|
|
130
|
+
.update(trustIdentity)
|
|
131
|
+
.digest('hex');
|
|
132
|
+
return `borg-server-enrollment-pending:${binding}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function validateUuid(value: string, label: string): void {
|
|
136
|
+
if (!UUID_RE.test(value)) throw new Error(`invalid Borg server ${label}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function validateServerCapabilities(value: readonly string[]): ServerCapability[] {
|
|
140
|
+
if (value.length > 1 || value.some((capability) => capability !== 'create_cube')) {
|
|
141
|
+
throw new Error('invalid Borg server capabilities');
|
|
142
|
+
}
|
|
143
|
+
return [...value] as ServerCapability[];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function validateClientName(clientName: string | undefined): void {
|
|
147
|
+
if (
|
|
148
|
+
clientName !== undefined &&
|
|
149
|
+
(Buffer.byteLength(clientName, 'utf8') < 1 ||
|
|
150
|
+
Buffer.byteLength(clientName, 'utf8') > 120 ||
|
|
151
|
+
!/^[A-Za-z0-9][A-Za-z0-9 ._-]*$/.test(clientName))
|
|
152
|
+
) {
|
|
153
|
+
throw new Error('invalid Borg server client name');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function validateInvitation(invitation: string): void {
|
|
158
|
+
if (
|
|
159
|
+
invitation.length < 43 ||
|
|
160
|
+
invitation.length > 1024 ||
|
|
161
|
+
!/^[A-Za-z0-9_-]+$/.test(invitation)
|
|
162
|
+
) {
|
|
163
|
+
throw new Error('invalid Borg server invitation');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function validateEnrollmentCredential(credential: string): void {
|
|
168
|
+
if (!/^[A-Za-z0-9_-]{43}$/.test(credential)) {
|
|
169
|
+
throw new Error('invalid Borg server credential');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
function serverCubeRetryAccount(
|
|
175
|
+
origin: string,
|
|
176
|
+
trustIdentity: string,
|
|
177
|
+
clientId: string,
|
|
178
|
+
repositoryBinding: string,
|
|
179
|
+
): string {
|
|
180
|
+
validateServerCredentialBinding(origin, trustIdentity);
|
|
181
|
+
validateUuid(clientId, 'client identity');
|
|
182
|
+
if (!/^[a-f0-9]{64}$/.test(repositoryBinding)) {
|
|
183
|
+
throw new Error('invalid Borg server repository binding');
|
|
184
|
+
}
|
|
185
|
+
const binding = createHash('sha256')
|
|
186
|
+
.update(origin)
|
|
187
|
+
.update('\0')
|
|
188
|
+
.update(trustIdentity)
|
|
189
|
+
.update('\0')
|
|
190
|
+
.update(clientId)
|
|
191
|
+
.update('\0')
|
|
192
|
+
.update(repositoryBinding)
|
|
193
|
+
.digest('hex');
|
|
194
|
+
return `borg-server-cube-pending:${binding}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
// Local-server bearers rest ONLY in the 0600 credential store (Queen rescope —
|
|
199
|
+
// parity with the server's TLS keys; no OS keychain, no obfuscation-grade
|
|
200
|
+
// fallback). The single store lock (CREDENTIALS_LOCK) serializes the RCW.
|
|
201
|
+
let serverCredentialBackendPromise: Promise<TokenBackend> | null = null;
|
|
202
|
+
async function getServerCredentialBackend(): Promise<TokenBackend> {
|
|
203
|
+
if (!serverCredentialBackendPromise) {
|
|
204
|
+
serverCredentialBackendPromise = Promise.resolve(makeFileBackend(CREDENTIALS_FILE));
|
|
205
|
+
}
|
|
206
|
+
return serverCredentialBackendPromise;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Test-only credential-store backend injection. */
|
|
210
|
+
export function __setServerCredentialBackendForTest(backend: TokenBackend | null): void {
|
|
211
|
+
serverCredentialBackendPromise = backend ? Promise.resolve(backend) : null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* CR3b: the UNLOCKED credential write body. Validates then set()s the account.
|
|
216
|
+
* Callers that ALREADY hold the credential-store lock (activatePendingServerEnrollment)
|
|
217
|
+
* invoke this directly so they do not re-acquire (and self-deadlock on) the single
|
|
218
|
+
* store lock; the public storeServerCredential wraps it in one lock hold.
|
|
219
|
+
*/
|
|
220
|
+
async function writeServerCredentialRecord(
|
|
221
|
+
backend: TokenBackend,
|
|
222
|
+
record: ServerCredentialRecord,
|
|
223
|
+
): Promise<void> {
|
|
224
|
+
validateServerCredentialBinding(record.origin, record.trustIdentity);
|
|
225
|
+
validateEnrollmentCredential(record.credential);
|
|
226
|
+
if (record.clientId !== undefined && record.clientId !== null) {
|
|
227
|
+
validateUuid(record.clientId, 'client identity');
|
|
228
|
+
}
|
|
229
|
+
const serverCapabilities = validateServerCapabilities(record.serverCapabilities ?? []);
|
|
230
|
+
await backend.set(
|
|
231
|
+
serverCredentialAccount(record.origin, record.trustIdentity),
|
|
232
|
+
JSON.stringify({
|
|
233
|
+
version: SERVER_CREDENTIAL_RECORD_VERSION,
|
|
234
|
+
origin: record.origin,
|
|
235
|
+
trustIdentity: record.trustIdentity,
|
|
236
|
+
credential: record.credential,
|
|
237
|
+
clientId: record.clientId ?? null,
|
|
238
|
+
serverCapabilities,
|
|
239
|
+
}),
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Persist one self-hosted server credential in the dedicated 0600 credential store.
|
|
245
|
+
*
|
|
246
|
+
* The account key binds both the canonical authority origin and the verified
|
|
247
|
+
* server/CA identity. A credential enrolled for one authority is therefore
|
|
248
|
+
* never considered for another endpoint or trust anchor. Enrollment owns the
|
|
249
|
+
* write; command-line arguments and environment variables are intentionally
|
|
250
|
+
* not credential sources. CR3b: the load→set→rename runs inside ONE hold of the
|
|
251
|
+
* single store lock so a concurrent writer cannot lose an unrelated account.
|
|
252
|
+
*/
|
|
253
|
+
export async function storeServerCredential(record: ServerCredentialRecord): Promise<void> {
|
|
254
|
+
const backend = await getServerCredentialBackend();
|
|
255
|
+
await withStoreLock(CREDENTIALS_LOCK, () => writeServerCredentialRecord(backend, record));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Read an authority-bound active client record, failing closed on corruption. */
|
|
259
|
+
export async function getServerCredentialRecord(
|
|
260
|
+
origin: string,
|
|
261
|
+
trustIdentity: string,
|
|
262
|
+
): Promise<ActiveServerCredentialRecord | null> {
|
|
263
|
+
const backend = await getServerCredentialBackend();
|
|
264
|
+
const stored = await backend.get(serverCredentialAccount(origin, trustIdentity));
|
|
265
|
+
if (!stored) return null;
|
|
266
|
+
try {
|
|
267
|
+
const record = JSON.parse(stored) as Partial<ActiveServerCredentialRecord> & {
|
|
268
|
+
version?: unknown;
|
|
269
|
+
};
|
|
270
|
+
if (
|
|
271
|
+
record.version !== SERVER_CREDENTIAL_RECORD_VERSION ||
|
|
272
|
+
record.origin !== origin ||
|
|
273
|
+
record.trustIdentity !== trustIdentity ||
|
|
274
|
+
typeof record.credential !== 'string' ||
|
|
275
|
+
!/^[A-Za-z0-9_-]{43}$/.test(record.credential) ||
|
|
276
|
+
(record.clientId !== null &&
|
|
277
|
+
(typeof record.clientId !== 'string' || !UUID_RE.test(record.clientId))) ||
|
|
278
|
+
!Array.isArray(record.serverCapabilities)
|
|
279
|
+
) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
let serverCapabilities: ServerCapability[];
|
|
283
|
+
try {
|
|
284
|
+
serverCapabilities = validateServerCapabilities(record.serverCapabilities);
|
|
285
|
+
} catch {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
origin,
|
|
290
|
+
trustIdentity,
|
|
291
|
+
credential: record.credential,
|
|
292
|
+
clientId: record.clientId,
|
|
293
|
+
serverCapabilities,
|
|
294
|
+
};
|
|
295
|
+
} catch {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Read only the bearer for existing call sites that do not need capability metadata. */
|
|
301
|
+
export async function getServerCredential(
|
|
302
|
+
origin: string,
|
|
303
|
+
trustIdentity: string,
|
|
304
|
+
): Promise<string | null> {
|
|
305
|
+
return (await getServerCredentialRecord(origin, trustIdentity))?.credential ?? null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function decodePendingServerEnrollment(
|
|
309
|
+
stored: string,
|
|
310
|
+
origin: string,
|
|
311
|
+
trustIdentity: string,
|
|
312
|
+
): PendingServerEnrollmentRecord {
|
|
313
|
+
const record = JSON.parse(stored) as Partial<PendingServerEnrollmentRecord> & {
|
|
314
|
+
version?: unknown;
|
|
315
|
+
state?: unknown;
|
|
316
|
+
};
|
|
317
|
+
if (
|
|
318
|
+
record.version !== SERVER_PENDING_ENROLLMENT_RECORD_VERSION ||
|
|
319
|
+
record.state !== 'pending' ||
|
|
320
|
+
record.origin !== origin ||
|
|
321
|
+
record.trustIdentity !== trustIdentity ||
|
|
322
|
+
typeof record.invitation !== 'string' ||
|
|
323
|
+
typeof record.retryKey !== 'string' || !UUID_RE.test(record.retryKey) ||
|
|
324
|
+
typeof record.credential !== 'string' || !/^[A-Za-z0-9_-]{43}$/.test(record.credential) ||
|
|
325
|
+
(record.clientName !== undefined && typeof record.clientName !== 'string')
|
|
326
|
+
) {
|
|
327
|
+
throw new Error('invalid');
|
|
328
|
+
}
|
|
329
|
+
validateInvitation(record.invitation);
|
|
330
|
+
validateClientName(record.clientName);
|
|
331
|
+
return {
|
|
332
|
+
origin,
|
|
333
|
+
trustIdentity,
|
|
334
|
+
invitation: record.invitation,
|
|
335
|
+
retryKey: record.retryKey,
|
|
336
|
+
credential: record.credential,
|
|
337
|
+
...(record.clientName === undefined ? {} : { clientName: record.clientName }),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Load an exact durable PENDING tuple so a new process can resume it. */
|
|
342
|
+
export async function getPendingServerEnrollment(
|
|
343
|
+
origin: string,
|
|
344
|
+
trustIdentity: string,
|
|
345
|
+
): Promise<PendingServerEnrollmentRecord | null> {
|
|
346
|
+
validateServerCredentialBinding(origin, trustIdentity);
|
|
347
|
+
const backend = await getServerCredentialBackend();
|
|
348
|
+
const account = serverPendingEnrollmentAccount(origin, trustIdentity);
|
|
349
|
+
return withStoreLock(CREDENTIALS_LOCK, async () => {
|
|
350
|
+
const stored = await backend.get(account);
|
|
351
|
+
if (!stored) return null;
|
|
352
|
+
try {
|
|
353
|
+
return decodePendingServerEnrollment(stored, origin, trustIdentity);
|
|
354
|
+
} catch {
|
|
355
|
+
throw new Error('pending Borg server enrollment is corrupt');
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Generate and persist an exact enrollment tuple before network I/O. A
|
|
362
|
+
* pre-existing PENDING tuple must match the invitation and presentation name;
|
|
363
|
+
* this makes response-loss retries exact without minting a second bearer.
|
|
364
|
+
*/
|
|
365
|
+
export async function getOrCreatePendingServerEnrollment(
|
|
366
|
+
input: {
|
|
367
|
+
origin: string;
|
|
368
|
+
trustIdentity: string;
|
|
369
|
+
invitation: string;
|
|
370
|
+
clientName?: string;
|
|
371
|
+
},
|
|
372
|
+
): Promise<PendingServerEnrollmentRecord> {
|
|
373
|
+
validateServerCredentialBinding(input.origin, input.trustIdentity);
|
|
374
|
+
validateInvitation(input.invitation);
|
|
375
|
+
validateClientName(input.clientName);
|
|
376
|
+
const backend = await getServerCredentialBackend();
|
|
377
|
+
const account = serverPendingEnrollmentAccount(input.origin, input.trustIdentity);
|
|
378
|
+
return withStoreLock(CREDENTIALS_LOCK, async () => {
|
|
379
|
+
const stored = await backend.get(account);
|
|
380
|
+
if (stored) {
|
|
381
|
+
try {
|
|
382
|
+
const record = decodePendingServerEnrollment(
|
|
383
|
+
stored,
|
|
384
|
+
input.origin,
|
|
385
|
+
input.trustIdentity,
|
|
386
|
+
);
|
|
387
|
+
if (
|
|
388
|
+
record.invitation !== input.invitation ||
|
|
389
|
+
record.clientName !== input.clientName
|
|
390
|
+
) {
|
|
391
|
+
throw new Error('mismatch');
|
|
392
|
+
}
|
|
393
|
+
return record;
|
|
394
|
+
} catch {
|
|
395
|
+
throw new Error('pending Borg server enrollment does not match this request');
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const record: PendingServerEnrollmentRecord = {
|
|
400
|
+
origin: input.origin,
|
|
401
|
+
trustIdentity: input.trustIdentity,
|
|
402
|
+
invitation: input.invitation,
|
|
403
|
+
retryKey: randomUUID(),
|
|
404
|
+
credential: randomBytes(32).toString('base64url'),
|
|
405
|
+
...(input.clientName === undefined ? {} : { clientName: input.clientName }),
|
|
406
|
+
};
|
|
407
|
+
validateEnrollmentCredential(record.credential);
|
|
408
|
+
await backend.set(account, JSON.stringify({
|
|
409
|
+
version: SERVER_PENDING_ENROLLMENT_RECORD_VERSION,
|
|
410
|
+
state: 'pending',
|
|
411
|
+
...record,
|
|
412
|
+
}));
|
|
413
|
+
return record;
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** Activate the exact pending tuple only after a verified server response. */
|
|
418
|
+
export async function activatePendingServerEnrollment(
|
|
419
|
+
input: {
|
|
420
|
+
origin: string;
|
|
421
|
+
trustIdentity: string;
|
|
422
|
+
retryKey: string;
|
|
423
|
+
credential: string;
|
|
424
|
+
clientId: string;
|
|
425
|
+
serverCapabilities: ServerCapability[];
|
|
426
|
+
},
|
|
427
|
+
): Promise<void> {
|
|
428
|
+
validateServerCredentialBinding(input.origin, input.trustIdentity);
|
|
429
|
+
validateUuid(input.retryKey, 'enrollment retry key');
|
|
430
|
+
validateEnrollmentCredential(input.credential);
|
|
431
|
+
validateUuid(input.clientId, 'client identity');
|
|
432
|
+
const serverCapabilities = validateServerCapabilities(input.serverCapabilities);
|
|
433
|
+
const backend = await getServerCredentialBackend();
|
|
434
|
+
const pendingAccount = serverPendingEnrollmentAccount(input.origin, input.trustIdentity);
|
|
435
|
+
await withStoreLock(CREDENTIALS_LOCK, async () => {
|
|
436
|
+
const stored = await backend.get(pendingAccount);
|
|
437
|
+
if (!stored) throw new Error('pending Borg server enrollment is missing');
|
|
438
|
+
try {
|
|
439
|
+
const pending = decodePendingServerEnrollment(
|
|
440
|
+
stored,
|
|
441
|
+
input.origin,
|
|
442
|
+
input.trustIdentity,
|
|
443
|
+
);
|
|
444
|
+
if (pending.retryKey !== input.retryKey || pending.credential !== input.credential) {
|
|
445
|
+
throw new Error('mismatch');
|
|
446
|
+
}
|
|
447
|
+
} catch {
|
|
448
|
+
throw new Error('pending Borg server enrollment does not match the verified response');
|
|
449
|
+
}
|
|
450
|
+
// Already inside the single store lock — use the UNLOCKED write body so we do
|
|
451
|
+
// not re-acquire (and self-deadlock on) CREDENTIALS_LOCK (CR3b).
|
|
452
|
+
await writeServerCredentialRecord(backend, {
|
|
453
|
+
origin: input.origin,
|
|
454
|
+
trustIdentity: input.trustIdentity,
|
|
455
|
+
credential: input.credential,
|
|
456
|
+
clientId: input.clientId,
|
|
457
|
+
serverCapabilities,
|
|
458
|
+
});
|
|
459
|
+
await backend.delete(pendingAccount);
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** Delete only the exact definitively rejected pending attempt. */
|
|
464
|
+
export async function clearPendingServerEnrollment(
|
|
465
|
+
origin: string,
|
|
466
|
+
trustIdentity: string,
|
|
467
|
+
retryKey: string,
|
|
468
|
+
): Promise<void> {
|
|
469
|
+
validateUuid(retryKey, 'enrollment retry key');
|
|
470
|
+
const backend = await getServerCredentialBackend();
|
|
471
|
+
const account = serverPendingEnrollmentAccount(origin, trustIdentity);
|
|
472
|
+
await withStoreLock(CREDENTIALS_LOCK, async () => {
|
|
473
|
+
const stored = await backend.get(account);
|
|
474
|
+
if (!stored) return;
|
|
475
|
+
try {
|
|
476
|
+
const pending = decodePendingServerEnrollment(stored, origin, trustIdentity);
|
|
477
|
+
if (pending.retryKey !== retryKey) return;
|
|
478
|
+
} catch {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
await backend.delete(account);
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** Persist one repository-scoped cube-create idempotency key in the 0600 credential store. */
|
|
486
|
+
export async function getOrCreatePendingServerCubeCreation(
|
|
487
|
+
input: {
|
|
488
|
+
origin: string;
|
|
489
|
+
trustIdentity: string;
|
|
490
|
+
clientId: string;
|
|
491
|
+
projectRoot: string;
|
|
492
|
+
name: string;
|
|
493
|
+
template: 'default';
|
|
494
|
+
},
|
|
495
|
+
): Promise<PendingServerCubeCreationRecord> {
|
|
496
|
+
validateServerCredentialBinding(input.origin, input.trustIdentity);
|
|
497
|
+
validateUuid(input.clientId, 'client identity');
|
|
498
|
+
if (input.projectRoot.length < 1 || input.projectRoot.length > 4096 || /[\u0000-\u001f\u007f]/.test(input.projectRoot)) {
|
|
499
|
+
throw new Error('invalid Borg server repository binding');
|
|
500
|
+
}
|
|
501
|
+
if (Buffer.byteLength(input.name, 'utf8') < 1 || Buffer.byteLength(input.name, 'utf8') > 120 ||
|
|
502
|
+
!/^[A-Za-z0-9][A-Za-z0-9 ._-]*$/.test(input.name)) {
|
|
503
|
+
throw new Error('invalid Borg server cube name');
|
|
504
|
+
}
|
|
505
|
+
const repositoryBinding = createHash('sha256').update(input.projectRoot).digest('hex');
|
|
506
|
+
const backend = await getServerCredentialBackend();
|
|
507
|
+
const account = serverCubeRetryAccount(
|
|
508
|
+
input.origin,
|
|
509
|
+
input.trustIdentity,
|
|
510
|
+
input.clientId,
|
|
511
|
+
repositoryBinding,
|
|
512
|
+
);
|
|
513
|
+
return withStoreLock(CREDENTIALS_LOCK, async () => {
|
|
514
|
+
const stored = await backend.get(account);
|
|
515
|
+
if (stored) {
|
|
516
|
+
try {
|
|
517
|
+
const record = JSON.parse(stored) as Partial<PendingServerCubeCreationRecord> & {
|
|
518
|
+
version?: unknown;
|
|
519
|
+
state?: unknown;
|
|
520
|
+
};
|
|
521
|
+
if (
|
|
522
|
+
record.version !== SERVER_CUBE_RETRY_RECORD_VERSION ||
|
|
523
|
+
record.state !== 'pending' ||
|
|
524
|
+
record.origin !== input.origin ||
|
|
525
|
+
record.trustIdentity !== input.trustIdentity ||
|
|
526
|
+
record.clientId !== input.clientId ||
|
|
527
|
+
record.repositoryBinding !== repositoryBinding ||
|
|
528
|
+
record.name !== input.name ||
|
|
529
|
+
record.template !== input.template ||
|
|
530
|
+
typeof record.retryKey !== 'string' || !UUID_RE.test(record.retryKey)
|
|
531
|
+
) {
|
|
532
|
+
throw new Error('mismatch');
|
|
533
|
+
}
|
|
534
|
+
return {
|
|
535
|
+
origin: input.origin,
|
|
536
|
+
trustIdentity: input.trustIdentity,
|
|
537
|
+
clientId: input.clientId,
|
|
538
|
+
repositoryBinding,
|
|
539
|
+
retryKey: record.retryKey,
|
|
540
|
+
name: input.name,
|
|
541
|
+
template: input.template,
|
|
542
|
+
};
|
|
543
|
+
} catch {
|
|
544
|
+
throw new Error('pending Borg server cube creation does not match this repository');
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
const record: PendingServerCubeCreationRecord = {
|
|
548
|
+
origin: input.origin,
|
|
549
|
+
trustIdentity: input.trustIdentity,
|
|
550
|
+
clientId: input.clientId,
|
|
551
|
+
repositoryBinding,
|
|
552
|
+
retryKey: randomUUID(),
|
|
553
|
+
name: input.name,
|
|
554
|
+
template: input.template,
|
|
555
|
+
};
|
|
556
|
+
await backend.set(account, JSON.stringify({
|
|
557
|
+
version: SERVER_CUBE_RETRY_RECORD_VERSION,
|
|
558
|
+
state: 'pending',
|
|
559
|
+
...record,
|
|
560
|
+
}));
|
|
561
|
+
return record;
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
export async function clearPendingServerCubeCreation(
|
|
566
|
+
record: PendingServerCubeCreationRecord,
|
|
567
|
+
): Promise<void> {
|
|
568
|
+
const backend = await getServerCredentialBackend();
|
|
569
|
+
const account = serverCubeRetryAccount(
|
|
570
|
+
record.origin,
|
|
571
|
+
record.trustIdentity,
|
|
572
|
+
record.clientId,
|
|
573
|
+
record.repositoryBinding,
|
|
574
|
+
);
|
|
575
|
+
await withStoreLock(CREDENTIALS_LOCK, async () => {
|
|
576
|
+
const stored = await backend.get(account);
|
|
577
|
+
if (!stored) return;
|
|
578
|
+
try {
|
|
579
|
+
const pending = JSON.parse(stored) as { retryKey?: unknown };
|
|
580
|
+
if (pending.retryKey !== record.retryKey) return;
|
|
581
|
+
} catch {
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
await backend.delete(account);
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export async function clearServerCredential(origin: string, trustIdentity: string): Promise<void> {
|
|
589
|
+
const backend = await getServerCredentialBackend();
|
|
590
|
+
const pendingAccount = serverPendingEnrollmentAccount(origin, trustIdentity);
|
|
591
|
+
await withStoreLock(CREDENTIALS_LOCK, async () => {
|
|
592
|
+
await backend.delete(serverCredentialAccount(origin, trustIdentity));
|
|
593
|
+
await backend.delete(pendingAccount);
|
|
594
|
+
});
|
|
595
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drone self-identification prefix for client-emitted console messages.
|
|
3
|
+
*
|
|
4
|
+
* Per gh#25: when a drone session emits a console error (e.g.
|
|
5
|
+
* "Authentication expired — your saved login has expired. Run: borg setup"),
|
|
6
|
+
* the Queen has no way to
|
|
7
|
+
* tell which drone window the message came from without scanning every
|
|
8
|
+
* open terminal. Window title alone (set by terminal-title.ts) is
|
|
9
|
+
* insufficient — the Queen reads the active terminal's output stream,
|
|
10
|
+
* not its title bar.
|
|
11
|
+
*
|
|
12
|
+
* This module exports a one-shot initializer that resolves the prefix
|
|
13
|
+
* from the local cube state cache, plus a synchronous getter that
|
|
14
|
+
* call sites use to wrap each console.error.
|
|
15
|
+
*
|
|
16
|
+
* Format (matches the terminal-title.ts middle-dot convention so
|
|
17
|
+
* surfaces stay internally consistent):
|
|
18
|
+
* `[<drone-label> · <cube-name>]` (assimilated)
|
|
19
|
+
* `[borg · <repo-basename>]` (no cube cached)
|
|
20
|
+
*
|
|
21
|
+
* The not-yet-assimilated shape reads as neutral metadata ("this is
|
|
22
|
+
* borg, in project X"), NOT a fault the user must fix — and mirrors the
|
|
23
|
+
* unassimilated terminal-title shape (`borg · <repo-basename>`), so the
|
|
24
|
+
* title bar and the console prefix agree (gh#818 P1).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { basename } from 'node:path';
|
|
28
|
+
import chalk from 'chalk';
|
|
29
|
+
import { getActiveCube } from './cubes.js';
|
|
30
|
+
|
|
31
|
+
let cachedPrefix: string | null = null;
|
|
32
|
+
|
|
33
|
+
/** Neutral prefix for a not-yet-assimilated session (gh#818 P1). */
|
|
34
|
+
function unassimilatedPrefix(): string {
|
|
35
|
+
return `[borg · ${basename(process.cwd())}]`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the drone-self-identification prefix from cube state and
|
|
40
|
+
* cache it for subsequent synchronous reads. Idempotent — calling
|
|
41
|
+
* multiple times returns the same value. Falls back silently to the
|
|
42
|
+
* unassimilated shape on any read error so console emission is never
|
|
43
|
+
* blocked.
|
|
44
|
+
*/
|
|
45
|
+
export async function initConsolePrefix(): Promise<string> {
|
|
46
|
+
if (cachedPrefix !== null) return cachedPrefix;
|
|
47
|
+
try {
|
|
48
|
+
const active = await getActiveCube();
|
|
49
|
+
if (active?.droneLabel && active?.name) {
|
|
50
|
+
cachedPrefix = `[${active.droneLabel} · ${active.name}]`;
|
|
51
|
+
return cachedPrefix;
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// Fall through to unassimilated fallback.
|
|
55
|
+
}
|
|
56
|
+
cachedPrefix = unassimilatedPrefix();
|
|
57
|
+
return cachedPrefix;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Synchronous prefix getter. Returns the cached value if initialized,
|
|
62
|
+
* otherwise the unassimilated fallback — safe to call before
|
|
63
|
+
* initConsolePrefix() resolves.
|
|
64
|
+
*/
|
|
65
|
+
export function droneIdPrefix(): string {
|
|
66
|
+
if (cachedPrefix !== null) return cachedPrefix;
|
|
67
|
+
return unassimilatedPrefix();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Prefix + trailing space, styled dim/gray so the prefix is metadata
|
|
72
|
+
* and the message body retains visual emphasis. Use as
|
|
73
|
+
* `${consolePrefix()}<message>`.
|
|
74
|
+
*/
|
|
75
|
+
export function consolePrefix(): string {
|
|
76
|
+
return chalk.gray(droneIdPrefix()) + ' ';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Drop-in replacement for `console.error` that prepends the drone
|
|
81
|
+
* self-id prefix. If the first arg is a string, the prefix is
|
|
82
|
+
* concatenated to it; otherwise the prefix is emitted as its own arg
|
|
83
|
+
* (handles the `console.error('label:', value)` shape).
|
|
84
|
+
*/
|
|
85
|
+
export function cerr(...args: any[]): void {
|
|
86
|
+
if (args.length === 0) {
|
|
87
|
+
console.error(consolePrefix());
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (typeof args[0] === 'string') {
|
|
91
|
+
console.error(consolePrefix() + args[0], ...args.slice(1));
|
|
92
|
+
} else {
|
|
93
|
+
console.error(consolePrefix(), ...args);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function _resetCachedPrefixForTests(): void {
|
|
98
|
+
cachedPrefix = null;
|
|
99
|
+
}
|