spexcode 0.6.1 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -149
- package/bin/spex.mjs +4 -0
- package/node_modules/@spexcode/spec-cli/bin/spex.mjs +76 -0
- package/node_modules/@spexcode/spec-cli/dist/attach.d.ts +2 -0
- package/node_modules/@spexcode/spec-cli/dist/attach.js +48 -0
- package/node_modules/@spexcode/spec-cli/dist/claude-headless.d.ts +33 -0
- package/node_modules/@spexcode/spec-cli/dist/claude-headless.js +307 -0
- package/node_modules/@spexcode/spec-cli/dist/cli.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/cli.js +1859 -0
- package/node_modules/@spexcode/spec-cli/dist/client.d.ts +111 -0
- package/node_modules/@spexcode/spec-cli/dist/client.js +476 -0
- package/node_modules/@spexcode/spec-cli/dist/cockpit.d.ts +7 -0
- package/node_modules/@spexcode/spec-cli/dist/cockpit.js +26 -0
- package/node_modules/@spexcode/spec-cli/dist/codex-headless.d.ts +1 -0
- package/{spec-cli/src/codex-headless.ts → node_modules/@spexcode/spec-cli/dist/codex-headless.js} +3 -9
- package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.d.ts +53 -0
- package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.js +686 -0
- package/node_modules/@spexcode/spec-cli/dist/commit-surgery.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/commit-surgery.js +90 -0
- package/node_modules/@spexcode/spec-cli/dist/contract-filter.d.ts +14 -0
- package/node_modules/@spexcode/spec-cli/dist/contract-filter.js +219 -0
- package/node_modules/@spexcode/spec-cli/dist/dashboard-assets.d.ts +6 -0
- package/node_modules/@spexcode/spec-cli/dist/dashboard-assets.js +50 -0
- package/node_modules/@spexcode/spec-cli/dist/delivery-queue.d.ts +23 -0
- package/node_modules/@spexcode/spec-cli/dist/delivery-queue.js +179 -0
- package/node_modules/@spexcode/spec-cli/dist/doctor.d.ts +9 -0
- package/node_modules/@spexcode/spec-cli/dist/doctor.js +547 -0
- package/node_modules/@spexcode/spec-cli/dist/eval-host.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/eval-host.js +19 -0
- package/node_modules/@spexcode/spec-cli/dist/execution-trace.d.ts +26 -0
- package/node_modules/@spexcode/spec-cli/dist/execution-trace.js +475 -0
- package/node_modules/@spexcode/spec-cli/dist/file-write.d.ts +2 -0
- package/node_modules/@spexcode/spec-cli/dist/file-write.js +26 -0
- package/node_modules/@spexcode/spec-cli/dist/flat.d.ts +89 -0
- package/node_modules/@spexcode/spec-cli/dist/flat.js +1076 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway-auth.d.ts +56 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway-auth.js +168 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway-hub.d.ts +28 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway-hub.js +361 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway.d.ts +37 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway.js +503 -0
- package/node_modules/@spexcode/spec-cli/dist/graphCache.d.ts +27 -0
- package/node_modules/@spexcode/spec-cli/dist/graphCache.js +716 -0
- package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.d.ts +14 -0
- package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.js +35 -0
- package/node_modules/@spexcode/spec-cli/dist/graphStream.d.ts +85 -0
- package/node_modules/@spexcode/spec-cli/dist/graphStream.js +1195 -0
- package/node_modules/@spexcode/spec-cli/dist/guidance-catalog.d.ts +64 -0
- package/node_modules/@spexcode/spec-cli/dist/guidance-catalog.js +166 -0
- package/node_modules/@spexcode/spec-cli/dist/guide.d.ts +8 -0
- package/{spec-cli/src/guide.ts → node_modules/@spexcode/spec-cli/dist/guide.js} +115 -22
- package/node_modules/@spexcode/spec-cli/dist/harness-select.d.ts +18 -0
- package/node_modules/@spexcode/spec-cli/dist/harness-select.js +62 -0
- package/node_modules/@spexcode/spec-cli/dist/harness.d.ts +325 -0
- package/node_modules/@spexcode/spec-cli/dist/harness.js +3026 -0
- package/node_modules/@spexcode/spec-cli/dist/headless-controller.d.ts +9 -0
- package/node_modules/@spexcode/spec-cli/dist/headless-controller.js +47 -0
- package/node_modules/@spexcode/spec-cli/dist/help.d.ts +10 -0
- package/node_modules/@spexcode/spec-cli/dist/help.js +574 -0
- package/node_modules/@spexcode/spec-cli/dist/hook-prompts.d.ts +18 -0
- package/node_modules/@spexcode/spec-cli/dist/hook-prompts.js +122 -0
- package/node_modules/@spexcode/spec-cli/dist/hooks.d.ts +1 -0
- package/{spec-cli/src/hooks.ts → node_modules/@spexcode/spec-cli/dist/hooks.js} +15 -15
- package/node_modules/@spexcode/spec-cli/dist/host-resources.d.ts +97 -0
- package/node_modules/@spexcode/spec-cli/dist/host-resources.js +795 -0
- package/node_modules/@spexcode/spec-cli/dist/host.d.ts +88 -0
- package/node_modules/@spexcode/spec-cli/dist/host.js +663 -0
- package/node_modules/@spexcode/spec-cli/dist/index.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/index.js +899 -0
- package/node_modules/@spexcode/spec-cli/dist/init.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/init.js +284 -0
- package/node_modules/@spexcode/spec-cli/dist/issues-cli.d.ts +6 -0
- package/node_modules/@spexcode/spec-cli/dist/issues-cli.js +334 -0
- package/node_modules/@spexcode/spec-cli/dist/issues.d.ts +69 -0
- package/node_modules/@spexcode/spec-cli/dist/issues.js +189 -0
- package/node_modules/@spexcode/spec-cli/dist/lint.d.ts +34 -0
- package/node_modules/@spexcode/spec-cli/dist/lint.js +476 -0
- package/node_modules/@spexcode/spec-cli/dist/listen.d.ts +7 -0
- package/{spec-cli/src/listen.ts → node_modules/@spexcode/spec-cli/dist/listen.js} +13 -17
- package/node_modules/@spexcode/spec-cli/dist/localIssues.d.ts +65 -0
- package/node_modules/@spexcode/spec-cli/dist/localIssues.js +582 -0
- package/node_modules/@spexcode/spec-cli/dist/login-page.d.ts +5 -0
- package/{spec-cli/src/login-page.ts → node_modules/@spexcode/spec-cli/dist/login-page.js} +7 -7
- package/node_modules/@spexcode/spec-cli/dist/loop-in.d.ts +23 -0
- package/{spec-cli/src/loop-in.ts → node_modules/@spexcode/spec-cli/dist/loop-in.js} +35 -45
- package/node_modules/@spexcode/spec-cli/dist/machine-peer.d.ts +69 -0
- package/node_modules/@spexcode/spec-cli/dist/machine-peer.js +603 -0
- package/node_modules/@spexcode/spec-cli/dist/materialize.d.ts +18 -0
- package/node_modules/@spexcode/spec-cli/dist/materialize.js +468 -0
- package/node_modules/@spexcode/spec-cli/dist/mentions.d.ts +50 -0
- package/node_modules/@spexcode/spec-cli/dist/mentions.js +117 -0
- package/node_modules/@spexcode/spec-cli/dist/opencode-headless.d.ts +4 -0
- package/node_modules/@spexcode/spec-cli/dist/opencode-headless.js +208 -0
- package/node_modules/@spexcode/spec-cli/dist/opencode.d.ts +3 -0
- package/{spec-cli/src/opencode.ts → node_modules/@spexcode/spec-cli/dist/opencode.js} +9 -13
- package/node_modules/@spexcode/spec-cli/dist/pi-harness.d.ts +3 -0
- package/{spec-cli/src/pi-harness.ts → node_modules/@spexcode/spec-cli/dist/pi-harness.js} +45 -40
- package/node_modules/@spexcode/spec-cli/dist/pi-headless.d.ts +25 -0
- package/node_modules/@spexcode/spec-cli/dist/pi-headless.js +196 -0
- package/node_modules/@spexcode/spec-cli/dist/plugin-harness.d.ts +21 -0
- package/node_modules/@spexcode/spec-cli/dist/plugin-harness.js +146 -0
- package/node_modules/@spexcode/spec-cli/dist/pty-bridge.d.ts +10 -0
- package/node_modules/@spexcode/spec-cli/dist/pty-bridge.js +404 -0
- package/node_modules/@spexcode/spec-cli/dist/pty-helper.d.mts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/pty-helper.mjs +101 -0
- package/node_modules/@spexcode/spec-cli/dist/pty-native-helper.d.mts +2 -0
- package/node_modules/@spexcode/spec-cli/dist/pty-native-helper.mjs +23 -0
- package/node_modules/@spexcode/spec-cli/dist/public-graph.d.ts +46 -0
- package/node_modules/@spexcode/spec-cli/dist/public-graph.js +56 -0
- package/node_modules/@spexcode/spec-cli/dist/ranker.d.ts +15 -0
- package/node_modules/@spexcode/spec-cli/dist/ranker.js +188 -0
- package/node_modules/@spexcode/spec-cli/dist/reaper.d.ts +6 -0
- package/node_modules/@spexcode/spec-cli/dist/reaper.js +81 -0
- package/node_modules/@spexcode/spec-cli/dist/reviews.d.ts +189 -0
- package/node_modules/@spexcode/spec-cli/dist/reviews.js +350 -0
- package/node_modules/@spexcode/spec-cli/dist/runtime-guard.d.ts +6 -0
- package/node_modules/@spexcode/spec-cli/dist/runtime-guard.js +37 -0
- package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.d.ts +21 -0
- package/node_modules/@spexcode/spec-cli/dist/runtime-ownership.js +84 -0
- package/node_modules/@spexcode/spec-cli/dist/search.bench.d.mts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/search.bench.mjs +75 -0
- package/node_modules/@spexcode/spec-cli/dist/search.d.ts +20 -0
- package/node_modules/@spexcode/spec-cli/dist/search.js +61 -0
- package/node_modules/@spexcode/spec-cli/dist/session-cursors.d.ts +14 -0
- package/node_modules/@spexcode/spec-cli/dist/session-cursors.js +82 -0
- package/node_modules/@spexcode/spec-cli/dist/session-declarations.d.ts +13 -0
- package/node_modules/@spexcode/spec-cli/dist/session-declarations.js +126 -0
- package/node_modules/@spexcode/spec-cli/dist/session-execution.d.ts +10 -0
- package/node_modules/@spexcode/spec-cli/dist/session-execution.js +70 -0
- package/node_modules/@spexcode/spec-cli/dist/session-files.d.ts +28 -0
- package/node_modules/@spexcode/spec-cli/dist/session-files.js +130 -0
- package/node_modules/@spexcode/spec-cli/dist/session-follow.d.ts +31 -0
- package/node_modules/@spexcode/spec-cli/dist/session-follow.js +179 -0
- package/node_modules/@spexcode/spec-cli/dist/session-reparent.d.ts +10 -0
- package/node_modules/@spexcode/spec-cli/dist/session-reparent.js +33 -0
- package/node_modules/@spexcode/spec-cli/dist/session-timeline.d.ts +51 -0
- package/node_modules/@spexcode/spec-cli/dist/session-timeline.js +233 -0
- package/node_modules/@spexcode/spec-cli/dist/session-web.d.ts +24 -0
- package/node_modules/@spexcode/spec-cli/dist/session-web.js +141 -0
- package/node_modules/@spexcode/spec-cli/dist/sessions.d.ts +412 -0
- package/node_modules/@spexcode/spec-cli/dist/sessions.js +4239 -0
- package/node_modules/@spexcode/spec-cli/dist/sh.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/sh.js +3 -0
- package/node_modules/@spexcode/spec-cli/dist/shim-runtime.d.ts +1 -0
- package/{spec-cli/src/shim-runtime.ts → node_modules/@spexcode/spec-cli/dist/shim-runtime.js} +3 -4
- package/node_modules/@spexcode/spec-cli/dist/slash-commands.d.ts +9 -0
- package/node_modules/@spexcode/spec-cli/dist/slash-commands.js +307 -0
- package/node_modules/@spexcode/spec-cli/dist/source-files.d.ts +9 -0
- package/node_modules/@spexcode/spec-cli/dist/source-files.js +113 -0
- package/node_modules/@spexcode/spec-cli/dist/supervise.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/supervise.js +308 -0
- package/node_modules/@spexcode/spec-cli/dist/tree.d.ts +25 -0
- package/node_modules/@spexcode/spec-cli/dist/tree.js +98 -0
- package/node_modules/@spexcode/spec-cli/dist/tsx-bin.d.ts +3 -0
- package/node_modules/@spexcode/spec-cli/dist/tsx-bin.js +41 -0
- package/node_modules/@spexcode/spec-cli/dist/uninstall.d.ts +3 -0
- package/node_modules/@spexcode/spec-cli/dist/uninstall.js +177 -0
- package/node_modules/@spexcode/spec-cli/dist/uploads.d.ts +36 -0
- package/node_modules/@spexcode/spec-cli/dist/uploads.js +243 -0
- package/node_modules/@spexcode/spec-cli/dist/worktree-sources.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/worktree-sources.js +47 -0
- package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/dispatch.sh +5 -5
- package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/harness.sh +27 -7
- package/node_modules/@spexcode/spec-cli/package.json +47 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +8 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/comment-altitude/spec.md +9 -0
- package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-first/spec-first.sh +1 -1
- package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-of-file/spec-of-file.sh +1 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/core/spec.md +22 -0
- package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/stop-gate/spec.md +2 -2
- package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/stop-gate/stop-gate.sh +31 -29
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/prompts/atomic-landing/spec.md +15 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/prompts/forge-link/spec.md +10 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/prompts/memory-hygiene/spec.md +13 -0
- package/node_modules/@spexcode/spec-cli/templates/spec/project/.plugins/prompts/reproduce-before-fix/spec.md +12 -0
- package/node_modules/@spexcode/spec-core/dist/anchors.d.ts +94 -0
- package/node_modules/@spexcode/spec-core/dist/anchors.js +730 -0
- package/node_modules/@spexcode/spec-core/dist/git.d.ts +166 -0
- package/node_modules/@spexcode/spec-core/dist/git.js +2736 -0
- package/node_modules/@spexcode/spec-core/dist/graph-delta.d.ts +44 -0
- package/node_modules/@spexcode/spec-core/dist/graph-delta.js +72 -0
- package/node_modules/@spexcode/spec-core/dist/graph.d.ts +34 -0
- package/node_modules/@spexcode/spec-core/dist/graph.js +237 -0
- package/node_modules/@spexcode/spec-core/dist/graphDelta.d.ts +3 -0
- package/node_modules/@spexcode/spec-core/dist/graphDelta.js +13 -0
- package/node_modules/@spexcode/spec-core/dist/harness-identity.d.ts +31 -0
- package/node_modules/@spexcode/spec-core/dist/harness-identity.js +20 -0
- package/node_modules/@spexcode/spec-core/dist/identity-presets.d.ts +152 -0
- package/node_modules/@spexcode/spec-core/dist/identity-presets.js +132 -0
- package/node_modules/@spexcode/spec-core/dist/index.d.ts +45 -0
- package/node_modules/@spexcode/spec-core/dist/index.js +19 -0
- package/node_modules/@spexcode/spec-core/dist/layout.d.ts +183 -0
- package/node_modules/@spexcode/spec-core/dist/layout.js +548 -0
- package/node_modules/@spexcode/spec-core/dist/process-identity.d.ts +37 -0
- package/node_modules/@spexcode/spec-core/dist/process-identity.js +214 -0
- package/node_modules/@spexcode/spec-core/dist/project-identity.d.ts +12 -0
- package/node_modules/@spexcode/spec-core/dist/project-identity.js +71 -0
- package/node_modules/@spexcode/spec-core/dist/project-store.d.ts +3 -0
- package/node_modules/@spexcode/spec-core/dist/project-store.js +14 -0
- package/node_modules/@spexcode/spec-core/dist/resilience.d.ts +2 -0
- package/node_modules/@spexcode/spec-core/dist/resilience.js +40 -0
- package/node_modules/@spexcode/spec-core/dist/review/index.d.ts +3 -0
- package/node_modules/@spexcode/spec-core/dist/review/index.js +5 -0
- package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.d.ts +77 -0
- package/node_modules/@spexcode/spec-core/dist/review/reviewFilters.js +308 -0
- package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.d.ts +66 -0
- package/node_modules/@spexcode/spec-core/dist/review/reviewQuery.js +180 -0
- package/node_modules/@spexcode/spec-core/dist/review/session.d.ts +4 -0
- package/node_modules/@spexcode/spec-core/dist/review/session.js +8 -0
- package/node_modules/@spexcode/spec-core/dist/reviewSnapshot.d.ts +15 -0
- package/node_modules/@spexcode/spec-core/dist/reviewSnapshot.js +12 -0
- package/node_modules/@spexcode/spec-core/dist/root-lru.d.ts +4 -0
- package/{spec-cli/src/root-lru.ts → node_modules/@spexcode/spec-core/dist/root-lru.js} +26 -30
- package/node_modules/@spexcode/spec-core/dist/specs.d.ts +117 -0
- package/node_modules/@spexcode/spec-core/dist/specs.js +489 -0
- package/node_modules/@spexcode/spec-core/package.json +28 -0
- package/node_modules/@spexcode/spec-eval/dist/cache.d.ts +10 -0
- package/node_modules/@spexcode/spec-eval/dist/cache.js +50 -0
- package/node_modules/@spexcode/spec-eval/dist/cli.d.ts +22 -0
- package/node_modules/@spexcode/spec-eval/dist/cli.js +981 -0
- package/node_modules/@spexcode/spec-eval/dist/evaltab.d.ts +98 -0
- package/node_modules/@spexcode/spec-eval/dist/evaltab.js +176 -0
- package/node_modules/@spexcode/spec-eval/dist/filing.d.ts +16 -0
- package/node_modules/@spexcode/spec-eval/dist/filing.js +36 -0
- package/node_modules/@spexcode/spec-eval/dist/freshness.d.ts +48 -0
- package/node_modules/@spexcode/spec-eval/dist/freshness.js +799 -0
- package/node_modules/@spexcode/spec-eval/dist/host.d.ts +46 -0
- package/node_modules/@spexcode/spec-eval/dist/host.js +53 -0
- package/node_modules/@spexcode/spec-eval/dist/humanok.d.ts +11 -0
- package/node_modules/@spexcode/spec-eval/dist/humanok.js +27 -0
- package/node_modules/@spexcode/spec-eval/dist/index.d.ts +4 -0
- package/node_modules/@spexcode/spec-eval/dist/index.js +4 -0
- package/node_modules/@spexcode/spec-eval/dist/remarks.d.ts +31 -0
- package/node_modules/@spexcode/spec-eval/dist/remarks.js +1 -0
- package/node_modules/@spexcode/spec-eval/dist/scenariofresh.d.ts +13 -0
- package/node_modules/@spexcode/spec-eval/dist/scenariofresh.js +303 -0
- package/node_modules/@spexcode/spec-eval/dist/scenarios.d.ts +99 -0
- package/node_modules/@spexcode/spec-eval/dist/scenarios.js +650 -0
- package/node_modules/@spexcode/spec-eval/dist/sessioneval.d.ts +307 -0
- package/node_modules/@spexcode/spec-eval/dist/sessioneval.js +1883 -0
- package/node_modules/@spexcode/spec-eval/dist/sidecar.d.ts +55 -0
- package/node_modules/@spexcode/spec-eval/dist/sidecar.js +82 -0
- package/node_modules/@spexcode/spec-eval/dist/timeline.d.ts +25 -0
- package/node_modules/@spexcode/spec-eval/dist/timeline.js +65 -0
- package/node_modules/@spexcode/spec-eval/dist/ui-path.d.ts +1 -0
- package/node_modules/@spexcode/spec-eval/dist/ui-path.js +2 -0
- package/node_modules/@spexcode/spec-eval/package.json +36 -0
- package/node_modules/@spexcode/spec-forge/dist/cache.d.ts +29 -0
- package/node_modules/@spexcode/spec-forge/dist/cache.js +59 -0
- package/node_modules/@spexcode/spec-forge/dist/cli.d.ts +1 -0
- package/node_modules/@spexcode/spec-forge/dist/cli.js +100 -0
- package/node_modules/@spexcode/spec-forge/dist/drivers/github.d.ts +2 -0
- package/node_modules/@spexcode/spec-forge/dist/drivers/github.js +116 -0
- package/node_modules/@spexcode/spec-forge/dist/drivers/gitlab.d.ts +7 -0
- package/node_modules/@spexcode/spec-forge/dist/drivers/gitlab.js +144 -0
- package/node_modules/@spexcode/spec-forge/dist/drivers.d.ts +10 -0
- package/node_modules/@spexcode/spec-forge/dist/drivers.js +83 -0
- package/node_modules/@spexcode/spec-forge/dist/index.d.ts +4 -0
- package/node_modules/@spexcode/spec-forge/dist/index.js +4 -0
- package/node_modules/@spexcode/spec-forge/dist/links.d.ts +15 -0
- package/node_modules/@spexcode/spec-forge/dist/links.js +65 -0
- package/node_modules/@spexcode/spec-forge/dist/needs-eval.d.ts +10 -0
- package/node_modules/@spexcode/spec-forge/dist/needs-eval.js +20 -0
- package/node_modules/@spexcode/spec-forge/dist/port.d.ts +53 -0
- package/node_modules/@spexcode/spec-forge/dist/port.js +1 -0
- package/node_modules/@spexcode/spec-forge/dist/resident.d.ts +7 -0
- package/node_modules/@spexcode/spec-forge/dist/resident.js +43 -0
- package/node_modules/@spexcode/spec-forge/package.json +32 -0
- package/package.json +33 -19
- package/spec-cli/bin/spex.mjs +0 -69
- package/spec-cli/src/anchors.ts +0 -706
- package/spec-cli/src/attach.ts +0 -43
- package/spec-cli/src/claude-headless.ts +0 -321
- package/spec-cli/src/cli.ts +0 -1115
- package/spec-cli/src/client.ts +0 -315
- package/spec-cli/src/cockpit.ts +0 -43
- package/spec-cli/src/codex-runtime-generations.ts +0 -646
- package/spec-cli/src/commit-surgery.ts +0 -68
- package/spec-cli/src/contract-filter.ts +0 -202
- package/spec-cli/src/delivery-queue.ts +0 -107
- package/spec-cli/src/doctor.ts +0 -516
- package/spec-cli/src/file-write.ts +0 -22
- package/spec-cli/src/gateway-auth.ts +0 -181
- package/spec-cli/src/gateway-hub.ts +0 -356
- package/spec-cli/src/gateway.ts +0 -368
- package/spec-cli/src/git.ts +0 -2180
- package/spec-cli/src/graph.ts +0 -221
- package/spec-cli/src/graphCache.ts +0 -641
- package/spec-cli/src/graphDelta.ts +0 -77
- package/spec-cli/src/graphStream.ts +0 -1019
- package/spec-cli/src/harness-select.ts +0 -77
- package/spec-cli/src/harness.ts +0 -2747
- package/spec-cli/src/headless-controller.ts +0 -63
- package/spec-cli/src/help.ts +0 -480
- package/spec-cli/src/host-resources.ts +0 -707
- package/spec-cli/src/host.ts +0 -611
- package/spec-cli/src/identity-presets.d.ts +0 -13
- package/spec-cli/src/identity-presets.js +0 -128
- package/spec-cli/src/index.ts +0 -730
- package/spec-cli/src/init.ts +0 -258
- package/spec-cli/src/issues-cli.ts +0 -276
- package/spec-cli/src/issues.ts +0 -237
- package/spec-cli/src/layout.ts +0 -557
- package/spec-cli/src/lint.ts +0 -458
- package/spec-cli/src/localIssues.ts +0 -538
- package/spec-cli/src/materialize.ts +0 -412
- package/spec-cli/src/mentions.ts +0 -81
- package/spec-cli/src/opencode-headless.ts +0 -205
- package/spec-cli/src/pi-headless.ts +0 -153
- package/spec-cli/src/plugin-harness.ts +0 -157
- package/spec-cli/src/process-identity.ts +0 -207
- package/spec-cli/src/project-identity.ts +0 -73
- package/spec-cli/src/project-store.ts +0 -17
- package/spec-cli/src/pty-bridge.ts +0 -396
- package/spec-cli/src/pty-helper.mjs +0 -96
- package/spec-cli/src/pty-native-helper.mjs +0 -22
- package/spec-cli/src/ranker.ts +0 -191
- package/spec-cli/src/reaper.ts +0 -125
- package/spec-cli/src/resilience.ts +0 -41
- package/spec-cli/src/reviewSnapshot.ts +0 -28
- package/spec-cli/src/reviews.ts +0 -405
- package/spec-cli/src/runtime-guard.ts +0 -35
- package/spec-cli/src/runtime-ownership.ts +0 -88
- package/spec-cli/src/search.bench.mjs +0 -80
- package/spec-cli/src/search.ts +0 -63
- package/spec-cli/src/session-cursors.ts +0 -91
- package/spec-cli/src/session-follow.ts +0 -182
- package/spec-cli/src/session-timeline.ts +0 -98
- package/spec-cli/src/sessions.ts +0 -3403
- package/spec-cli/src/sh.ts +0 -3
- package/spec-cli/src/slash-commands.ts +0 -318
- package/spec-cli/src/source-files.ts +0 -104
- package/spec-cli/src/specs.ts +0 -498
- package/spec-cli/src/supervise.ts +0 -245
- package/spec-cli/src/tree.ts +0 -118
- package/spec-cli/src/tsx-bin.ts +0 -18
- package/spec-cli/src/uninstall.ts +0 -171
- package/spec-cli/src/uploads.ts +0 -253
- package/spec-cli/src/worktree-sources.ts +0 -42
- package/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +0 -8
- package/spec-cli/templates/spec/project/.plugins/core/comment-altitude/spec.md +0 -28
- package/spec-cli/templates/spec/project/.plugins/core/spec.md +0 -23
- package/spec-cli/templates/spec/project/.plugins/prompts/atomic-landing/spec.md +0 -32
- package/spec-cli/templates/spec/project/.plugins/prompts/forge-link/spec.md +0 -9
- package/spec-cli/templates/spec/project/.plugins/prompts/memory-hygiene/spec.md +0 -15
- package/spec-cli/templates/spec/project/.plugins/prompts/reproduce-before-fix/spec.md +0 -18
- package/spec-dashboard/dist/assets/App-F9uaAVcH.js +0 -2
- package/spec-dashboard/dist/assets/Dashboard-Ba_jhxp1.js +0 -27
- package/spec-dashboard/dist/assets/Dashboard-C5ap-Sga.css +0 -1
- package/spec-dashboard/dist/assets/EvalsPage-FixoOg_n.js +0 -2
- package/spec-dashboard/dist/assets/IssuesPage-CuKLFhH3.js +0 -1
- package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/spec-dashboard/dist/assets/MobileApp-CHgEHORJ.js +0 -2
- package/spec-dashboard/dist/assets/Modal-CQgYymmr.js +0 -1
- package/spec-dashboard/dist/assets/PageScroll-hT7UTLvD.js +0 -1
- package/spec-dashboard/dist/assets/ProjectsPage-CtXxakF9.js +0 -1
- package/spec-dashboard/dist/assets/SessionInterface-6GBZ9nXN.css +0 -32
- package/spec-dashboard/dist/assets/SessionInterface-Bpie-9fs.js +0 -39
- package/spec-dashboard/dist/assets/SessionWindow-CixDi4PI.js +0 -1
- package/spec-dashboard/dist/assets/Settings-C2MsucfE.js +0 -1
- package/spec-dashboard/dist/assets/Thread-C6Go8HRh.js +0 -13
- package/spec-dashboard/dist/assets/bindings-BC9vqpYU.js +0 -1
- package/spec-dashboard/dist/assets/data-B-RQmit6.js +0 -1
- package/spec-dashboard/dist/assets/index-CsI8DElI.css +0 -1
- package/spec-dashboard/dist/assets/index-DrVao0Ep.js +0 -41
- package/spec-dashboard/dist/assets/launch-BBH02b1v.js +0 -276
- package/spec-dashboard/dist/assets/launch-Cp579UoJ.css +0 -1
- package/spec-dashboard/dist/assets/terminalFont-C6oGNKrf.js +0 -1
- package/spec-dashboard/dist/index.html +0 -29
- package/spec-dashboard/src/reviewFilters.js +0 -322
- package/spec-dashboard/src/reviewQuery.js +0 -174
- package/spec-dashboard/src/session.js +0 -199
- package/spec-eval/src/cache.ts +0 -57
- package/spec-eval/src/cli.ts +0 -889
- package/spec-eval/src/evaltab.ts +0 -262
- package/spec-eval/src/filing.ts +0 -39
- package/spec-eval/src/freshness.ts +0 -557
- package/spec-eval/src/humanok.ts +0 -28
- package/spec-eval/src/scenariofresh.ts +0 -244
- package/spec-eval/src/scenarios.ts +0 -672
- package/spec-eval/src/sessioneval.ts +0 -2226
- package/spec-eval/src/sidecar.ts +0 -93
- package/spec-eval/src/timeline.ts +0 -54
- package/spec-forge/src/__fixtures__/github-forge.json +0 -63
- package/spec-forge/src/cache.ts +0 -66
- package/spec-forge/src/cli.ts +0 -99
- package/spec-forge/src/drivers/github.ts +0 -132
- package/spec-forge/src/drivers/gitlab.ts +0 -153
- package/spec-forge/src/drivers.ts +0 -73
- package/spec-forge/src/links.ts +0 -72
- package/spec-forge/src/needs-eval.ts +0 -29
- package/spec-forge/src/port.ts +0 -36
- package/spec-forge/src/resident.ts +0 -46
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/README.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/compat/mark-active-0.5.2-eef1.fixture +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/hooks/compat/mark-active-sed-v0.fixture +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/commit-msg +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/post-checkout +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/post-merge +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/pre-commit +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/prepare-commit-msg +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/hooks/reference-transaction +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/extract/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/regroup/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/rename/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/commands/tidy/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/idle/idle.sh +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/idle/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/mark-active/mark-active.sh +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/mark-active/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/session-fail/fail.sh +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/session-fail/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-first/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/core/spec-of-file/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/prompts/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/skills/distill/digest.mjs +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/skills/distill/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/skills/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/.plugins/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-cli}/templates/spec/project/spec.md +0 -0
- /package/{spec-cli → node_modules/@spexcode/spec-core}/templates/spexcode.json +0 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export type Verifier = {
|
|
2
|
+
algo: 'scrypt';
|
|
3
|
+
N: number;
|
|
4
|
+
r: number;
|
|
5
|
+
p: number;
|
|
6
|
+
salt: string;
|
|
7
|
+
hash: string;
|
|
8
|
+
gen: string;
|
|
9
|
+
};
|
|
10
|
+
export type AuthStore = {
|
|
11
|
+
v: 1;
|
|
12
|
+
secret: string;
|
|
13
|
+
admin?: Verifier;
|
|
14
|
+
projects: Record<string, Verifier>;
|
|
15
|
+
};
|
|
16
|
+
export type Scope = {
|
|
17
|
+
s: 'admin';
|
|
18
|
+
} | {
|
|
19
|
+
s: 'project';
|
|
20
|
+
p: string;
|
|
21
|
+
};
|
|
22
|
+
export type Claims = {
|
|
23
|
+
v: 1;
|
|
24
|
+
s: 'admin' | 'project';
|
|
25
|
+
p?: string;
|
|
26
|
+
g: string;
|
|
27
|
+
t: number;
|
|
28
|
+
};
|
|
29
|
+
export type Decision = {
|
|
30
|
+
ok: true;
|
|
31
|
+
via: 'admin' | 'project' | 'open' | 'loopback';
|
|
32
|
+
} | {
|
|
33
|
+
ok: false;
|
|
34
|
+
reason: 'admin-login' | 'locked' | 'project-login';
|
|
35
|
+
};
|
|
36
|
+
export declare function authStorePath(): string;
|
|
37
|
+
export declare function loadAuthStore(): AuthStore;
|
|
38
|
+
export declare function makeVerifier(password: string): Verifier;
|
|
39
|
+
export declare function verifyPassword(v: Verifier | undefined, password: string): boolean;
|
|
40
|
+
export declare function setAdminPassword(password: string): AuthStore;
|
|
41
|
+
export declare function clearAdminPassword(): AuthStore;
|
|
42
|
+
export declare function setProjectPassword(projectId: string, password: string): AuthStore;
|
|
43
|
+
export declare function clearProjectPassword(projectId: string): AuthStore;
|
|
44
|
+
export declare function mintToken(store: AuthStore, scope: Scope, nowMs?: number): string;
|
|
45
|
+
export declare function verifyToken(store: AuthStore, token: string, nowMs?: number): Claims | null;
|
|
46
|
+
export declare function adminCookieName(port: number): string;
|
|
47
|
+
export declare function projectCookieName(port: number, projectId: string): string;
|
|
48
|
+
export declare function cookieOf(header: string | undefined, name: string): string | null;
|
|
49
|
+
export declare function isLoopback(addr: string | null | undefined): boolean;
|
|
50
|
+
export type Route = {
|
|
51
|
+
kind: 'admin';
|
|
52
|
+
} | {
|
|
53
|
+
kind: 'project';
|
|
54
|
+
projectId: string;
|
|
55
|
+
};
|
|
56
|
+
export declare function authorize(store: AuthStore, route: Route, cookieHeader: string | undefined, remoteAddr: string | null | undefined, port: number): Decision;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { createHmac, createHash, randomBytes, scryptSync, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { spexcodeHome } from '@spexcode/spec-core';
|
|
5
|
+
const TOKEN_TTL_MS = 30 * 24 * 3600 * 1000; // matches the single-project gateway's 30-day cookie
|
|
6
|
+
const SCRYPT = { N: 16384, r: 8, p: 1 };
|
|
7
|
+
// ---- the private per-user store --------------------------------------------------------------------
|
|
8
|
+
export function authStorePath() {
|
|
9
|
+
return join(spexcodeHome(), 'gateway', 'auth.json');
|
|
10
|
+
}
|
|
11
|
+
// first load creates the store (and its random signing secret) so sessions survive restarts; the file is
|
|
12
|
+
// 0600 in a 0700 dir — this is a per-USER secret store, no other principal has business reading it.
|
|
13
|
+
export function loadAuthStore() {
|
|
14
|
+
try {
|
|
15
|
+
const raw = JSON.parse(readFileSync(authStorePath(), 'utf8'));
|
|
16
|
+
if (raw?.v === 1 && typeof raw.secret === 'string' && raw.secret)
|
|
17
|
+
return { v: 1, secret: raw.secret, admin: raw.admin, projects: raw.projects ?? {} };
|
|
18
|
+
}
|
|
19
|
+
catch { /* absent or unreadable → fresh below; malformed secrets must not be half-trusted */ }
|
|
20
|
+
const fresh = { v: 1, secret: randomBytes(32).toString('base64url'), projects: {} };
|
|
21
|
+
saveAuthStore(fresh);
|
|
22
|
+
return fresh;
|
|
23
|
+
}
|
|
24
|
+
function saveAuthStore(store) {
|
|
25
|
+
const file = authStorePath();
|
|
26
|
+
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
27
|
+
const tmp = `${file}.tmp`;
|
|
28
|
+
writeFileSync(tmp, JSON.stringify(store, null, 2) + '\n', { mode: 0o600 });
|
|
29
|
+
renameSync(tmp, file);
|
|
30
|
+
chmodSync(file, 0o600); // rename preserves the tmp mode, but never let a pre-existing looser file win
|
|
31
|
+
}
|
|
32
|
+
// ---- password verifiers (scrypt, salted, constant-time) --------------------------------------------
|
|
33
|
+
export function makeVerifier(password) {
|
|
34
|
+
const salt = randomBytes(16);
|
|
35
|
+
const hash = scryptSync(password, salt, 32, SCRYPT);
|
|
36
|
+
return { algo: 'scrypt', ...SCRYPT, salt: salt.toString('base64url'), hash: hash.toString('base64url'), gen: randomBytes(8).toString('base64url') };
|
|
37
|
+
}
|
|
38
|
+
export function verifyPassword(v, password) {
|
|
39
|
+
if (!v || v.algo !== 'scrypt' || typeof password !== 'string')
|
|
40
|
+
return false;
|
|
41
|
+
const got = scryptSync(password, Buffer.from(v.salt, 'base64url'), 32, { N: v.N, r: v.r, p: v.p });
|
|
42
|
+
return constEq(got, Buffer.from(v.hash, 'base64url'));
|
|
43
|
+
}
|
|
44
|
+
function constEq(a, b) {
|
|
45
|
+
const ab = Buffer.isBuffer(a) ? a : Buffer.from(a);
|
|
46
|
+
const bb = Buffer.isBuffer(b) ? b : Buffer.from(b);
|
|
47
|
+
return ab.length === bb.length && timingSafeEqual(ab, bb);
|
|
48
|
+
}
|
|
49
|
+
// ---- verifier management (the gateway admin APIs write through these) ------------------------------
|
|
50
|
+
export function setAdminPassword(password) {
|
|
51
|
+
const store = loadAuthStore();
|
|
52
|
+
store.admin = makeVerifier(password);
|
|
53
|
+
saveAuthStore(store);
|
|
54
|
+
return store;
|
|
55
|
+
}
|
|
56
|
+
export function clearAdminPassword() {
|
|
57
|
+
const store = loadAuthStore();
|
|
58
|
+
delete store.admin;
|
|
59
|
+
saveAuthStore(store);
|
|
60
|
+
return store;
|
|
61
|
+
}
|
|
62
|
+
export function setProjectPassword(projectId, password) {
|
|
63
|
+
const store = loadAuthStore();
|
|
64
|
+
store.projects[projectId] = makeVerifier(password);
|
|
65
|
+
saveAuthStore(store);
|
|
66
|
+
return store;
|
|
67
|
+
}
|
|
68
|
+
export function clearProjectPassword(projectId) {
|
|
69
|
+
const store = loadAuthStore();
|
|
70
|
+
delete store.projects[projectId];
|
|
71
|
+
saveAuthStore(store);
|
|
72
|
+
return store;
|
|
73
|
+
}
|
|
74
|
+
// ---- signed session tokens --------------------------------------------------------------------------
|
|
75
|
+
function sign(secret, payload) {
|
|
76
|
+
return createHmac('sha256', Buffer.from(secret, 'base64url')).update(payload).digest('base64url');
|
|
77
|
+
}
|
|
78
|
+
// a token exists only for a scope that has a verifier — its `g` claim is that verifier's current gen.
|
|
79
|
+
export function mintToken(store, scope, nowMs = Date.now()) {
|
|
80
|
+
const v = scope.s === 'admin' ? store.admin : store.projects[scope.p];
|
|
81
|
+
if (!v)
|
|
82
|
+
throw new Error(`gateway-auth: cannot mint a ${scope.s} token with no verifier configured`);
|
|
83
|
+
const claims = { v: 1, s: scope.s, ...(scope.s === 'project' ? { p: scope.p } : {}), g: v.gen, t: nowMs };
|
|
84
|
+
const payload = Buffer.from(JSON.stringify(claims)).toString('base64url');
|
|
85
|
+
return `${payload}.${sign(store.secret, payload)}`;
|
|
86
|
+
}
|
|
87
|
+
// full validity lives here: signature (constant-time), shape, expiry, and the gen check against the
|
|
88
|
+
// CURRENT verifier — a cleared or re-set password leaves no valid tokens behind.
|
|
89
|
+
export function verifyToken(store, token, nowMs = Date.now()) {
|
|
90
|
+
const dot = token.indexOf('.');
|
|
91
|
+
if (dot <= 0)
|
|
92
|
+
return null;
|
|
93
|
+
const payload = token.slice(0, dot), sig = token.slice(dot + 1);
|
|
94
|
+
if (!constEq(sig, sign(store.secret, payload)))
|
|
95
|
+
return null;
|
|
96
|
+
let claims;
|
|
97
|
+
try {
|
|
98
|
+
claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
if (claims?.v !== 1 || typeof claims.t !== 'number' || typeof claims.g !== 'string')
|
|
104
|
+
return null;
|
|
105
|
+
if (nowMs > claims.t + TOKEN_TTL_MS || claims.t > nowMs + 60_000)
|
|
106
|
+
return null;
|
|
107
|
+
if (claims.s === 'admin')
|
|
108
|
+
return store.admin && constEq(claims.g, store.admin.gen) ? claims : null;
|
|
109
|
+
if (claims.s === 'project' && typeof claims.p === 'string') {
|
|
110
|
+
const v = store.projects[claims.p];
|
|
111
|
+
return v && constEq(claims.g, v.gen) ? claims : null;
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
// ---- cookies ----------------------------------------------------------------------------------------
|
|
116
|
+
// Cookie names are keyed by the gateway's public port (cookies are host-scoped; the port is the
|
|
117
|
+
// discriminator — same rationale as the single-project gateway) and, for project scopes, by a hash of the
|
|
118
|
+
// projectId (ids are path-derived and may hold non-token chars). The NAME is only a mailbox: authorization
|
|
119
|
+
// always re-validates the token's own projectId claim against the requested route — never the cookie's
|
|
120
|
+
// name, and never its Path attribute, both of which are client-controlled.
|
|
121
|
+
export function adminCookieName(port) { return `spex_admin_${port}`; }
|
|
122
|
+
export function projectCookieName(port, projectId) {
|
|
123
|
+
return `spex_proj_${port}_${createHash('sha256').update(projectId).digest('hex').slice(0, 12)}`;
|
|
124
|
+
}
|
|
125
|
+
export function cookieOf(header, name) {
|
|
126
|
+
for (const part of (header ?? '').split(';')) {
|
|
127
|
+
const eq = part.indexOf('=');
|
|
128
|
+
if (eq > 0 && part.slice(0, eq).trim() === name)
|
|
129
|
+
return decodeURIComponent(part.slice(eq + 1).trim());
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
// ---- loopback ---------------------------------------------------------------------------------------
|
|
134
|
+
// The implicit-admin decision reads the SOCKET's remote address only. Never a header (X-Forwarded-For,
|
|
135
|
+
// X-Real-IP): headers are attacker-controlled, and this gateway is designed to face the internet directly,
|
|
136
|
+
// not to sit behind a trusted proxy.
|
|
137
|
+
export function isLoopback(addr) {
|
|
138
|
+
if (!addr)
|
|
139
|
+
return false;
|
|
140
|
+
const a = addr.startsWith('::ffff:') ? addr.slice(7) : addr;
|
|
141
|
+
return a === '::1' || /^127(\.\d{1,3}){3}$/.test(a);
|
|
142
|
+
}
|
|
143
|
+
export function authorize(store, route, cookieHeader, remoteAddr, port) {
|
|
144
|
+
const adminTok = cookieOf(cookieHeader, adminCookieName(port));
|
|
145
|
+
const adminClaims = adminTok ? verifyToken(store, adminTok) : null;
|
|
146
|
+
const adminOk = adminClaims?.s === 'admin';
|
|
147
|
+
if (route.kind === 'admin') {
|
|
148
|
+
if (adminOk)
|
|
149
|
+
return { ok: true, via: 'admin' };
|
|
150
|
+
// no admin verifier configured: loopback manages implicitly (the bootstrap path — set the first
|
|
151
|
+
// password from the machine itself); anyone else finds /projects locked, not open.
|
|
152
|
+
if (!store.admin)
|
|
153
|
+
return isLoopback(remoteAddr) ? { ok: true, via: 'loopback' } : { ok: false, reason: 'locked' };
|
|
154
|
+
return { ok: false, reason: 'admin-login' };
|
|
155
|
+
}
|
|
156
|
+
// project route — admin reaches every project; then the project's own gate decides.
|
|
157
|
+
if (adminOk)
|
|
158
|
+
return { ok: true, via: 'admin' };
|
|
159
|
+
if (!store.projects[route.projectId])
|
|
160
|
+
return { ok: true, via: 'open' };
|
|
161
|
+
const tok = cookieOf(cookieHeader, projectCookieName(port, route.projectId));
|
|
162
|
+
const claims = tok ? verifyToken(store, tok) : null;
|
|
163
|
+
// the projectId CLAIM must match the route — presenting project A's token under project B's cookie
|
|
164
|
+
// name (or any Path trick) authorizes nothing.
|
|
165
|
+
if (claims?.s === 'project' && claims.p === route.projectId)
|
|
166
|
+
return { ok: true, via: 'project' };
|
|
167
|
+
return { ok: false, reason: 'project-login' };
|
|
168
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import { type AuthStore } from './gateway-auth.js';
|
|
3
|
+
import { type ResolvedIdentity } from '@spexcode/spec-core';
|
|
4
|
+
export type HubProject = {
|
|
5
|
+
id: string;
|
|
6
|
+
identity: ResolvedIdentity;
|
|
7
|
+
url: string;
|
|
8
|
+
port: number;
|
|
9
|
+
gated: boolean;
|
|
10
|
+
};
|
|
11
|
+
export type HubExtensions = {
|
|
12
|
+
listProjects?: (store: AuthStore) => Promise<object[]> | object[];
|
|
13
|
+
adminRoute?: (req: http.IncomingMessage, res: http.ServerResponse, path: string) => Promise<boolean> | boolean;
|
|
14
|
+
fallback?: (req: http.IncomingMessage, res: http.ServerResponse, path: string) => void;
|
|
15
|
+
};
|
|
16
|
+
export type HubOpts = {
|
|
17
|
+
port: number;
|
|
18
|
+
host?: string;
|
|
19
|
+
tls?: {
|
|
20
|
+
cert: string;
|
|
21
|
+
key: string;
|
|
22
|
+
} | null;
|
|
23
|
+
label?: string;
|
|
24
|
+
onBindFail?: () => void;
|
|
25
|
+
extensions?: HubExtensions;
|
|
26
|
+
};
|
|
27
|
+
export declare function listHubProjects(store: AuthStore): HubProject[];
|
|
28
|
+
export declare function startHubGateway(opts: HubOpts): http.Server;
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
// @@@ gateway hub - the multi-project face of [[public-mode]]: ONE gateway fronting every project backend
|
|
2
|
+
// this user runs. The backends stay loopback internal services (each `spex serve` records its endpoint in
|
|
3
|
+
// ~/.spexcode/projects/<enc>/backend.json — that record IS the hub's registry); the hub terminates the
|
|
4
|
+
// outside world, decides authorization ([[gateway-auth]] — the single mechanism, two signed scopes), and
|
|
5
|
+
// reverse-proxies into the matching loopback backend with the gateway's own cookies STRIPPED, so a backend
|
|
6
|
+
// never sees a credential.
|
|
7
|
+
//
|
|
8
|
+
// The route contract:
|
|
9
|
+
// /login /logout admin session (the designed login page)
|
|
10
|
+
// GET /projects admin: list the registry (+ gating state); with a host shell
|
|
11
|
+
// mounted, an explicit text/html GET gets the Projects UI instead
|
|
12
|
+
// PUT|DELETE /projects/admin-password admin: set/clear the admin password
|
|
13
|
+
// PUT|DELETE /projects/:id/password admin: set/clear one project's password
|
|
14
|
+
// /p/:projectId/login|logout project session for that project
|
|
15
|
+
// ANY /p/:projectId/* (+ WS upgrade) authorized → proxied, prefix-stripped, to that project's backend
|
|
16
|
+
// Authorization never trusts the cookie's name or Path — the token's projectId claim is validated against
|
|
17
|
+
// the :projectId in the URL on every request (see gateway-auth.ts).
|
|
18
|
+
import http from 'node:http';
|
|
19
|
+
import https from 'node:https';
|
|
20
|
+
import net from 'node:net';
|
|
21
|
+
import { readdirSync } from 'node:fs';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
import { adminCookieName, authorize, clearAdminPassword, clearProjectPassword, loadAuthStore, mintToken, projectCookieName, setAdminPassword, setProjectPassword, verifyPassword, } from './gateway-auth.js';
|
|
24
|
+
import { loginPage } from './login-page.js';
|
|
25
|
+
import { listenOrExit } from './listen.js';
|
|
26
|
+
import { installConnectionReaper } from './reaper.js';
|
|
27
|
+
import { spexcodeHome, encodeProject } from '@spexcode/spec-core';
|
|
28
|
+
import { readEndpointRecord } from './host.js';
|
|
29
|
+
import { readGatewayIdentity } from '@spexcode/spec-core';
|
|
30
|
+
import { proxyHttp, proxySessionWeb, proxySessionWebUpgrade } from './gateway.js';
|
|
31
|
+
// ---- registry ---------------------------------------------------------------------------------------
|
|
32
|
+
// A project = a live backend record under ~/.spexcode/projects/<enc>/backend.json (written by supervise.ts
|
|
33
|
+
// at bind time). The <enc> dir name is the projectId. ONE record-read seam ([[host-gateway]]'s
|
|
34
|
+
// readEndpointRecord — the identity-carrying shape): a legacy or torn record is not routable, and a record
|
|
35
|
+
// sitting in a slot its own root does not encode to is not trusted. Only LOOPBACK upstream urls are
|
|
36
|
+
// honored — the hub proxies into this machine's trust boundary, never out to an arbitrary host a crafted
|
|
37
|
+
// record names.
|
|
38
|
+
function upstreamOf(id) {
|
|
39
|
+
const rec = readEndpointRecord(join(spexcodeHome(), 'projects', id, 'backend.json'));
|
|
40
|
+
if (!rec || encodeProject(rec.root) !== id)
|
|
41
|
+
return null;
|
|
42
|
+
let u;
|
|
43
|
+
try {
|
|
44
|
+
u = new URL(rec.url);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
if (u.protocol !== 'http:' || (u.hostname !== '127.0.0.1' && u.hostname !== 'localhost')) {
|
|
50
|
+
console.error(`[hub] ignoring project '${id}': backend url ${rec.url} is not a loopback http endpoint`);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const port = Number(u.port || 80);
|
|
54
|
+
if (!Number.isInteger(port) || port <= 0)
|
|
55
|
+
return null;
|
|
56
|
+
return { url: rec.url, port, root: rec.root, identity: rec.identity };
|
|
57
|
+
}
|
|
58
|
+
// a projectId arrives as ONE url path segment; reject anything that could re-shape the registry path.
|
|
59
|
+
function validProjectId(id) {
|
|
60
|
+
return id.length > 0 && id.length <= 256 && id !== '.' && id !== '..' &&
|
|
61
|
+
!id.includes('/') && !id.includes('\\') && !id.includes('\0');
|
|
62
|
+
}
|
|
63
|
+
export function listHubProjects(store) {
|
|
64
|
+
const dir = join(spexcodeHome(), 'projects');
|
|
65
|
+
let entries = [];
|
|
66
|
+
try {
|
|
67
|
+
entries = readdirSync(dir);
|
|
68
|
+
}
|
|
69
|
+
catch { /* no projects yet */ }
|
|
70
|
+
const out = [];
|
|
71
|
+
for (const id of entries) {
|
|
72
|
+
const up = upstreamOf(id);
|
|
73
|
+
if (!up)
|
|
74
|
+
continue;
|
|
75
|
+
out.push({ id, identity: up.identity, url: up.url, port: up.port, gated: !!store.projects[id] });
|
|
76
|
+
}
|
|
77
|
+
return out.sort((a, b) => a.id.localeCompare(b.id));
|
|
78
|
+
}
|
|
79
|
+
// ---- helpers ----------------------------------------------------------------------------------------
|
|
80
|
+
function sendJson(res, status, body) {
|
|
81
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
82
|
+
res.end(JSON.stringify(body));
|
|
83
|
+
}
|
|
84
|
+
function sendHtml(res, status, html) {
|
|
85
|
+
res.writeHead(status, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
86
|
+
res.end(html);
|
|
87
|
+
}
|
|
88
|
+
function redirect(res, location, setCookie) {
|
|
89
|
+
res.writeHead(302, { Location: location, ...(setCookie ? { 'Set-Cookie': setCookie } : {}) });
|
|
90
|
+
res.end();
|
|
91
|
+
}
|
|
92
|
+
function readBody(req, limit = 4096) {
|
|
93
|
+
return new Promise((resolve) => {
|
|
94
|
+
let body = '';
|
|
95
|
+
req.on('data', (d) => { body += d; if (body.length > limit) {
|
|
96
|
+
req.destroy();
|
|
97
|
+
resolve('');
|
|
98
|
+
} });
|
|
99
|
+
req.on('end', () => resolve(body));
|
|
100
|
+
req.on('error', () => resolve(''));
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
// the login POST accepts the designed page's form encoding or JSON (the future admin UI)
|
|
104
|
+
function passwordOf(req, body) {
|
|
105
|
+
try {
|
|
106
|
+
return req.headers['content-type']?.includes('application/json')
|
|
107
|
+
? String(JSON.parse(body).password ?? '')
|
|
108
|
+
: new URLSearchParams(body).get('password') ?? '';
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return '';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// the gateway's own cookies never cross into a backend — a backend knows nothing about visitors, and a
|
|
115
|
+
// leaked gateway token in some backend log would be a credential spill. Everything else passes through.
|
|
116
|
+
function stripGatewayCookies(header) {
|
|
117
|
+
return (header ?? '').split(';').map((s) => s.trim())
|
|
118
|
+
.filter((p) => p && !/^spex_(admin|proj|auth)_/i.test(p.slice(0, Math.max(p.indexOf('='), 0))))
|
|
119
|
+
.join('; ');
|
|
120
|
+
}
|
|
121
|
+
function cookieAttrs(secure) {
|
|
122
|
+
return `; HttpOnly; Path=/; SameSite=Lax; Max-Age=2592000${secure ? '; Secure' : ''}`;
|
|
123
|
+
}
|
|
124
|
+
// ---- the hub ----------------------------------------------------------------------------------------
|
|
125
|
+
export function startHubGateway(opts) {
|
|
126
|
+
const secure = !!opts.tls;
|
|
127
|
+
const port = opts.port;
|
|
128
|
+
const ext = opts.extensions ?? {};
|
|
129
|
+
const attrs = cookieAttrs(secure);
|
|
130
|
+
const clearCookie = (name) => `${name}=; HttpOnly; Path=/; Max-Age=0`;
|
|
131
|
+
const handler = async (req, res) => {
|
|
132
|
+
// the store is re-read per request: a password set/clear (this process or another) is live at once,
|
|
133
|
+
// and there is no in-memory session state to invalidate.
|
|
134
|
+
const store = loadAuthStore();
|
|
135
|
+
const rawUrl = req.url || '/';
|
|
136
|
+
const q = rawUrl.indexOf('?');
|
|
137
|
+
const path = q >= 0 ? rawUrl.slice(0, q) : rawUrl;
|
|
138
|
+
const query = q >= 0 ? rawUrl.slice(q) : '';
|
|
139
|
+
const remote = req.socket.remoteAddress;
|
|
140
|
+
const cookies = req.headers.cookie;
|
|
141
|
+
const adminz = () => authorize(store, { kind: 'admin' }, cookies, remote, port);
|
|
142
|
+
// ---- admin surface ----
|
|
143
|
+
if (path === '/')
|
|
144
|
+
return redirect(res, '/projects');
|
|
145
|
+
if (path === '/login') {
|
|
146
|
+
if (!store.admin)
|
|
147
|
+
return req.method === 'POST'
|
|
148
|
+
? sendJson(res, 403, { error: 'no admin password is configured — /projects is manageable from loopback only' })
|
|
149
|
+
: redirect(res, '/projects');
|
|
150
|
+
if (req.method === 'POST') {
|
|
151
|
+
const pw = passwordOf(req, await readBody(req));
|
|
152
|
+
if (verifyPassword(store.admin, pw))
|
|
153
|
+
return redirect(res, '/projects', `${adminCookieName(port)}=${mintToken(store, { s: 'admin' })}${attrs}`);
|
|
154
|
+
return sendHtml(res, 401, loginPage(true));
|
|
155
|
+
}
|
|
156
|
+
return sendHtml(res, 200, loginPage(false));
|
|
157
|
+
}
|
|
158
|
+
if (path === '/logout')
|
|
159
|
+
return redirect(res, '/login', clearCookie(adminCookieName(port)));
|
|
160
|
+
if (path === '/projects' || path.startsWith('/projects/')) {
|
|
161
|
+
// browser navigation (the `/` redirect lands here with Accept: text/html) wants the Projects UI,
|
|
162
|
+
// not the catalog envelope. With a host shell mounted, an explicit text/html GET is served the SPA
|
|
163
|
+
// shell — code, not data, same unauthorized-by-design posture as the fallback seam: every data call
|
|
164
|
+
// the shell makes re-enters the JSON route below with its auth gate intact. API fetches
|
|
165
|
+
// (application/json, */*, no Accept) keep the JSON catalog unchanged, as does the bare hub.
|
|
166
|
+
if (path === '/projects' && req.method === 'GET' && ext.fallback && (req.headers.accept ?? '').includes('text/html')) {
|
|
167
|
+
return ext.fallback(req, res, path);
|
|
168
|
+
}
|
|
169
|
+
const d = adminz();
|
|
170
|
+
if (!d.ok) {
|
|
171
|
+
return d.reason === 'locked'
|
|
172
|
+
? sendJson(res, 403, { error: 'admin surface is locked: no admin password is configured and this is not a loopback connection' })
|
|
173
|
+
: sendJson(res, 401, { error: 'authentication required', login: '/login' });
|
|
174
|
+
}
|
|
175
|
+
if (path === '/projects' && req.method === 'GET') {
|
|
176
|
+
const gateway = readGatewayIdentity();
|
|
177
|
+
return sendJson(res, 200, {
|
|
178
|
+
adminGated: !!store.admin,
|
|
179
|
+
gateway: { ...gateway.identity, revision: gateway.revision },
|
|
180
|
+
projects: await (ext.listProjects ? ext.listProjects(store) : listHubProjects(store)),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
if (path === '/projects/admin-password') {
|
|
184
|
+
if (req.method === 'PUT') {
|
|
185
|
+
const pw = passwordOf(req, await readBody(req));
|
|
186
|
+
if (!pw)
|
|
187
|
+
return sendJson(res, 400, { error: 'body must be JSON {"password": "<non-empty>"}' });
|
|
188
|
+
const next = setAdminPassword(pw);
|
|
189
|
+
// the setter stays authenticated: implicit-loopback access ends the moment the password exists,
|
|
190
|
+
// so hand them a session minted under the new verifier in the same response.
|
|
191
|
+
res.setHeader('Set-Cookie', `${adminCookieName(port)}=${mintToken(next, { s: 'admin' })}${attrs}`);
|
|
192
|
+
return sendJson(res, 200, { ok: true });
|
|
193
|
+
}
|
|
194
|
+
if (req.method === 'DELETE') {
|
|
195
|
+
clearAdminPassword();
|
|
196
|
+
res.setHeader('Set-Cookie', clearCookie(adminCookieName(port)));
|
|
197
|
+
return sendJson(res, 200, { ok: true });
|
|
198
|
+
}
|
|
199
|
+
return sendJson(res, 405, { error: 'PUT or DELETE' });
|
|
200
|
+
}
|
|
201
|
+
const m = path.match(/^\/projects\/([^/]+)\/password$/);
|
|
202
|
+
if (m) {
|
|
203
|
+
const id = decodeURIComponent(m[1]);
|
|
204
|
+
if (!validProjectId(id))
|
|
205
|
+
return sendJson(res, 404, { error: 'unknown project' });
|
|
206
|
+
if (req.method === 'PUT') {
|
|
207
|
+
if (!upstreamOf(id))
|
|
208
|
+
return sendJson(res, 404, { error: 'unknown project' });
|
|
209
|
+
const pw = passwordOf(req, await readBody(req));
|
|
210
|
+
if (!pw)
|
|
211
|
+
return sendJson(res, 400, { error: 'body must be JSON {"password": "<non-empty>"}' });
|
|
212
|
+
setProjectPassword(id, pw);
|
|
213
|
+
return sendJson(res, 200, { ok: true });
|
|
214
|
+
}
|
|
215
|
+
if (req.method === 'DELETE') { // clearing works even for a retired registry entry — the cleanup path
|
|
216
|
+
clearProjectPassword(id);
|
|
217
|
+
return sendJson(res, 200, { ok: true });
|
|
218
|
+
}
|
|
219
|
+
return sendJson(res, 405, { error: 'PUT or DELETE' });
|
|
220
|
+
}
|
|
221
|
+
// host-level admin routes ([[host-gateway]]: /projects/stream, registration, project operations)
|
|
222
|
+
// ride the SAME admin gate — the extension only ever sees an authorized request.
|
|
223
|
+
if (ext.adminRoute && await ext.adminRoute(req, res, path))
|
|
224
|
+
return;
|
|
225
|
+
return sendJson(res, 404, { error: 'not found' });
|
|
226
|
+
}
|
|
227
|
+
// ---- project surface ----
|
|
228
|
+
const pm = path.match(/^\/p\/([^/]+)(\/.*)?$/);
|
|
229
|
+
if (pm) {
|
|
230
|
+
const id = decodeURIComponent(pm[1]);
|
|
231
|
+
if (!validProjectId(id))
|
|
232
|
+
return sendJson(res, 404, { error: 'unknown project' });
|
|
233
|
+
const up = upstreamOf(id);
|
|
234
|
+
if (!up)
|
|
235
|
+
return sendJson(res, 404, { error: 'unknown project' });
|
|
236
|
+
const sub = pm[2] || '/';
|
|
237
|
+
const base = `/p/${encodeURIComponent(id)}`;
|
|
238
|
+
const gated = !!store.projects[id];
|
|
239
|
+
if (sub === '/login') {
|
|
240
|
+
if (!gated)
|
|
241
|
+
return redirect(res, `${base}/`);
|
|
242
|
+
if (req.method === 'POST') {
|
|
243
|
+
const pw = passwordOf(req, await readBody(req));
|
|
244
|
+
if (verifyPassword(store.projects[id], pw)) {
|
|
245
|
+
return redirect(res, `${base}/`, `${projectCookieName(port, id)}=${mintToken(store, { s: 'project', p: id })}${attrs}`);
|
|
246
|
+
}
|
|
247
|
+
return sendHtml(res, 401, loginPage(true, { action: `${base}/login`, heading: 'Project access', sub: `Enter the password for this project.` }));
|
|
248
|
+
}
|
|
249
|
+
return sendHtml(res, 200, loginPage(false, { action: `${base}/login`, heading: 'Project access', sub: `Enter the password for this project.` }));
|
|
250
|
+
}
|
|
251
|
+
if (sub === '/logout')
|
|
252
|
+
return redirect(res, `${base}/login`, clearCookie(projectCookieName(port, id)));
|
|
253
|
+
// browser navigation into the scoped page (`/p/<id>/`, any non-api/non-web sub) is content-negotiated
|
|
254
|
+
// exactly like GET /projects, and — like the fallback it rides — PRE-authorization: the shell is
|
|
255
|
+
// code, not data ([[projects-hub]]'s one credential card renders in-app off the scoped api's 401,
|
|
256
|
+
// and a direct guest must reach that card, not a dead-end redirect). With a host fallback mounted,
|
|
257
|
+
// an explicit text/html GET outside /api and /web serves the SPA shell (its root-absolute assets
|
|
258
|
+
// resolve outside /p onto the same fallback); API/SSE/health fetches, posted-web frames, and WS
|
|
259
|
+
// upgrades keep the auth gate and proxy to their backend untouched.
|
|
260
|
+
if (req.method === 'GET' && !sub.startsWith('/api') && !sub.startsWith('/web/') && ext.fallback && (req.headers.accept ?? '').includes('text/html')) {
|
|
261
|
+
return ext.fallback(req, res, '/');
|
|
262
|
+
}
|
|
263
|
+
const d = authorize(store, { kind: 'project', projectId: id }, cookies, remote, port);
|
|
264
|
+
if (!d.ok) {
|
|
265
|
+
if (sub.startsWith('/api'))
|
|
266
|
+
return sendJson(res, 401, { error: 'authentication required', login: `${base}/login` });
|
|
267
|
+
return redirect(res, `${base}/login`);
|
|
268
|
+
}
|
|
269
|
+
if (proxySessionWeb(req, res, sub + query, up.root))
|
|
270
|
+
return;
|
|
271
|
+
return proxyTo(req, res, up.port, sub + query);
|
|
272
|
+
}
|
|
273
|
+
// outside /projects and /p/: the dashboard shell + assets when a host extension serves them, 404 bare.
|
|
274
|
+
if (ext.fallback)
|
|
275
|
+
return ext.fallback(req, res, path);
|
|
276
|
+
return sendJson(res, 404, { error: 'not found' });
|
|
277
|
+
};
|
|
278
|
+
// a handler throw answers 500 and never becomes an unhandled rejection — the hub owns a public port
|
|
279
|
+
// and must keep serving (same posture as the supervisor's process guards).
|
|
280
|
+
const safe = (req, res) => void handler(req, res).catch((e) => {
|
|
281
|
+
console.error(`[hub] request failed: ${e.message}`);
|
|
282
|
+
if (!res.headersSent)
|
|
283
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
284
|
+
res.end('{"error":"internal error"}');
|
|
285
|
+
});
|
|
286
|
+
const server = secure
|
|
287
|
+
? https.createServer({ cert: opts.tls.cert, key: opts.tls.key }, safe)
|
|
288
|
+
: http.createServer(safe);
|
|
289
|
+
installConnectionReaper(server);
|
|
290
|
+
// the terminal WebSocket rides an HTTP upgrade on a /p/:id path: same authorization, then a raw byte
|
|
291
|
+
// pipe to that project's backend with the prefix stripped and the gateway cookies filtered out.
|
|
292
|
+
server.on('upgrade', (req, socket, head) => {
|
|
293
|
+
const store = loadAuthStore();
|
|
294
|
+
const rawUrl = req.url || '/';
|
|
295
|
+
const q = rawUrl.indexOf('?');
|
|
296
|
+
const path = q >= 0 ? rawUrl.slice(0, q) : rawUrl;
|
|
297
|
+
const pm = path.match(/^\/p\/([^/]+)(\/.*)?$/);
|
|
298
|
+
if (!pm)
|
|
299
|
+
return socket.destroy();
|
|
300
|
+
const id = decodeURIComponent(pm[1]);
|
|
301
|
+
if (!validProjectId(id))
|
|
302
|
+
return socket.destroy();
|
|
303
|
+
const up = upstreamOf(id);
|
|
304
|
+
if (!up)
|
|
305
|
+
return socket.destroy();
|
|
306
|
+
const d = authorize(store, { kind: 'project', projectId: id }, req.headers.cookie, req.socket.remoteAddress, port);
|
|
307
|
+
if (!d.ok)
|
|
308
|
+
return socket.destroy();
|
|
309
|
+
const sub = (pm[2] || '/') + (q >= 0 ? rawUrl.slice(q) : '');
|
|
310
|
+
if (proxySessionWebUpgrade(req, socket, head, sub, up.root))
|
|
311
|
+
return;
|
|
312
|
+
const upstream = net.connect(up.port, '127.0.0.1', () => {
|
|
313
|
+
upstream.write(`${req.method} ${sub} HTTP/1.1\r\n` + filteredRawHeaders(req));
|
|
314
|
+
if (head && head.length)
|
|
315
|
+
upstream.write(head);
|
|
316
|
+
socket.pipe(upstream);
|
|
317
|
+
upstream.pipe(socket);
|
|
318
|
+
});
|
|
319
|
+
const bail = () => { socket.destroy(); upstream.destroy(); };
|
|
320
|
+
socket.on('error', bail);
|
|
321
|
+
upstream.on('error', bail);
|
|
322
|
+
// pair the halves fully (the supervisor's rule): an http server's sockets are allowHalfOpen, so a bare
|
|
323
|
+
// client FIN ('end') would otherwise leave a zombie socket the server can never close — an upgraded
|
|
324
|
+
// stream never half-closes legitimately, so either half's FIN or close tears down both.
|
|
325
|
+
socket.on('end', bail);
|
|
326
|
+
upstream.on('end', bail);
|
|
327
|
+
socket.once('close', () => upstream.destroy());
|
|
328
|
+
upstream.once('close', () => socket.destroy());
|
|
329
|
+
});
|
|
330
|
+
const onListen = () => {
|
|
331
|
+
const scheme = secure ? 'https' : 'http';
|
|
332
|
+
console.log(`[hub] multi-project gateway on ${scheme}://${opts.host ?? '0.0.0.0'}:${port} — /projects + /p/:projectId/*`);
|
|
333
|
+
};
|
|
334
|
+
listenOrExit(server, port, { host: opts.host, label: opts.label ?? 'hub gateway', cleanup: opts.onBindFail, onListen });
|
|
335
|
+
return server;
|
|
336
|
+
}
|
|
337
|
+
// replay an upgrade's headers with the Cookie header rewritten to exclude the gateway's own cookies.
|
|
338
|
+
function filteredRawHeaders(req) {
|
|
339
|
+
let s = '';
|
|
340
|
+
for (let i = 0; i < req.rawHeaders.length; i += 2) {
|
|
341
|
+
const name = req.rawHeaders[i];
|
|
342
|
+
if (name.toLowerCase() === 'cookie') {
|
|
343
|
+
const kept = stripGatewayCookies(req.rawHeaders[i + 1]);
|
|
344
|
+
if (kept)
|
|
345
|
+
s += `Cookie: ${kept}\r\n`;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
s += `${name}: ${req.rawHeaders[i + 1]}\r\n`;
|
|
349
|
+
}
|
|
350
|
+
return s + '\r\n';
|
|
351
|
+
}
|
|
352
|
+
// reverse-proxy one authorized request to a project's loopback backend, prefix already stripped.
|
|
353
|
+
function proxyTo(req, res, upstreamPort, path) {
|
|
354
|
+
const headers = { ...req.headers };
|
|
355
|
+
const kept = stripGatewayCookies(req.headers.cookie);
|
|
356
|
+
if (kept)
|
|
357
|
+
headers.cookie = kept;
|
|
358
|
+
else
|
|
359
|
+
delete headers.cookie;
|
|
360
|
+
proxyHttp(req, res, upstreamPort, path, headers);
|
|
361
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import type { Duplex } from 'node:stream';
|
|
3
|
+
export type PublicConfig = {
|
|
4
|
+
password: string;
|
|
5
|
+
tls: {
|
|
6
|
+
cert: string;
|
|
7
|
+
key: string;
|
|
8
|
+
} | null;
|
|
9
|
+
};
|
|
10
|
+
export declare function resolvePublicConfig(repoRoot: string): PublicConfig | null;
|
|
11
|
+
export declare function resolveDistDir(): string;
|
|
12
|
+
export type GatewayOpts = {
|
|
13
|
+
publicPort: number;
|
|
14
|
+
upstreamPort: number;
|
|
15
|
+
password: string;
|
|
16
|
+
tls: {
|
|
17
|
+
cert: string;
|
|
18
|
+
key: string;
|
|
19
|
+
} | null;
|
|
20
|
+
distDir: string;
|
|
21
|
+
host?: string;
|
|
22
|
+
label?: string;
|
|
23
|
+
onBindFail?: () => void;
|
|
24
|
+
projectRoot?: string;
|
|
25
|
+
};
|
|
26
|
+
export declare function startGateway(opts: GatewayOpts): void;
|
|
27
|
+
export declare function rawHeaders(req: http.IncomingMessage): string;
|
|
28
|
+
export declare function proxyHttp(req: http.IncomingMessage, res: http.ServerResponse, upstreamPort: number, path?: string, headers?: http.OutgoingHttpHeaders, unavailableMessage?: string, upstreamHost?: string): void;
|
|
29
|
+
export declare function proxySessionWeb(req: http.IncomingMessage, res: http.ServerResponse, raw?: string, projectRoot?: string): boolean;
|
|
30
|
+
export declare function proxySessionWebUpgrade(req: http.IncomingMessage, socket: Duplex, head: Buffer, raw?: string, projectRoot?: string): boolean;
|
|
31
|
+
export declare function serveStatic(req: http.IncomingMessage, res: http.ServerResponse, distDir: string, urlPath: string): http.ServerResponse<http.IncomingMessage> | undefined;
|
|
32
|
+
export declare function serveDashboardLocal(opts: {
|
|
33
|
+
port: number;
|
|
34
|
+
apiPort: number;
|
|
35
|
+
host?: string;
|
|
36
|
+
projectRoot?: string;
|
|
37
|
+
}): void;
|