@pi-harness/pi-harness 0.1.23 → 0.1.24
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 +11 -9
- package/apps/web/dist/assets/index-BcLXLKqO.js +24 -0
- package/apps/web/dist/assets/index-xF647pQJ.css +2 -0
- package/apps/web/dist/index.html +2 -2
- package/apps/web/package.json +6 -6
- package/apps/web/profile/cordis.yml +1 -0
- package/apps/web/server-dist/bin.js +27 -2
- package/apps/web/server-dist/bin.js.map +1 -1
- package/apps/web/src/server/bin.ts +27 -2
- package/apps/web/test/bin.test.ts +228 -5
- package/node_modules/@pi-harness/api-gateway/dist/index.d.ts.map +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/index.js +113 -42
- package/node_modules/@pi-harness/api-gateway/dist/index.js.map +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/agent-teams.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/anchored-standard.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/annotation.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/archify.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/at-file.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/auto-mode.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/better-sidebar.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/browser-fetch.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/browser-session.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/canvas-draw.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/change-verifier.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/cleaner.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/cli-notifier.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/code2skill.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/colleague-skill.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/context-doctor.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/context.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/cost-meter.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/dependency-checker.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/docker-sandbox.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/fail-logger.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/genui.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/git-time-capsule.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/graph-memory.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/history-compressor.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/hol-guard.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/i18n-pair.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/image-compressor.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/llm-verifier.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/mcp-client.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/mcp-panel.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/memory.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/mirage-bridge.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/mock-server.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/modlens.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/module-search.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/obsidian-sync.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/openpets.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plan-execute.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-check.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-dev.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-finder.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-radar.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-stars.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/prompt-guard.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/prompt-library.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/readme-gen.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/recall-unread.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/reverse-skill.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/reviewer-bot.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/runtime-doctor.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/secure-audit.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-bookmarks.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-bridge.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-compare.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-export.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-insights.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-search.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/skill-catalog.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/skill-guard.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/sql-lens.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/synapse.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/tab-manager.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/taskboard.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/telemetry-blocker.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/test-harness.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/theme-studio.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/token-guard.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/turn-rewind.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/undo-savepoint.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/vision-toolkit.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/web-research.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/workspace-navigator.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/workspace-search.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/yaml-validator.json +1 -1
- package/node_modules/@pi-harness/api-gateway/package.json +1 -1
- package/node_modules/@pi-harness/cli/dist/main.d.ts +1 -0
- package/node_modules/@pi-harness/cli/dist/main.d.ts.map +1 -1
- package/node_modules/@pi-harness/cli/dist/main.js +11 -0
- package/node_modules/@pi-harness/cli/dist/main.js.map +1 -1
- package/node_modules/@pi-harness/cli/dist/relaunch.d.ts.map +1 -1
- package/node_modules/@pi-harness/cli/dist/relaunch.js +2 -4
- package/node_modules/@pi-harness/cli/dist/relaunch.js.map +1 -1
- package/node_modules/@pi-harness/cli/package.json +1 -1
- package/node_modules/@pi-harness/host-webserver/dist/index.d.ts +1 -0
- package/node_modules/@pi-harness/host-webserver/dist/index.d.ts.map +1 -1
- package/node_modules/@pi-harness/host-webserver/dist/index.js +39 -5
- package/node_modules/@pi-harness/host-webserver/dist/index.js.map +1 -1
- package/node_modules/@pi-harness/host-webserver/package.json +1 -1
- package/node_modules/@pi-harness/web-app/dist/index.d.ts.map +1 -1
- package/node_modules/@pi-harness/web-app/dist/index.js +12 -3
- package/node_modules/@pi-harness/web-app/dist/index.js.map +1 -1
- package/node_modules/@pi-harness/web-app/package.json +1 -1
- package/package.json +7 -7
- package/packages/api-gateway/dist/index.d.ts.map +1 -1
- package/packages/api-gateway/dist/index.js +113 -42
- package/packages/api-gateway/dist/index.js.map +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/agent-teams.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/anchored-standard.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/annotation.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/archify.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/at-file.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/auto-mode.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/better-sidebar.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/browser-fetch.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/browser-session.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/canvas-draw.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/change-verifier.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/cleaner.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/cli-notifier.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/code2skill.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/colleague-skill.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/context-doctor.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/context.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/cost-meter.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/dependency-checker.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/docker-sandbox.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/fail-logger.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/genui.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/git-time-capsule.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/graph-memory.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/history-compressor.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/hol-guard.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/i18n-pair.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/image-compressor.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/llm-verifier.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/mcp-client.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/mcp-panel.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/memory.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/mirage-bridge.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/mock-server.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/modlens.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/module-search.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/obsidian-sync.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/openpets.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/plan-execute.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/plugin-check.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/plugin-dev.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/plugin-finder.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/plugin-radar.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/plugin-stars.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/prompt-guard.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/prompt-library.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/readme-gen.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/recall-unread.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/reverse-skill.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/reviewer-bot.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/runtime-doctor.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/secure-audit.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/session-bookmarks.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/session-bridge.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/session-compare.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/session-export.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/session-insights.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/session-search.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/skill-catalog.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/skill-guard.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/sql-lens.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/synapse.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/tab-manager.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/taskboard.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/telemetry-blocker.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/test-harness.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/theme-studio.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/token-guard.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/turn-rewind.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/undo-savepoint.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/vision-toolkit.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/web-research.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/workspace-navigator.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/workspace-search.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/yaml-validator.json +1 -1
- package/packages/api-gateway/package.json +1 -1
- package/packages/api-gateway/src/index.ts +104 -40
- package/packages/api-gateway/src/marketplace-entries/official/agent-teams.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/anchored-standard.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/annotation.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/archify.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/at-file.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/auto-mode.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/better-sidebar.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/browser-fetch.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/browser-session.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/canvas-draw.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/change-verifier.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/cleaner.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/cli-notifier.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/code2skill.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/colleague-skill.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/context-doctor.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/context.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/cost-meter.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/dependency-checker.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/docker-sandbox.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/fail-logger.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/genui.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/git-time-capsule.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/graph-memory.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/history-compressor.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/hol-guard.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/i18n-pair.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/image-compressor.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/llm-verifier.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/mcp-client.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/mcp-panel.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/memory.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/mirage-bridge.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/mock-server.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/modlens.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/module-search.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/obsidian-sync.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/openpets.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/plan-execute.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/plugin-check.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/plugin-dev.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/plugin-finder.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/plugin-radar.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/plugin-stars.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/prompt-guard.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/prompt-library.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/readme-gen.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/recall-unread.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/reverse-skill.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/reviewer-bot.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/runtime-doctor.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/secure-audit.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/session-bookmarks.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/session-bridge.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/session-compare.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/session-export.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/session-insights.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/session-search.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/skill-catalog.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/skill-guard.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/sql-lens.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/synapse.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/tab-manager.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/taskboard.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/telemetry-blocker.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/test-harness.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/theme-studio.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/token-guard.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/turn-rewind.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/undo-savepoint.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/vision-toolkit.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/web-research.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/workspace-navigator.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/workspace-search.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/yaml-validator.json +1 -1
- package/packages/api-gateway/test/index.test.ts +438 -4
- package/packages/api-gateway/test/packed-package.test.ts +70 -0
- package/packages/api-gateway/test/reference-docs.test.ts +50 -0
- package/packages/api-gateway/test/release-package.test.ts +32 -1
- package/packages/api-gateway/test/release-version.test.ts +40 -0
- package/packages/bundle-web-app/dist/index.d.ts.map +1 -1
- package/packages/bundle-web-app/dist/index.js +12 -3
- package/packages/bundle-web-app/dist/index.js.map +1 -1
- package/packages/bundle-web-app/package.json +1 -1
- package/packages/bundle-web-app/src/index.ts +13 -3
- package/packages/bundle-web-app/test/index.test.ts +83 -2
- package/packages/cli/dist/main.d.ts +1 -0
- package/packages/cli/dist/main.d.ts.map +1 -1
- package/packages/cli/dist/main.js +11 -0
- package/packages/cli/dist/main.js.map +1 -1
- package/packages/cli/dist/relaunch.d.ts.map +1 -1
- package/packages/cli/dist/relaunch.js +2 -4
- package/packages/cli/dist/relaunch.js.map +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/main.ts +11 -0
- package/packages/cli/src/relaunch.ts +2 -3
- package/packages/cli/test/cli.test.ts +3 -1
- package/packages/cli/test/main.test.ts +134 -0
- package/packages/cli/test/relaunch.test.ts +86 -11
- package/packages/host-webserver/dist/index.d.ts +1 -0
- package/packages/host-webserver/dist/index.d.ts.map +1 -1
- package/packages/host-webserver/dist/index.js +39 -5
- package/packages/host-webserver/dist/index.js.map +1 -1
- package/packages/host-webserver/package.json +1 -1
- package/packages/host-webserver/src/index.ts +44 -6
- package/packages/host-webserver/test/index.test.ts +36 -5
- package/apps/web/dist/assets/index-BVI_IS-R.css +0 -2
- package/apps/web/dist/assets/index-Bo9CNI_U.js +0 -22
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { describe, expect, test } from "vitest";
|
|
6
|
+
import { DUPLICATE_SIGNAL_WINDOW_MS } from "../src/main.js";
|
|
6
7
|
import { PI_HARNESS_RESTART_EXIT_CODE, shouldRelaunchForDevelopmentProfile, superviseDevelopmentProcess } from "../src/relaunch.js";
|
|
7
8
|
|
|
8
9
|
describe("development profile relaunch", () => {
|
|
@@ -66,7 +67,8 @@ describe("development profile signal forwarding", () => {
|
|
|
66
67
|
const directory = await mkdtemp(join(tmpdir(), `pi-harness-supervisor-${label}-`));
|
|
67
68
|
const markerPath = join(directory, "started.txt");
|
|
68
69
|
const childPath = join(directory, "child.mjs");
|
|
69
|
-
|
|
70
|
+
// The marker is written last, after the body has installed its handlers. Written first, it announces a child that node would still kill outright with the default disposition, because a signal only becomes catchable once a listener for it exists - which is how this test was seen to observe exit 130 where it expected 31.
|
|
71
|
+
await writeFile(childPath, `import { writeFileSync } from "node:fs"; ${body} writeFileSync(process.argv[2], "started");`, "utf8");
|
|
70
72
|
return { markerPath, childPath };
|
|
71
73
|
}
|
|
72
74
|
|
|
@@ -80,17 +82,90 @@ describe("development profile signal forwarding", () => {
|
|
|
80
82
|
await expect(supervised).resolves.toBe(143);
|
|
81
83
|
});
|
|
82
84
|
|
|
85
|
+
// The vitest worker has no terminal on any of the three descriptors, so the tty state is faked here to pin that relaying does not depend on it.
|
|
86
|
+
function withTerminalStdio<T>(isTTY: boolean, spawnChild: () => T): T {
|
|
87
|
+
const streams = [process.stdin, process.stdout, process.stderr];
|
|
88
|
+
const original = streams.map((stream) => stream.isTTY);
|
|
89
|
+
for (const stream of streams) stream.isTTY = isTTY;
|
|
90
|
+
try {
|
|
91
|
+
return spawnChild();
|
|
92
|
+
} finally {
|
|
93
|
+
for (const [index, stream] of streams.entries()) stream.isTTY = original[index] ?? false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
83
97
|
test.skipIf(process.platform === "win32")(
|
|
84
|
-
"
|
|
98
|
+
"relays SIGINT and SIGHUP to the child whether or not the inherited stdio is a terminal",
|
|
85
99
|
async () => {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
100
|
+
for (const isTTY of [true, false]) {
|
|
101
|
+
for (const [signal, code] of [
|
|
102
|
+
["SIGINT", 31],
|
|
103
|
+
["SIGHUP", 32],
|
|
104
|
+
] as const) {
|
|
105
|
+
const { markerPath, childPath } = await startChild(
|
|
106
|
+
`${signal.toLowerCase()}-${String(isTTY)}`,
|
|
107
|
+
// Exiting with 21 when no signal arrives distinguishes a swallowed relay from a slow one.
|
|
108
|
+
`process.on("SIGINT", () => process.exit(31)); process.on("SIGHUP", () => process.exit(32)); setTimeout(() => process.exit(21), 2_000);`,
|
|
109
|
+
);
|
|
110
|
+
const supervised = withTerminalStdio(isTTY, () => superviseDevelopmentProcess(process.execPath, [childPath, markerPath], { stdio: "ignore" }));
|
|
111
|
+
await expect.poll(() => existsSync(markerPath), { interval: 20, timeout: 5_000 }).toBe(true);
|
|
112
|
+
|
|
113
|
+
expect(process.emit(signal, signal)).toBe(true);
|
|
114
|
+
|
|
115
|
+
await expect(supervised).resolves.toBe(code);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
94
118
|
},
|
|
119
|
+
30_000,
|
|
95
120
|
);
|
|
121
|
+
|
|
122
|
+
// A real `kill -INT -<pgid>` would also hit the vitest worker, so the group broadcast is simulated: the signal is delivered straight to the child (the terminal's copy) and emitted on this process (the copy the supervisor relays), 2ms apart, which matches the sub-millisecond spacing measured between the two copies and is well inside the duplicate window.
|
|
123
|
+
// How many deliveries the child sees is the kernel's decision, not this code's: two SIGINTs that are pending together are coalesced into one, which was observed on roughly one run in five and is why the count is asserted as at least one rather than exactly two. What this test pins is the outcome under a group broadcast - one graceful shutdown, exit 130, no force-quit - and it holds under either delivery count. The window logic itself is pinned deterministically and in-process against the real runCli in main.test.ts.
|
|
124
|
+
test.skipIf(process.platform === "win32")("a group broadcast that reaches the child directly and through the relay shuts the child down once", async () => {
|
|
125
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-supervisor-broadcast-"));
|
|
126
|
+
const markerPath = join(directory, "signals.txt");
|
|
127
|
+
const pidPath = join(directory, "pid.txt");
|
|
128
|
+
const childPath = join(directory, "child.mjs");
|
|
129
|
+
await writeFile(
|
|
130
|
+
childPath,
|
|
131
|
+
// The child mirrors runCli's handler: every delivery is recorded, but a repeat of the same signal inside DUPLICATE_SIGNAL_WINDOW_MS is ignored instead of counting toward the force-quit path.
|
|
132
|
+
`import { appendFileSync, writeFileSync } from "node:fs";
|
|
133
|
+
const marker = process.argv[2];
|
|
134
|
+
let lastSignal;
|
|
135
|
+
let lastSignalAt = 0;
|
|
136
|
+
let signalCount = 0;
|
|
137
|
+
process.on("SIGINT", () => {
|
|
138
|
+
appendFileSync(marker, "delivery\\n");
|
|
139
|
+
const receivedAt = performance.now();
|
|
140
|
+
if (lastSignal === "SIGINT" && receivedAt - lastSignalAt < ${DUPLICATE_SIGNAL_WINDOW_MS}) return;
|
|
141
|
+
lastSignal = "SIGINT";
|
|
142
|
+
lastSignalAt = receivedAt;
|
|
143
|
+
signalCount += 1;
|
|
144
|
+
if (signalCount > 1) {
|
|
145
|
+
appendFileSync(marker, "force-quit\\n");
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
setTimeout(() => {
|
|
149
|
+
appendFileSync(marker, "graceful\\n");
|
|
150
|
+
process.exit(130);
|
|
151
|
+
}, 25);
|
|
152
|
+
});
|
|
153
|
+
writeFileSync(process.argv[3], String(process.pid));
|
|
154
|
+
setInterval(() => {}, 1_000);`,
|
|
155
|
+
"utf8",
|
|
156
|
+
);
|
|
157
|
+
const supervised = superviseDevelopmentProcess(process.execPath, [childPath, markerPath, pidPath], { stdio: "ignore" });
|
|
158
|
+
await expect.poll(() => (existsSync(pidPath) ? readFileSync(pidPath, "utf8") : ""), { interval: 20, timeout: 5_000 }).not.toBe("");
|
|
159
|
+
const childPid = Number(readFileSync(pidPath, "utf8"));
|
|
160
|
+
|
|
161
|
+
process.kill(childPid, "SIGINT");
|
|
162
|
+
await new Promise((resolve) => setTimeout(resolve, 2));
|
|
163
|
+
expect(process.emit("SIGINT", "SIGINT")).toBe(true);
|
|
164
|
+
|
|
165
|
+
await expect(supervised).resolves.toBe(130);
|
|
166
|
+
const events = (await readFile(markerPath, "utf8")).split("\n").filter(Boolean);
|
|
167
|
+
expect(events.filter((event) => event === "delivery").length).toBeGreaterThanOrEqual(1);
|
|
168
|
+
expect(events).toContain("graceful");
|
|
169
|
+
expect(events).not.toContain("force-quit");
|
|
170
|
+
});
|
|
96
171
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAC;AAEpF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAEnD,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,cAAc,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChG;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAEvB,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3C;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,IAAI,CAAC;IACtC,gBAAgB,CAAC,OAAO,EAAE,QAAQ,CAAC,SAAS,CAAC,GAAG,MAAM,IAAI,CAAC;IAC3D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,eAAO,MAAM,uBAAuB,OAAO,CAAC;AAE5C,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf,SAAS,EAAE,SAAS,CAAC;KACtB;CACF;;;mBAqGsB,OAAO,UAAU,eAAe;;AAFvD,wBA4DE"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
|
+
import { hostname as machineHostname, networkInterfaces } from "node:os";
|
|
2
3
|
export const DEFAULT_WEB_SERVER_PORT = 3141;
|
|
3
4
|
function normalizePath(path) {
|
|
4
5
|
if (!path.startsWith("/") || (path.length > 1 && path.endsWith("/")))
|
|
@@ -32,16 +33,48 @@ function parseHttpAuthority(value) {
|
|
|
32
33
|
return undefined;
|
|
33
34
|
return { hostname: url.hostname, port: url.port === "" ? 80 : Number(url.port) };
|
|
34
35
|
}
|
|
35
|
-
//
|
|
36
|
-
function
|
|
36
|
+
// Normalizes a configured host or allowlist entry the same way parseHttpAuthority normalizes a header value, so IPv6 forms and letter case compare equal. Returns undefined for values that are not a valid host.
|
|
37
|
+
function canonicalHostname(host) {
|
|
38
|
+
try {
|
|
39
|
+
return new URL("http://" + hostForUrl(host)).hostname;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// A wildcard bind answers on every local address, so the names a browser may legitimately put in the Host header are this machine's own addresses and hostname. They are read per request because DHCP and interface changes rewrite them while the server runs. An interface-scoped IPv6 address ("fe80::1%en0") is compared without its zone id, which is what a URL host carries.
|
|
46
|
+
function localHostnames() {
|
|
47
|
+
const names = new Set();
|
|
48
|
+
const own = canonicalHostname(machineHostname());
|
|
49
|
+
if (own !== undefined)
|
|
50
|
+
names.add(own);
|
|
51
|
+
for (const entries of Object.values(networkInterfaces())) {
|
|
52
|
+
for (const entry of entries ?? []) {
|
|
53
|
+
const name = canonicalHostname(entry.address.split("%")[0] ?? entry.address);
|
|
54
|
+
if (name !== undefined)
|
|
55
|
+
names.add(name);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return names;
|
|
59
|
+
}
|
|
60
|
+
function isHostnameAllowed(hostname, configuredHost, allowedHosts) {
|
|
61
|
+
if (LOOPBACK_HOSTNAMES.has(hostname))
|
|
62
|
+
return true;
|
|
63
|
+
if (allowedHosts.some((entry) => canonicalHostname(entry) === hostname))
|
|
64
|
+
return true;
|
|
65
|
+
if (!WILDCARD_HOSTS.has(configuredHost))
|
|
66
|
+
return hostname === hostForUrl(configuredHost).toLowerCase();
|
|
67
|
+
return localHostnames().has(hostname);
|
|
68
|
+
}
|
|
69
|
+
// Browsers send the Origin header on every cross-origin request and on all non-GET/HEAD requests, so requiring it to match the (already validated) Host header blocks cross-site requests from pages the user visits. Requests without an Origin header (same-origin navigations, EventSource, curl, Node fetch) pass. The Host check defeats DNS rebinding: a page on attacker.example that resolves to one of this machine's addresses arrives with Host: attacker.example, which is neither loopback, nor the bind address, nor an address or hostname this machine owns. The check stays on for a wildcard bind, where only the machine's own addresses (or an operator-supplied allowedHosts entry) are accepted, because the Origin check cannot help: a rebinding page sends a matching Origin and Host pair.
|
|
70
|
+
function rejectForeignRequest(request, response, configuredHost, boundPort, allowedHosts) {
|
|
37
71
|
const hostHeader = request.headers.host;
|
|
38
72
|
const authority = hostHeader === undefined ? undefined : parseHttpAuthority(hostHeader);
|
|
39
73
|
if (authority === undefined) {
|
|
40
74
|
sendError(response, 400, "Invalid Host header");
|
|
41
75
|
return true;
|
|
42
76
|
}
|
|
43
|
-
|
|
44
|
-
if (!hostnameAllowed || authority.port !== boundPort) {
|
|
77
|
+
if (!isHostnameAllowed(authority.hostname, configuredHost, allowedHosts) || authority.port !== boundPort) {
|
|
45
78
|
sendError(response, 400, "Host header does not match the web server address");
|
|
46
79
|
return true;
|
|
47
80
|
}
|
|
@@ -60,12 +93,13 @@ export default {
|
|
|
60
93
|
async apply(context, config) {
|
|
61
94
|
const host = config.host ?? "127.0.0.1";
|
|
62
95
|
const port = config.port ?? DEFAULT_WEB_SERVER_PORT;
|
|
96
|
+
const allowedHosts = config.allowedHosts ?? [];
|
|
63
97
|
const routes = new Map();
|
|
64
98
|
let fallback;
|
|
65
99
|
// Assigned once listen() completes; tests bind port 0 so the configured port is not the one clients address.
|
|
66
100
|
let boundPort = 0;
|
|
67
101
|
const server = createServer((request, response) => {
|
|
68
|
-
if (rejectForeignRequest(request, response, host, boundPort))
|
|
102
|
+
if (rejectForeignRequest(request, response, host, boundPort, allowedHosts))
|
|
69
103
|
return;
|
|
70
104
|
const path = new URL(request.url ?? "/", "http://" + hostForUrl(host)).pathname;
|
|
71
105
|
const handler = routes.get(path) ?? fallback;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAA6C,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
|
2
|
+
import { hostname as machineHostname, networkInterfaces } from "node:os";
|
|
2
3
|
import type { Context } from "@deepseek-ai/cordis";
|
|
3
4
|
|
|
4
5
|
export interface WebRoute {
|
|
@@ -9,6 +10,8 @@ export interface WebRoute {
|
|
|
9
10
|
export interface WebServerConfig {
|
|
10
11
|
readonly host?: string;
|
|
11
12
|
readonly port?: number;
|
|
13
|
+
// Extra hostnames accepted in the Host header, on top of loopback names, the bound address and (for a wildcard bind) this machine's own addresses and hostname. Needed when the harness is reached through a proxy or a DNS name the machine itself does not know about.
|
|
14
|
+
readonly allowedHosts?: readonly string[];
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
export interface WebServer {
|
|
@@ -67,17 +70,51 @@ function parseHttpAuthority(value: string): Authority | undefined {
|
|
|
67
70
|
return { hostname: url.hostname, port: url.port === "" ? 80 : Number(url.port) };
|
|
68
71
|
}
|
|
69
72
|
|
|
70
|
-
//
|
|
71
|
-
function
|
|
73
|
+
// Normalizes a configured host or allowlist entry the same way parseHttpAuthority normalizes a header value, so IPv6 forms and letter case compare equal. Returns undefined for values that are not a valid host.
|
|
74
|
+
function canonicalHostname(host: string): string | undefined {
|
|
75
|
+
try {
|
|
76
|
+
return new URL("http://" + hostForUrl(host)).hostname;
|
|
77
|
+
} catch {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// A wildcard bind answers on every local address, so the names a browser may legitimately put in the Host header are this machine's own addresses and hostname. They are read per request because DHCP and interface changes rewrite them while the server runs. An interface-scoped IPv6 address ("fe80::1%en0") is compared without its zone id, which is what a URL host carries.
|
|
83
|
+
function localHostnames(): Set<string> {
|
|
84
|
+
const names = new Set<string>();
|
|
85
|
+
const own = canonicalHostname(machineHostname());
|
|
86
|
+
if (own !== undefined) names.add(own);
|
|
87
|
+
for (const entries of Object.values(networkInterfaces())) {
|
|
88
|
+
for (const entry of entries ?? []) {
|
|
89
|
+
const name = canonicalHostname(entry.address.split("%")[0] ?? entry.address);
|
|
90
|
+
if (name !== undefined) names.add(name);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return names;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isHostnameAllowed(hostname: string, configuredHost: string, allowedHosts: readonly string[]): boolean {
|
|
97
|
+
if (LOOPBACK_HOSTNAMES.has(hostname)) return true;
|
|
98
|
+
if (allowedHosts.some((entry) => canonicalHostname(entry) === hostname)) return true;
|
|
99
|
+
if (!WILDCARD_HOSTS.has(configuredHost)) return hostname === hostForUrl(configuredHost).toLowerCase();
|
|
100
|
+
return localHostnames().has(hostname);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Browsers send the Origin header on every cross-origin request and on all non-GET/HEAD requests, so requiring it to match the (already validated) Host header blocks cross-site requests from pages the user visits. Requests without an Origin header (same-origin navigations, EventSource, curl, Node fetch) pass. The Host check defeats DNS rebinding: a page on attacker.example that resolves to one of this machine's addresses arrives with Host: attacker.example, which is neither loopback, nor the bind address, nor an address or hostname this machine owns. The check stays on for a wildcard bind, where only the machine's own addresses (or an operator-supplied allowedHosts entry) are accepted, because the Origin check cannot help: a rebinding page sends a matching Origin and Host pair.
|
|
104
|
+
function rejectForeignRequest(
|
|
105
|
+
request: IncomingMessage,
|
|
106
|
+
response: ServerResponse,
|
|
107
|
+
configuredHost: string,
|
|
108
|
+
boundPort: number,
|
|
109
|
+
allowedHosts: readonly string[],
|
|
110
|
+
): boolean {
|
|
72
111
|
const hostHeader = request.headers.host;
|
|
73
112
|
const authority = hostHeader === undefined ? undefined : parseHttpAuthority(hostHeader);
|
|
74
113
|
if (authority === undefined) {
|
|
75
114
|
sendError(response, 400, "Invalid Host header");
|
|
76
115
|
return true;
|
|
77
116
|
}
|
|
78
|
-
|
|
79
|
-
WILDCARD_HOSTS.has(configuredHost) || LOOPBACK_HOSTNAMES.has(authority.hostname) || authority.hostname === hostForUrl(configuredHost).toLowerCase();
|
|
80
|
-
if (!hostnameAllowed || authority.port !== boundPort) {
|
|
117
|
+
if (!isHostnameAllowed(authority.hostname, configuredHost, allowedHosts) || authority.port !== boundPort) {
|
|
81
118
|
sendError(response, 400, "Host header does not match the web server address");
|
|
82
119
|
return true;
|
|
83
120
|
}
|
|
@@ -96,12 +133,13 @@ export default {
|
|
|
96
133
|
async apply(context: Context, config: WebServerConfig) {
|
|
97
134
|
const host = config.host ?? "127.0.0.1";
|
|
98
135
|
const port = config.port ?? DEFAULT_WEB_SERVER_PORT;
|
|
136
|
+
const allowedHosts = config.allowedHosts ?? [];
|
|
99
137
|
const routes = new Map<string, WebRoute["handler"]>();
|
|
100
138
|
let fallback: WebRoute["handler"] | undefined;
|
|
101
139
|
// Assigned once listen() completes; tests bind port 0 so the configured port is not the one clients address.
|
|
102
140
|
let boundPort = 0;
|
|
103
141
|
const server = createServer((request, response) => {
|
|
104
|
-
if (rejectForeignRequest(request, response, host, boundPort)) return;
|
|
142
|
+
if (rejectForeignRequest(request, response, host, boundPort, allowedHosts)) return;
|
|
105
143
|
const path = new URL(request.url ?? "/", "http://" + hostForUrl(host)).pathname;
|
|
106
144
|
const handler = routes.get(path) ?? fallback;
|
|
107
145
|
if (handler === undefined) {
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import { Context } from "@deepseek-ai/cordis";
|
|
2
2
|
import { request as httpRequest } from "node:http";
|
|
3
|
+
import { hostname as machineHostname, networkInterfaces } from "node:os";
|
|
3
4
|
import { afterEach, describe, expect, test } from "vitest";
|
|
4
5
|
import webServerPlugin, { DEFAULT_WEB_SERVER_PORT, type WebServer } from "../src/index.js";
|
|
5
6
|
|
|
6
7
|
const contexts: Context[] = [];
|
|
7
8
|
|
|
9
|
+
function firstLocalIpv4(): string | undefined {
|
|
10
|
+
return Object.values(networkInterfaces())
|
|
11
|
+
.flatMap((entries) => entries ?? [])
|
|
12
|
+
.find((entry) => entry.family === "IPv4" && !entry.internal)?.address;
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
afterEach(async () => {
|
|
9
16
|
await Promise.all(contexts.splice(0).map(async (context) => context.fiber.dispose()));
|
|
10
17
|
});
|
|
11
18
|
|
|
12
|
-
async function startServer(config: { host?: string; port: number }): Promise<WebServer> {
|
|
19
|
+
async function startServer(config: { host?: string; port: number; allowedHosts?: readonly string[] }): Promise<WebServer> {
|
|
13
20
|
const context = new Context();
|
|
14
21
|
contexts.push(context);
|
|
15
22
|
await context.plugin(webServerPlugin, config);
|
|
@@ -163,12 +170,36 @@ describe("web server plugin", () => {
|
|
|
163
170
|
expect(health.calls).toBe(3);
|
|
164
171
|
});
|
|
165
172
|
|
|
166
|
-
test("accepts
|
|
173
|
+
test("accepts only this machine's own names on a wildcard bind so DNS rebinding still fails", async () => {
|
|
167
174
|
const wildcard = await startServer({ host: "0.0.0.0", port: 0 });
|
|
168
|
-
registerHealth(wildcard);
|
|
169
|
-
|
|
170
|
-
await expect(rawRequest(wildcard, { host: "
|
|
175
|
+
const health = registerHealth(wildcard);
|
|
176
|
+
|
|
177
|
+
await expect(rawRequest(wildcard, { host: "attacker.example:" + wildcard.port })).resolves.toEqual({
|
|
178
|
+
status: 400,
|
|
179
|
+
body: JSON.stringify({ error: "Host header does not match the web server address" }),
|
|
180
|
+
});
|
|
181
|
+
await expect(rawRequest(wildcard, { host: "workstation.lan:" + wildcard.port })).resolves.toMatchObject({ status: 400 });
|
|
182
|
+
expect(health.calls).toBe(0);
|
|
183
|
+
|
|
184
|
+
await expect(rawRequest(wildcard, { host: "127.0.0.1:" + wildcard.port })).resolves.toMatchObject({ status: 200 });
|
|
185
|
+
await expect(rawRequest(wildcard, { host: machineHostname() + ":" + wildcard.port })).resolves.toMatchObject({ status: 200 });
|
|
186
|
+
const localIpv4 = firstLocalIpv4();
|
|
187
|
+
if (localIpv4 !== undefined) await expect(rawRequest(wildcard, { host: localIpv4 + ":" + wildcard.port })).resolves.toMatchObject({ status: 200 });
|
|
188
|
+
await expect(rawRequest(wildcard, { host: machineHostname() + ":" + (wildcard.port + 1) })).resolves.toMatchObject({ status: 400 });
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("accepts hostnames named by allowedHosts on a wildcard bind", async () => {
|
|
192
|
+
const proxied = await startServer({ host: "0.0.0.0", port: 0, allowedHosts: ["Console.lan"] });
|
|
193
|
+
registerHealth(proxied);
|
|
194
|
+
|
|
195
|
+
await expect(rawRequest(proxied, { host: "console.lan:" + proxied.port })).resolves.toMatchObject({ status: 200 });
|
|
196
|
+
await expect(rawRequest(proxied, { host: "console.lan:" + proxied.port, origin: "http://console.lan:" + proxied.port }, "POST")).resolves.toMatchObject({
|
|
197
|
+
status: 200,
|
|
198
|
+
});
|
|
199
|
+
await expect(rawRequest(proxied, { host: "other.lan:" + proxied.port })).resolves.toMatchObject({ status: 400 });
|
|
200
|
+
});
|
|
171
201
|
|
|
202
|
+
test("serves an IPv6 bind at its bracketed URL", async () => {
|
|
172
203
|
const ipv6 = await startServer({ host: "::1", port: 0 });
|
|
173
204
|
registerHealth(ipv6);
|
|
174
205
|
expect(ipv6.url).toBe("http://[::1]:" + ipv6.port);
|