@pi-harness/pi-harness 0.1.24 → 0.1.26
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 +3 -0
- package/apps/web/dist/assets/{index-BcLXLKqO.js → index-BCeWdgv2.js} +3 -2
- package/apps/web/dist/index.html +1 -1
- package/apps/web/package.json +6 -6
- package/node_modules/@pi-harness/api-gateway/dist/index.d.ts.map +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/index.js +67 -47
- 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/package.json +1 -1
- package/node_modules/@pi-harness/host-webserver/dist/index.d.ts.map +1 -1
- package/node_modules/@pi-harness/host-webserver/dist/index.js +4 -2
- 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/package.json +1 -1
- package/package.json +6 -6
- package/packages/api-gateway/dist/index.d.ts.map +1 -1
- package/packages/api-gateway/dist/index.js +67 -47
- 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 +100 -66
- 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 +220 -2
- package/packages/api-gateway/test/release-contract.test.ts +7 -2
- package/packages/api-gateway/test/release-package.test.ts +16 -0
- package/packages/bundle-web-app/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/host-webserver/dist/index.d.ts.map +1 -1
- package/packages/host-webserver/dist/index.js +4 -2
- 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 +4 -2
- package/packages/host-webserver/test/index.test.ts +22 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chmod, mkdir, mkdtemp, readdir, readFile, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
1
|
+
import { chmod, lstat, mkdir, mkdtemp, readdir, readFile, realpath, rm, stat, symlink, writeFile } from "node:fs/promises";
|
|
2
2
|
import { execFile as execFileCallback } from "node:child_process";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import { promisify } from "node:util";
|
|
@@ -13,6 +13,19 @@ import webServerPlugin from "@pi-harness/host-webserver";
|
|
|
13
13
|
import { PiPluginUiRegistry, PiToolRegistry } from "@pi-harness/core";
|
|
14
14
|
import type { MarketplacePlugin } from "../src/marketplace.js";
|
|
15
15
|
import apiPlugin from "../src/index.js";
|
|
16
|
+
import type * as FsPromises from "node:fs/promises";
|
|
17
|
+
|
|
18
|
+
// The gateway reads the session metadata file through node:fs/promises; this pass-through mock lets one test hold a read open after its bytes arrived so lock ordering can be observed deterministically. Everything else goes straight to the real implementation.
|
|
19
|
+
const fsHooks = vi.hoisted(() => ({ afterReadFile: undefined as ((path: string) => Promise<void>) | undefined }));
|
|
20
|
+
vi.mock("node:fs/promises", async (importOriginal) => {
|
|
21
|
+
const actual = await importOriginal<typeof FsPromises>();
|
|
22
|
+
const readFile = async (path: unknown, options?: unknown) => {
|
|
23
|
+
const result: unknown = await (actual.readFile as (path: unknown, options?: unknown) => Promise<unknown>)(path, options);
|
|
24
|
+
if (fsHooks.afterReadFile) await fsHooks.afterReadFile(String(path));
|
|
25
|
+
return result;
|
|
26
|
+
};
|
|
27
|
+
return { ...actual, readFile };
|
|
28
|
+
});
|
|
16
29
|
|
|
17
30
|
// Every shipped catalog entry that npm has to install names a bare package, so `packageName` and its npm package name coincide and the install route cannot show whether it narrows the specifier. This entry is the case where the two differ: a deep import path whose npm package is only the first two segments. It is hoisted because the module mock below reads it while the test module body is still in its temporal dead zone.
|
|
18
31
|
const { subpathPlugin } = vi.hoisted(() => ({
|
|
@@ -43,6 +56,7 @@ vi.mock("../src/marketplace.js", async (importOriginal) => {
|
|
|
43
56
|
const contexts: Context[] = [];
|
|
44
57
|
const temporaryDirectories: string[] = [];
|
|
45
58
|
const execFile = promisify(execFileCallback);
|
|
59
|
+
const sleep = (ms: number) => new Promise<void>((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
46
60
|
|
|
47
61
|
afterEach(async () => {
|
|
48
62
|
await Promise.all(contexts.splice(0).map(async (context) => context.fiber.dispose()));
|
|
@@ -2581,7 +2595,12 @@ describe("API gateway plugin", () => {
|
|
|
2581
2595
|
expect(failed.status).toBe(500);
|
|
2582
2596
|
await expect(failed.json()).resolves.toEqual({ error: "fatal: index file corrupt" });
|
|
2583
2597
|
|
|
2584
|
-
|
|
2598
|
+
// The gateway consumes NUL-separated `--porcelain -z` entries and then asks git for the cwd prefix, so the shim answers both calls.
|
|
2599
|
+
await writeFile(
|
|
2600
|
+
shim,
|
|
2601
|
+
"#!/bin/sh\ncase \"$1\" in rev-parse) exit 0;; esac\nprintf ' M tracked.txt\\0'\nseq 1 40000 | sed 's/^/?? untracked-/;s/$/.txt/' | tr '\\n' '\\0'\n",
|
|
2602
|
+
{ mode: 0o755 },
|
|
2603
|
+
);
|
|
2585
2604
|
const truncated = await fetch(context.webServer.url + "/api/files");
|
|
2586
2605
|
expect(truncated.status).toBe(200);
|
|
2587
2606
|
const payload = (await truncated.json()) as { items: { path: string }[]; truncated?: boolean };
|
|
@@ -3215,4 +3234,203 @@ describe("API gateway plugin", () => {
|
|
|
3215
3234
|
await expect(readFile(join(harnessDir, "package.json"), "utf8")).resolves.toBe('{ "name": "harness" }\n');
|
|
3216
3235
|
await expect(stat(join(harnessDir, "package-lock.json"))).rejects.toMatchObject({ code: "ENOENT" });
|
|
3217
3236
|
});
|
|
3237
|
+
|
|
3238
|
+
test("propagates metadata read errors other than ENOENT instead of quarantining a file it could not read", async () => {
|
|
3239
|
+
const context = new Context();
|
|
3240
|
+
contexts.push(context);
|
|
3241
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-metadata-eisdir-"));
|
|
3242
|
+
temporaryDirectories.push(directory);
|
|
3243
|
+
const metadataFile = join(directory, ".pi-harness-session-meta.json");
|
|
3244
|
+
// A directory at the metadata path makes readFile fail with EISDIR while rename would still succeed, which is exactly the shape of a transient read failure.
|
|
3245
|
+
await mkdir(metadataFile);
|
|
3246
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
3247
|
+
const session = {
|
|
3248
|
+
sessionId: "metadata-session",
|
|
3249
|
+
sessionFile: undefined,
|
|
3250
|
+
messages: [],
|
|
3251
|
+
isStreaming: false,
|
|
3252
|
+
sessionManager: { getSessionDir: () => directory, isPersisted: () => true, getEntries: () => [] },
|
|
3253
|
+
subscribe: () => () => {},
|
|
3254
|
+
};
|
|
3255
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve() } as never);
|
|
3256
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
3257
|
+
context.provide("piHarnessLaunch", { cwd: "/tmp", agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
3258
|
+
await context.plugin(apiPlugin);
|
|
3259
|
+
|
|
3260
|
+
const list = await fetch(context.webServer.url + "/api/sessions");
|
|
3261
|
+
expect(list.status).toBe(500);
|
|
3262
|
+
const listBody = (await list.json()) as { error: string };
|
|
3263
|
+
expect(listBody.error).toContain("EISDIR");
|
|
3264
|
+
const update = await fetch(context.webServer.url + "/api/session/metadata", {
|
|
3265
|
+
method: "POST",
|
|
3266
|
+
headers: { "content-type": "application/json" },
|
|
3267
|
+
body: JSON.stringify({ path: join(directory, "2026-08-30T00-00-00-000Z_pinned.jsonl"), pinned: true }),
|
|
3268
|
+
});
|
|
3269
|
+
expect(update.status).toBe(400);
|
|
3270
|
+
expect((await stat(metadataFile)).isDirectory()).toBe(true);
|
|
3271
|
+
expect((await readdir(directory)).filter((name) => name.startsWith(".pi-harness-session-meta.json.corrupt-"))).toEqual([]);
|
|
3272
|
+
});
|
|
3273
|
+
|
|
3274
|
+
test("serializes the session list read with metadata mutations so a stale quarantine cannot discard a fresh write", async () => {
|
|
3275
|
+
const context = new Context();
|
|
3276
|
+
contexts.push(context);
|
|
3277
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-metadata-race-"));
|
|
3278
|
+
temporaryDirectories.push(directory);
|
|
3279
|
+
const metadataFile = join(directory, ".pi-harness-session-meta.json");
|
|
3280
|
+
const corrupt = '{ "other.jsonl": { "arc';
|
|
3281
|
+
await writeFile(metadataFile, corrupt, "utf8");
|
|
3282
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
3283
|
+
const session = {
|
|
3284
|
+
sessionId: "metadata-session",
|
|
3285
|
+
sessionFile: undefined,
|
|
3286
|
+
messages: [],
|
|
3287
|
+
isStreaming: false,
|
|
3288
|
+
sessionManager: { getSessionDir: () => directory, isPersisted: () => true, getEntries: () => [] },
|
|
3289
|
+
subscribe: () => () => {},
|
|
3290
|
+
};
|
|
3291
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve() } as never);
|
|
3292
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
3293
|
+
context.provide("piHarnessLaunch", { cwd: "/tmp", agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
3294
|
+
await context.plugin(apiPlugin);
|
|
3295
|
+
|
|
3296
|
+
// Hold the first metadata read after its (corrupt) bytes arrived: the reader now owns the lock but has not parsed yet.
|
|
3297
|
+
let release = () => {};
|
|
3298
|
+
const held = new Promise<void>((resolveHeld) => {
|
|
3299
|
+
let first = true;
|
|
3300
|
+
fsHooks.afterReadFile = async (path) => {
|
|
3301
|
+
if (path !== metadataFile || !first) return;
|
|
3302
|
+
first = false;
|
|
3303
|
+
resolveHeld();
|
|
3304
|
+
await new Promise<void>((resolveRelease) => {
|
|
3305
|
+
release = resolveRelease;
|
|
3306
|
+
});
|
|
3307
|
+
};
|
|
3308
|
+
});
|
|
3309
|
+
try {
|
|
3310
|
+
const list = fetch(context.webServer.url + "/api/sessions");
|
|
3311
|
+
await held;
|
|
3312
|
+
const path = join(directory, "2026-08-30T00-00-00-000Z_pinned.jsonl");
|
|
3313
|
+
const update = fetch(context.webServer.url + "/api/session/metadata", {
|
|
3314
|
+
method: "POST",
|
|
3315
|
+
headers: { "content-type": "application/json" },
|
|
3316
|
+
body: JSON.stringify({ path, pinned: true }),
|
|
3317
|
+
});
|
|
3318
|
+
await expect(Promise.race([update.then(() => "settled"), sleep(300).then(() => "pending")])).resolves.toBe("pending");
|
|
3319
|
+
release();
|
|
3320
|
+
expect((await list).status).toBe(200);
|
|
3321
|
+
const updated = await update;
|
|
3322
|
+
expect(updated.status).toBe(200);
|
|
3323
|
+
await expect(updated.json()).resolves.toEqual({ path, metadata: { pinned: true } });
|
|
3324
|
+
expect(JSON.parse(await readFile(metadataFile, "utf8"))).toEqual({ [path]: { pinned: true } });
|
|
3325
|
+
const quarantined = (await readdir(directory)).filter((name) => name.startsWith(".pi-harness-session-meta.json.corrupt-"));
|
|
3326
|
+
expect(quarantined).toHaveLength(1);
|
|
3327
|
+
await expect(readFile(join(directory, quarantined[0] ?? ""), "utf8")).resolves.toBe(corrupt);
|
|
3328
|
+
} finally {
|
|
3329
|
+
fsHooks.afterReadFile = undefined;
|
|
3330
|
+
release();
|
|
3331
|
+
}
|
|
3332
|
+
});
|
|
3333
|
+
|
|
3334
|
+
test("lets git commit outlive the read-only timeout so slow hooks are not killed mid-commit", { timeout: 40_000 }, async () => {
|
|
3335
|
+
const context = new Context();
|
|
3336
|
+
contexts.push(context);
|
|
3337
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-git-slow-commit-"));
|
|
3338
|
+
temporaryDirectories.push(directory);
|
|
3339
|
+
const shimDirectory = join(directory, "bin");
|
|
3340
|
+
await mkdir(shimDirectory);
|
|
3341
|
+
// A commit that takes longer than the 15 s read-only bound, as a pre-commit hook running a test suite would.
|
|
3342
|
+
await writeFile(join(shimDirectory, "git"), '#!/bin/sh\ncase "$1" in commit) sleep 16;; rev-parse) printf abc1234;; esac\nexit 0\n', { mode: 0o755 });
|
|
3343
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
3344
|
+
const session = { sessionId: "commit-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
|
|
3345
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve(), abort: () => Promise.resolve(), dispose: () => Promise.resolve() } as never);
|
|
3346
|
+
context.provide("piModels", { model: { provider: "test", id: "model" }, runtime: { getModels: () => [], getModel: () => undefined } } as never);
|
|
3347
|
+
context.provide("piHarnessLaunch", { cwd: directory, agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
3348
|
+
await context.plugin(apiPlugin);
|
|
3349
|
+
|
|
3350
|
+
const originalPath = process.env.PATH ?? "";
|
|
3351
|
+
process.env.PATH = `${shimDirectory}:${originalPath}`;
|
|
3352
|
+
try {
|
|
3353
|
+
const response = await fetch(context.webServer.url + "/api/files/commit", {
|
|
3354
|
+
method: "POST",
|
|
3355
|
+
headers: { "content-type": "application/json" },
|
|
3356
|
+
body: JSON.stringify({ paths: ["README.md"], message: "Slow hook" }),
|
|
3357
|
+
});
|
|
3358
|
+
expect(response.status).toBe(200);
|
|
3359
|
+
await expect(response.json()).resolves.toEqual({ committed: true, message: "Slow hook", commit: "abc1234" });
|
|
3360
|
+
} finally {
|
|
3361
|
+
process.env.PATH = originalPath;
|
|
3362
|
+
}
|
|
3363
|
+
});
|
|
3364
|
+
|
|
3365
|
+
test("reports workspace paths relative to a subdirectory cwd so diff and revert resolve them", async () => {
|
|
3366
|
+
const context = new Context();
|
|
3367
|
+
contexts.push(context);
|
|
3368
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-files-subdir-"));
|
|
3369
|
+
temporaryDirectories.push(directory);
|
|
3370
|
+
await execFile("git", ["init", "-q"], { cwd: directory });
|
|
3371
|
+
await execFile("git", ["config", "user.email", "pi-harness@test.invalid"], { cwd: directory });
|
|
3372
|
+
await execFile("git", ["config", "user.name", "Pi Harness Test"], { cwd: directory });
|
|
3373
|
+
await mkdir(join(directory, "sub", "nested"), { recursive: true });
|
|
3374
|
+
await writeFile(join(directory, "sub", "nested", "inner.txt"), "before\n", "utf8");
|
|
3375
|
+
await writeFile(join(directory, "root.txt"), "before\n", "utf8");
|
|
3376
|
+
await execFile("git", ["add", "-A"], { cwd: directory });
|
|
3377
|
+
await execFile("git", ["commit", "-qm", "initial"], { cwd: directory });
|
|
3378
|
+
await writeFile(join(directory, "sub", "nested", "inner.txt"), "after\n", "utf8");
|
|
3379
|
+
await writeFile(join(directory, "root.txt"), "after\n", "utf8");
|
|
3380
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
3381
|
+
const session = { sessionId: "files-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
|
|
3382
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve(), abort: () => Promise.resolve(), dispose: () => Promise.resolve() } as never);
|
|
3383
|
+
context.provide("piModels", { model: { provider: "test", id: "model" }, runtime: { getModels: () => [], getModel: () => undefined } } as never);
|
|
3384
|
+
context.provide("piHarnessLaunch", { cwd: join(directory, "sub"), agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
3385
|
+
await context.plugin(apiPlugin);
|
|
3386
|
+
|
|
3387
|
+
const list = await fetch(context.webServer.url + "/api/files");
|
|
3388
|
+
const payload = (await list.json()) as { items: { path: string }[] };
|
|
3389
|
+
expect(payload.items.map((item) => item.path).sort()).toEqual(["../root.txt", "nested/inner.txt"]);
|
|
3390
|
+
const diff = await fetch(context.webServer.url + "/api/files/diff?path=" + encodeURIComponent("nested/inner.txt"));
|
|
3391
|
+
const nestedDiffBody = (await diff.json()) as { path: string; diff: string };
|
|
3392
|
+
expect(nestedDiffBody.path).toBe("nested/inner.txt");
|
|
3393
|
+
expect(nestedDiffBody.diff).toContain("+after");
|
|
3394
|
+
const revert = await fetch(context.webServer.url + "/api/files/revert", {
|
|
3395
|
+
method: "POST",
|
|
3396
|
+
headers: { "content-type": "application/json" },
|
|
3397
|
+
body: JSON.stringify({ paths: ["nested/inner.txt"], confirm: true }),
|
|
3398
|
+
});
|
|
3399
|
+
expect(revert.status).toBe(200);
|
|
3400
|
+
await expect(readFile(join(directory, "sub", "nested", "inner.txt"), "utf8")).resolves.toBe("before\n");
|
|
3401
|
+
await expect(readFile(join(directory, "root.txt"), "utf8")).resolves.toBe("after\n");
|
|
3402
|
+
});
|
|
3403
|
+
|
|
3404
|
+
test("writes through a symlinked settings.json instead of replacing the link with a regular file", async () => {
|
|
3405
|
+
const context = new Context();
|
|
3406
|
+
contexts.push(context);
|
|
3407
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-agent-symlink-"));
|
|
3408
|
+
temporaryDirectories.push(directory);
|
|
3409
|
+
const agentDir = join(directory, "agent");
|
|
3410
|
+
const dotfiles = join(directory, "dotfiles");
|
|
3411
|
+
await mkdir(agentDir);
|
|
3412
|
+
await mkdir(dotfiles);
|
|
3413
|
+
const realSettings = join(dotfiles, "settings.json");
|
|
3414
|
+
const settingsPath = join(agentDir, "settings.json");
|
|
3415
|
+
await writeFile(realSettings, '{"stale":true}\n', "utf8");
|
|
3416
|
+
await symlink(realSettings, settingsPath);
|
|
3417
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
3418
|
+
const settingsManager = new Proxy({}, { get: () => () => ({}) });
|
|
3419
|
+
const session = { sessionId: "config-session", sessionFile: undefined, messages: [], isStreaming: false, settingsManager, subscribe: () => () => {} };
|
|
3420
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve() } as never);
|
|
3421
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
3422
|
+
context.provide("piHarnessLaunch", { cwd: "/tmp", agentDir, args: [], requestExit() {} });
|
|
3423
|
+
await context.plugin(apiPlugin);
|
|
3424
|
+
|
|
3425
|
+
const response = await fetch(context.webServer.url + "/api/config/source", {
|
|
3426
|
+
method: "POST",
|
|
3427
|
+
headers: { "content-type": "application/json" },
|
|
3428
|
+
body: JSON.stringify({ source: '{"defaultProvider":"test"}' }),
|
|
3429
|
+
});
|
|
3430
|
+
expect(response.status).toBe(200);
|
|
3431
|
+
expect((await lstat(settingsPath)).isSymbolicLink()).toBe(true);
|
|
3432
|
+
await expect(readFile(realSettings, "utf8")).resolves.toBe('{\n "defaultProvider": "test"\n}\n');
|
|
3433
|
+
expect((await readdir(dotfiles)).filter((name) => name.endsWith(".tmp"))).toEqual([]);
|
|
3434
|
+
expect((await readdir(agentDir)).filter((name) => name.endsWith(".tmp"))).toEqual([]);
|
|
3435
|
+
});
|
|
3218
3436
|
});
|
|
@@ -58,7 +58,7 @@ describe("release contract", () => {
|
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
test("core declares every package its sources and shipped profiles load", () => {
|
|
61
|
-
const core = readJson("packages/core/package.json") as { name: string; dependencies: Record<string, string> };
|
|
61
|
+
const core = readJson("packages/core/package.json") as { name: string; dependencies: Record<string, string>; peerDependencies: Record<string, string> };
|
|
62
62
|
const specifierPattern =
|
|
63
63
|
/(?:from\s*|import\s*\(\s*|import\.meta\.resolve\(\s*|require(?:\.resolve)?\(\s*)["']([@a-z][^"'\s]*)["']|"(@[a-z0-9._~-]+\/[a-z0-9._~-]+(?:\/[a-z0-9._~-]+)*)"/gu;
|
|
64
64
|
const required = new Map<string, string>();
|
|
@@ -72,6 +72,11 @@ describe("release contract", () => {
|
|
|
72
72
|
}
|
|
73
73
|
const external = [...required].filter(([name]) => !name.startsWith("node:") && !name.includes(":") && name !== core.name);
|
|
74
74
|
expect(external.length).toBeGreaterThan(0);
|
|
75
|
-
|
|
75
|
+
// A runtime that appears in the published type surface is declared as a peer instead, so the consumer owns the single installed copy.
|
|
76
|
+
for (const [name, file] of external)
|
|
77
|
+
expect(
|
|
78
|
+
core.dependencies[name] ?? core.peerDependencies[name],
|
|
79
|
+
`${name} (used by ${file}) is declared by neither ${core.name} dependencies nor peerDependencies`,
|
|
80
|
+
).toBeDefined();
|
|
76
81
|
});
|
|
77
82
|
});
|
|
@@ -7,6 +7,8 @@ const bundledWorkspacePaths = ["api-gateway", "cli", "host-webserver", "bundle-w
|
|
|
7
7
|
const bundledRuntimePackageNames = ["@earendil-works/pi-agent-core", "@earendil-works/pi-ai", "@earendil-works/pi-coding-agent"] as const;
|
|
8
8
|
const bundledWorkspacePackageNames = ["@pi-harness/api-gateway", "@pi-harness/cli", "@pi-harness/host-webserver", "@pi-harness/web-app"] as const;
|
|
9
9
|
const bundledPackageNames = [...bundledRuntimePackageNames, ...bundledWorkspacePackageNames];
|
|
10
|
+
// Runtimes whose types appear in the published surface of @pi-harness/core. npm nests a second copy of a pinned dependency when the consumer wants a different version; a peer resolves to the consumer's single copy instead.
|
|
11
|
+
const corePeerRuntimeNames = ["@deepseek-ai/cordis", "@deepseek-ai/schemastery", "@earendil-works/pi-ai", "@earendil-works/pi-coding-agent"] as const;
|
|
10
12
|
|
|
11
13
|
const readJson = async (path: string): Promise<Record<string, unknown>> =>
|
|
12
14
|
JSON.parse(await readFile(resolve(repositoryRoot, path), "utf8")) as Record<string, unknown>;
|
|
@@ -36,6 +38,20 @@ describe("release package", () => {
|
|
|
36
38
|
expect(workflow).not.toContain("RELEASE_TAG_EXISTS");
|
|
37
39
|
});
|
|
38
40
|
|
|
41
|
+
it("keeps the runtimes in the core type surface as peers so a plugin author resolves one copy", async () => {
|
|
42
|
+
const rootManifest = await readJson("package.json");
|
|
43
|
+
const rootDependencies = rootManifest.dependencies as Record<string, string>;
|
|
44
|
+
const core = await readJson("packages/core/package.json");
|
|
45
|
+
const coreDependencies = (core.dependencies ?? {}) as Record<string, string>;
|
|
46
|
+
const corePeers = (core.peerDependencies ?? {}) as Record<string, string>;
|
|
47
|
+
|
|
48
|
+
expect(Object.keys(corePeers).sort()).toEqual([...corePeerRuntimeNames]);
|
|
49
|
+
for (const name of corePeerRuntimeNames) {
|
|
50
|
+
expect(coreDependencies[name], `${name} must not also be a dependency, which reintroduces the nested copy`).toBeUndefined();
|
|
51
|
+
expect(corePeers[name], `${name} peer range must pin the version the launcher installs`).toBe(rootDependencies[name]);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
39
55
|
it("cleans every compiled workspace output", async () => {
|
|
40
56
|
// `tsc` never removes stale emit, so an output directory that no clean script touches keeps publishing files whose sources are gone.
|
|
41
57
|
const rootManifest = await readJson("package.json");
|
|
@@ -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;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,
|
|
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,wBA8DE"}
|
|
@@ -94,6 +94,8 @@ export default {
|
|
|
94
94
|
const host = config.host ?? "127.0.0.1";
|
|
95
95
|
const port = config.port ?? DEFAULT_WEB_SERVER_PORT;
|
|
96
96
|
const allowedHosts = config.allowedHosts ?? [];
|
|
97
|
+
// A wildcard bind (including the empty host, which listen() accepts) has no hostname of its own, and "http://" + "" is not a parsable URL base, so resolving a request path against it threw out of the request listener. Loopback is the address such a bind always answers on, so it stands in both as the base for request URLs and as the advertised URL.
|
|
98
|
+
const urlHost = WILDCARD_HOSTS.has(host) ? "127.0.0.1" : hostForUrl(host);
|
|
97
99
|
const routes = new Map();
|
|
98
100
|
let fallback;
|
|
99
101
|
// Assigned once listen() completes; tests bind port 0 so the configured port is not the one clients address.
|
|
@@ -101,7 +103,7 @@ export default {
|
|
|
101
103
|
const server = createServer((request, response) => {
|
|
102
104
|
if (rejectForeignRequest(request, response, host, boundPort, allowedHosts))
|
|
103
105
|
return;
|
|
104
|
-
const path = new URL(request.url ?? "/", "http://" +
|
|
106
|
+
const path = new URL(request.url ?? "/", "http://" + urlHost).pathname;
|
|
105
107
|
const handler = routes.get(path) ?? fallback;
|
|
106
108
|
if (handler === undefined) {
|
|
107
109
|
notFound(response);
|
|
@@ -131,7 +133,7 @@ export default {
|
|
|
131
133
|
const service = {
|
|
132
134
|
host,
|
|
133
135
|
port: address.port,
|
|
134
|
-
url: "http://" +
|
|
136
|
+
url: "http://" + urlHost + ":" + address.port,
|
|
135
137
|
register(route) {
|
|
136
138
|
const path = normalizePath(route.path);
|
|
137
139
|
if (routes.has(path))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAA6C,MAAM,WAAW,CAAC;AACpF,OAAO,EAAE,QAAQ,IAAI,eAAe,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAwBzE,MAAM,CAAC,MAAM,uBAAuB,GAAG,IAAI,CAAC;AAQ5C,SAAS,aAAa,CAAC,IAAY;IACjC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,4DAA4D,GAAG,IAAI,CAAC,CAAC;IAC3J,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,SAAS,CAAC,QAAwB,EAAE,MAAc,EAAE,OAAe;IAC1E,QAAQ,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,cAAc,EAAE,iCAAiC,EAAE,CAAC,CAAC;IAClF,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC;AACnD,CAAC;AAED,SAAS,QAAQ,CAAC,QAAwB;IACxC,SAAS,CAAC,QAAQ,EAAE,GAAG,EAAE,WAAW,CAAC,CAAC;AACxC,CAAC;AAED,SAAS,UAAU,CAAC,IAAY;IAC9B,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,IAAI,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;AAC/E,CAAC;AAED,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC;AACxE,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;AAO9D,6PAA6P;AAC7P,SAAS,kBAAkB,CAAC,KAAa;IACvC,IAAI,GAAQ,CAAC;IACb,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,GAAG,CAAC,SAAS,GAAG,KAAK,CAAC,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,GAAG,CAAC,QAAQ,KAAK,EAAE,IAAI,GAAG,CAAC,QAAQ,KAAK,EAAE,IAAI,GAAG,CAAC,QAAQ,KAAK,GAAG,IAAI,GAAG,CAAC,MAAM,KAAK,EAAE,IAAI,GAAG,CAAC,IAAI,KAAK,EAAE,IAAI,GAAG,CAAC,QAAQ,KAAK,EAAE;QAAE,OAAO,SAAS,CAAC;IACxJ,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC;IACxF,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;AACnF,CAAC;AAED,kNAAkN;AAClN,SAAS,iBAAiB,CAAC,IAAY;IACrC,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,SAAS,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC;IACxD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,qXAAqX;AACrX,SAAS,cAAc;IACrB,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,MAAM,GAAG,GAAG,iBAAiB,CAAC,eAAe,EAAE,CAAC,CAAC;IACjD,IAAI,GAAG,KAAK,SAAS;QAAE,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACtC,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,MAAM,CAAC,iBAAiB,EAAE,CAAC,EAAE,CAAC;QACzD,KAAK,MAAM,KAAK,IAAI,OAAO,IAAI,EAAE,EAAE,CAAC;YAClC,MAAM,IAAI,GAAG,iBAAiB,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC;YAC7E,IAAI,IAAI,KAAK,SAAS;gBAAE,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC1C,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,iBAAiB,CAAC,QAAgB,EAAE,cAAsB,EAAE,YAA+B;IAClG,IAAI,kBAAkB,CAAC,GAAG,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IAClD,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,iBAAiB,CAAC,KAAK,CAAC,KAAK,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IACrF,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,cAAc,CAAC;QAAE,OAAO,QAAQ,KAAK,UAAU,CAAC,cAAc,CAAC,CAAC,WAAW,EAAE,CAAC;IACtG,OAAO,cAAc,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;AACxC,CAAC;AAED,qxBAAqxB;AACrxB,SAAS,oBAAoB,CAC3B,OAAwB,EACxB,QAAwB,EACxB,cAAsB,EACtB,SAAiB,EACjB,YAA+B;IAE/B,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC;IACxC,MAAM,SAAS,GAAG,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAC;IACxF,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC5B,SAAS,CAAC,QAAQ,EAAE,GAAG,EAAE,qBAAqB,CAAC,CAAC;QAChD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,CAAC,iBAAiB,CAAC,SAAS,CAAC,QAAQ,EAAE,cAAc,EAAE,YAAY,CAAC,IAAI,SAAS,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QACzG,SAAS,CAAC,QAAQ,EAAE,GAAG,EAAE,mDAAmD,CAAC,CAAC;QAC9E,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC;IACtC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACvC,MAAM,eAAe,GAAG,MAAM,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACtH,IAAI,eAAe,KAAK,SAAS,IAAI,eAAe,CAAC,QAAQ,KAAK,SAAS,CAAC,QAAQ,IAAI,eAAe,CAAC,IAAI,KAAK,SAAS,CAAC,IAAI,EAAE,CAAC;QAChI,SAAS,CAAC,QAAQ,EAAE,GAAG,EAAE,uCAAuC,CAAC,CAAC;QAClE,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,eAAe;IACb,IAAI,EAAE,cAAc;IACpB,KAAK,CAAC,KAAK,CAAC,OAAgB,EAAE,MAAuB;QACnD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,IAAI,WAAW,CAAC;QACxC,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,IAAI,uBAAuB,CAAC;QACpD,MAAM,YAAY,GAAG,MAAM,CAAC,YAAY,IAAI,EAAE,CAAC;QAC/C,8VAA8V;QAC9V,MAAM,OAAO,GAAG,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QAC1E,MAAM,MAAM,GAAG,IAAI,GAAG,EAA+B,CAAC;QACtD,IAAI,QAAyC,CAAC;QAC9C,6GAA6G;QAC7G,IAAI,SAAS,GAAG,CAAC,CAAC;QAClB,MAAM,MAAM,GAAG,YAAY,CAAC,CAAC,OAAO,EAAE,QAAQ,EAAE,EAAE;YAChD,IAAI,oBAAoB,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,YAAY,CAAC;gBAAE,OAAO;YACnF,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,IAAI,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,CAAC,QAAQ,CAAC;YACvE,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC;YAC7C,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;gBAC1B,QAAQ,CAAC,QAAQ,CAAC,CAAC;gBACnB,OAAO;YACT,CAAC;YACD,mLAAmL;YACnL,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;gBACvE,IAAI,QAAQ,CAAC,WAAW,EAAE,CAAC;oBACzB,QAAQ,CAAC,OAAO,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;oBAC5E,OAAO;gBACT,CAAC;gBACD,QAAQ,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,iCAAiC,EAAE,CAAC,CAAC;gBAC/E,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;YAClG,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QACH,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YAC1C,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YAC7B,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE;gBAC7B,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;gBAC5B,OAAO,EAAE,CAAC;YACZ,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;QACjC,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,OAAO,KAAK,QAAQ;YAAE,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;QAChH,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;QACzB,MAAM,OAAO,GAAc;YACzB,IAAI;YACJ,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,GAAG,EAAE,SAAS,GAAG,OAAO,GAAG,GAAG,GAAG,OAAO,CAAC,IAAI;YAC7C,QAAQ,CAAC,KAAK;gBACZ,MAAM,IAAI,GAAG,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBACvC,IAAI,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;oBAAE,MAAM,IAAI,KAAK,CAAC,gCAAgC,GAAG,IAAI,CAAC,CAAC;gBAC/E,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;gBAChC,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YACnC,CAAC;YACD,gBAAgB,CAAC,OAAO;gBACtB,IAAI,QAAQ,KAAK,SAAS;oBAAE,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;gBAClF,QAAQ,GAAG,OAAO,CAAC;gBACnB,OAAO,GAAG,EAAE;oBACV,IAAI,QAAQ,KAAK,OAAO;wBAAE,QAAQ,GAAG,SAAS,CAAC;gBACjD,CAAC,CAAC;YACJ,CAAC;YACD,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;SAClH,CAAC;QACF,OAAO,CAAC,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;QACtC,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;IACpD,CAAC;CACF,CAAC"}
|
|
@@ -134,13 +134,15 @@ export default {
|
|
|
134
134
|
const host = config.host ?? "127.0.0.1";
|
|
135
135
|
const port = config.port ?? DEFAULT_WEB_SERVER_PORT;
|
|
136
136
|
const allowedHosts = config.allowedHosts ?? [];
|
|
137
|
+
// A wildcard bind (including the empty host, which listen() accepts) has no hostname of its own, and "http://" + "" is not a parsable URL base, so resolving a request path against it threw out of the request listener. Loopback is the address such a bind always answers on, so it stands in both as the base for request URLs and as the advertised URL.
|
|
138
|
+
const urlHost = WILDCARD_HOSTS.has(host) ? "127.0.0.1" : hostForUrl(host);
|
|
137
139
|
const routes = new Map<string, WebRoute["handler"]>();
|
|
138
140
|
let fallback: WebRoute["handler"] | undefined;
|
|
139
141
|
// Assigned once listen() completes; tests bind port 0 so the configured port is not the one clients address.
|
|
140
142
|
let boundPort = 0;
|
|
141
143
|
const server = createServer((request, response) => {
|
|
142
144
|
if (rejectForeignRequest(request, response, host, boundPort, allowedHosts)) return;
|
|
143
|
-
const path = new URL(request.url ?? "/", "http://" +
|
|
145
|
+
const path = new URL(request.url ?? "/", "http://" + urlHost).pathname;
|
|
144
146
|
const handler = routes.get(path) ?? fallback;
|
|
145
147
|
if (handler === undefined) {
|
|
146
148
|
notFound(response);
|
|
@@ -169,7 +171,7 @@ export default {
|
|
|
169
171
|
const service: WebServer = {
|
|
170
172
|
host,
|
|
171
173
|
port: address.port,
|
|
172
|
-
url: "http://" +
|
|
174
|
+
url: "http://" + urlHost + ":" + address.port,
|
|
173
175
|
register(route) {
|
|
174
176
|
const path = normalizePath(route.path);
|
|
175
177
|
if (routes.has(path)) throw new Error("Web route already registered: " + path);
|
|
@@ -173,6 +173,7 @@ describe("web server plugin", () => {
|
|
|
173
173
|
test("accepts only this machine's own names on a wildcard bind so DNS rebinding still fails", async () => {
|
|
174
174
|
const wildcard = await startServer({ host: "0.0.0.0", port: 0 });
|
|
175
175
|
const health = registerHealth(wildcard);
|
|
176
|
+
expect(wildcard.url).toBe("http://127.0.0.1:" + wildcard.port);
|
|
176
177
|
|
|
177
178
|
await expect(rawRequest(wildcard, { host: "attacker.example:" + wildcard.port })).resolves.toEqual({
|
|
178
179
|
status: 400,
|
|
@@ -199,6 +200,27 @@ describe("web server plugin", () => {
|
|
|
199
200
|
await expect(rawRequest(proxied, { host: "other.lan:" + proxied.port })).resolves.toMatchObject({ status: 400 });
|
|
200
201
|
});
|
|
201
202
|
|
|
203
|
+
test("canonicalizes configured hostnames the same way as the Host header", async () => {
|
|
204
|
+
// WHATWG URL compresses the IPv6 literal in the Host header, so a bind address or allowlist entry written out in full has to go through the same parser or it can never match.
|
|
205
|
+
const server = await startServer({ host: "127.0.0.1", port: 0, allowedHosts: ["2001:db8:0:0:0:0:0:1", "WORKSTATION.LAN"] });
|
|
206
|
+
registerHealth(server);
|
|
207
|
+
|
|
208
|
+
await expect(rawRequest(server, { host: "[2001:db8::1]:" + server.port })).resolves.toMatchObject({ status: 200 });
|
|
209
|
+
await expect(rawRequest(server, { host: "workstation.lan:" + server.port })).resolves.toMatchObject({ status: 200 });
|
|
210
|
+
await expect(rawRequest(server, { host: "[2001:db8::2]:" + server.port })).resolves.toMatchObject({ status: 400 });
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("serves an empty configured host, which listen() treats as a wildcard bind", async () => {
|
|
214
|
+
// "http://" + "" is not a parsable URL base, so resolving the request path against the raw configured host threw a TypeError out of the request listener and killed the process on the very first request.
|
|
215
|
+
const server = await startServer({ host: "", port: 0 });
|
|
216
|
+
const health = registerHealth(server);
|
|
217
|
+
|
|
218
|
+
expect(server.url).toBe("http://127.0.0.1:" + server.port);
|
|
219
|
+
await expect(rawRequest(server, { host: "127.0.0.1:" + server.port })).resolves.toMatchObject({ status: 200, body: "ok" });
|
|
220
|
+
await expect(rawRequest(server, { host: "evil.example:" + server.port })).resolves.toMatchObject({ status: 400 });
|
|
221
|
+
expect(health.calls).toBe(1);
|
|
222
|
+
});
|
|
223
|
+
|
|
202
224
|
test("serves an IPv6 bind at its bracketed URL", async () => {
|
|
203
225
|
const ipv6 = await startServer({ host: "::1", port: 0 });
|
|
204
226
|
registerHealth(ipv6);
|