pi-lean-dimension 0.5.0 → 0.6.0
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 +5 -3
- package/node_modules/pi-lean-host/AGENTS.md +13 -6
- package/node_modules/pi-lean-host/__tests__/api-learn-fetch-recipe.test.ts +282 -10
- package/node_modules/pi-lean-host/__tests__/api-learn-multi-file.test.ts +8 -16
- package/node_modules/pi-lean-host/__tests__/api-probe.test.ts +276 -279
- package/node_modules/pi-lean-host/__tests__/api-scaffold.test.ts +12 -33
- package/node_modules/pi-lean-host/__tests__/api-toggle.test.ts +14 -12
- package/node_modules/pi-lean-host/__tests__/bootstrap-command.test.ts +1 -7
- package/node_modules/pi-lean-host/__tests__/delete-command.test.ts +1 -12
- package/node_modules/pi-lean-host/__tests__/guide-catalog.test.ts +484 -0
- package/node_modules/pi-lean-host/__tests__/helpers.test.ts +0 -132
- package/node_modules/pi-lean-host/__tests__/oauth-command.test.ts +95 -0
- package/node_modules/pi-lean-host/__tests__/oauth-flow.test.ts +163 -0
- package/node_modules/pi-lean-host/__tests__/oauth-mint.test.ts +79 -6
- package/node_modules/pi-lean-host/__tests__/parse-api-guide.test.ts +4 -409
- package/node_modules/pi-lean-host/__tests__/response-spill.test.ts +0 -1
- package/node_modules/pi-lean-host/__tests__/secrets-command.test.ts +19 -35
- package/node_modules/pi-lean-host/__tests__/smoke.test.ts +1 -82
- package/node_modules/pi-lean-host/__tests__/ssrf-guard.test.ts +98 -0
- package/node_modules/pi-lean-host/__tests__/test-utils.ts +73 -0
- package/node_modules/pi-lean-host/__tests__/tools.test.ts +2 -518
- package/node_modules/pi-lean-host/__tests__/transport.test.ts +178 -31
- package/node_modules/pi-lean-host/__tests__/verify-command.test.ts +1 -8
- package/node_modules/pi-lean-host/__tests__/verify-stamp.test.ts +1 -4
- package/node_modules/pi-lean-host/api-guides/boe/local-helper.test.ts +9 -20
- package/node_modules/pi-lean-host/api-guides/dnb/error-envelope.test.ts +6 -18
- package/node_modules/pi-lean-host/api-guides/dnb/resumption-token.test.ts +6 -18
- package/node_modules/pi-lean-host/api-guides/frost-sensorthings/dotted-key.test.ts +6 -18
- package/node_modules/pi-lean-host/api-guides/github/static-key.test.ts +7 -25
- package/node_modules/pi-lean-host/api-guides/inaturalist/derived-id.test.ts +6 -18
- package/node_modules/pi-lean-host/api-guides/internet-archive/multi-recipe.test.ts +6 -26
- package/node_modules/pi-lean-host/api-guides/stripe/has-more.test.ts +6 -18
- package/node_modules/pi-lean-host/api-guides/telegram-bot/path-auth.test.ts +7 -25
- package/node_modules/pi-lean-host/api-guides/twitch/oauth2.test.ts +12 -47
- package/node_modules/pi-lean-host/api-guides/twitch-user/oauth-user.test.ts +11 -52
- package/node_modules/pi-lean-host/api-guides/usgs/transform.test.ts +10 -27
- package/node_modules/pi-lean-host/api-guides/wikidata-search/numeric-cursor.test.ts +6 -18
- package/node_modules/pi-lean-host/api-guides/wikimedia-action/token-bag.test.ts +9 -20
- package/node_modules/pi-lean-host/core/auth.ts +2 -1
- package/node_modules/pi-lean-host/core/helpers.ts +29 -35
- package/node_modules/pi-lean-host/core/oauth-command.ts +10 -8
- package/node_modules/pi-lean-host/core/oauth-flow.ts +20 -4
- package/node_modules/pi-lean-host/core/parse-api-guide.ts +3 -5
- package/node_modules/pi-lean-host/core/transport.ts +1 -1
- package/node_modules/pi-lean-host/package.json +1 -1
- package/node_modules/pi-lean-host/tools/api-guide.ts +18 -57
- package/node_modules/pi-lean-host/tools/api-learn.ts +20 -49
- package/node_modules/pi-lean-host/tools/api-probe.ts +17 -26
- package/node_modules/pi-lean-host/tools/api-scaffold.ts +20 -42
- package/node_modules/pi-lean-host/tools/utils.ts +69 -0
- package/node_modules/pi-lean-portal/AGENTS.md +4 -3
- package/node_modules/pi-lean-portal/README.md +9 -9
- package/node_modules/pi-lean-portal/__tests__/accessibility-tree.test.ts +0 -15
- package/node_modules/pi-lean-portal/__tests__/browser-install.test.ts +737 -0
- package/node_modules/pi-lean-portal/__tests__/browser-navigate.test.ts +33 -2
- package/node_modules/pi-lean-portal/__tests__/browser-status.test.ts +48 -25
- package/node_modules/pi-lean-portal/__tests__/browser-toggle-profile.test.ts +3 -9
- package/node_modules/pi-lean-portal/__tests__/browser-toggle.test.ts +3 -29
- package/node_modules/pi-lean-portal/__tests__/fetch-backend.test.ts +51 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/__pycache__/mock-python-bridge.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/__pycache__/mock-python-bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/mock-pi.ts +34 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/mock-python-bridge.py +14 -24
- package/node_modules/pi-lean-portal/__tests__/plugin-registry.test.ts +0 -90
- package/node_modules/pi-lean-portal/__tests__/python-adapter.test.ts +32 -125
- package/node_modules/pi-lean-portal/__tests__/router-dispatch.test.ts +12 -170
- package/node_modules/pi-lean-portal/__tests__/router-session.test.ts +39 -151
- package/node_modules/pi-lean-portal/__tests__/web-guides.test.ts +1 -44
- package/node_modules/pi-lean-portal/backends/chromium/index.ts +1 -1
- package/node_modules/pi-lean-portal/backends/firefox/index.ts +1 -1
- package/node_modules/pi-lean-portal/backends/playwright-base/playwright-plugin.ts +1 -1
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/playwright_base.py +43 -57
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/transport.py +6 -21
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_accessibility.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_transport.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_playwright_base_quirks.py +157 -73
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_transport.py +15 -41
- package/node_modules/pi-lean-portal/browser-install.ts +454 -0
- package/node_modules/pi-lean-portal/browser-status.ts +21 -0
- package/node_modules/pi-lean-portal/browser-toggle.ts +13 -11
- package/node_modules/pi-lean-portal/core/fetch-backend.ts +2 -2
- package/node_modules/pi-lean-portal/core/router.ts +7 -32
- package/node_modules/pi-lean-portal/core/shared/session-manager.ts +1 -3
- package/node_modules/pi-lean-portal/index.ts +1 -18
- package/node_modules/pi-lean-portal/package.json +2 -1
- package/node_modules/pi-lean-portal/tools/browser-navigate.ts +6 -2
- package/node_modules/pi-lean-portal/tools/utils.ts +5 -6
- package/node_modules/pi-lean-search/AGENTS.md +5 -3
- package/node_modules/pi-lean-search/__tests__/web-search.test.ts +180 -127
- package/node_modules/pi-lean-search/index.ts +0 -3
- package/node_modules/pi-lean-search/package.json +1 -1
- package/node_modules/pi-lean-search/web-search-tool.ts +22 -13
- package/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +19 -18
- package/node_modules/yaml/browser/dist/nodes/Alias.js +25 -23
- package/node_modules/yaml/dist/compose/resolve-flow-scalar.js +19 -18
- package/node_modules/yaml/dist/nodes/Alias.js +25 -23
- package/node_modules/yaml/package.json +1 -1
- package/package.json +4 -4
|
@@ -43,6 +43,45 @@ import { join } from "node:path";
|
|
|
43
43
|
import http from "node:http";
|
|
44
44
|
import type { AddressInfo } from "node:net";
|
|
45
45
|
|
|
46
|
+
// ═════════════════════════════════════════════════════════
|
|
47
|
+
// Loopback-server + store-isolation scaffolding
|
|
48
|
+
// ═════════════════════════════════════════════════════════
|
|
49
|
+
|
|
50
|
+
/** Start a loopback server on an ephemeral port; `base` is its root URL
|
|
51
|
+
* (with optional path prefix), `port` for final-URL assertions. */
|
|
52
|
+
async function startProbeServer(
|
|
53
|
+
handler: http.RequestListener,
|
|
54
|
+
basePath = "",
|
|
55
|
+
): Promise<{ server: http.Server; base: string; port: number }> {
|
|
56
|
+
const server = http.createServer(handler);
|
|
57
|
+
await new Promise<void>((r) => server.listen(0, r));
|
|
58
|
+
const port = (server.address() as AddressInfo).port;
|
|
59
|
+
return { server, port, base: `http://127.0.0.1:${port}${basePath}` };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Close a probe server and any keep-alive connections it holds. */
|
|
63
|
+
function closeProbeServer(server: http.Server): void {
|
|
64
|
+
server.close();
|
|
65
|
+
server.closeAllConnections?.();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Point a dir-backed module store (secrets/oauth) at a fresh tmp dir;
|
|
69
|
+
* returns the finally-callback that restores the previous dir and
|
|
70
|
+
* removes the tmp dir. */
|
|
71
|
+
function stageStoreDir(
|
|
72
|
+
prefix: string,
|
|
73
|
+
set: (dir: string) => void,
|
|
74
|
+
get: () => string,
|
|
75
|
+
): () => void {
|
|
76
|
+
const tmp = mkdtempSync(join(tmpdir(), prefix));
|
|
77
|
+
const prev = get();
|
|
78
|
+
set(tmp);
|
|
79
|
+
return () => {
|
|
80
|
+
set(prev);
|
|
81
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
46
85
|
describe("summarize", () => {
|
|
47
86
|
it("maps an envelope with an array-valued key to paginate + itemsPath", () => {
|
|
48
87
|
const s = summarize({
|
|
@@ -242,7 +281,7 @@ describe("#4 probe draft carries the verified version", () => {
|
|
|
242
281
|
|
|
243
282
|
it("walkVersions: over-claimed /v3 404 → walks backward to /v2 (via probe)", async () => {
|
|
244
283
|
const urls: string[] = [];
|
|
245
|
-
const server =
|
|
284
|
+
const { server, base } = await startProbeServer((req, res) => {
|
|
246
285
|
urls.push(req.url ?? "");
|
|
247
286
|
if (req.url === "/v3/items") {
|
|
248
287
|
res.writeHead(404);
|
|
@@ -251,9 +290,7 @@ describe("#4 probe draft carries the verified version", () => {
|
|
|
251
290
|
}
|
|
252
291
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
253
292
|
res.end(JSON.stringify({ data: [{ id: 1 }] }));
|
|
254
|
-
});
|
|
255
|
-
await new Promise<void>((r) => server.listen(0, r));
|
|
256
|
-
const base = `http://127.0.0.1:${(server.address() as AddressInfo).port}/v3`;
|
|
293
|
+
}, "/v3");
|
|
257
294
|
try {
|
|
258
295
|
const result = await probe(base, "/items");
|
|
259
296
|
expect(result.status).toBe(200);
|
|
@@ -262,26 +299,22 @@ describe("#4 probe draft carries the verified version", () => {
|
|
|
262
299
|
// base (/v3) + one walk (/v2) — never guesses /v1 upward.
|
|
263
300
|
expect(urls).toEqual(["/v3/items", "/v2/items"]);
|
|
264
301
|
} finally {
|
|
265
|
-
server
|
|
266
|
-
server.closeAllConnections?.();
|
|
302
|
+
closeProbeServer(server);
|
|
267
303
|
}
|
|
268
304
|
});
|
|
269
305
|
|
|
270
306
|
it("base case via probe: apiHost with trailing /v3 → draft path carries /v3", async () => {
|
|
271
|
-
const server =
|
|
307
|
+
const { server, base } = await startProbeServer((_req, res) => {
|
|
272
308
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
273
309
|
res.end(JSON.stringify({ data: [{ id: 1 }], total_count: 10 }));
|
|
274
|
-
});
|
|
275
|
-
await new Promise<void>((r) => server.listen(0, r));
|
|
276
|
-
const base = `http://127.0.0.1:${(server.address() as AddressInfo).port}/v3`;
|
|
310
|
+
}, "/v3");
|
|
277
311
|
try {
|
|
278
312
|
const result = await probe(base, "/cryptocurrency/listings/latest");
|
|
279
313
|
expect(result.status).toBe(200);
|
|
280
314
|
expect(result.url).toBe(`${base}/cryptocurrency/listings/latest`);
|
|
281
315
|
expect(result.draft).toContain("path: /v3/cryptocurrency/listings/latest");
|
|
282
316
|
} finally {
|
|
283
|
-
server
|
|
284
|
-
server.closeAllConnections?.();
|
|
317
|
+
closeProbeServer(server);
|
|
285
318
|
}
|
|
286
319
|
});
|
|
287
320
|
});
|
|
@@ -291,7 +324,7 @@ describe("probe version walk (backward recovery)", () => {
|
|
|
291
324
|
|
|
292
325
|
it("version gaps: /v4 and /v3 404, /v2 live → /v2", async () => {
|
|
293
326
|
const urls: string[] = [];
|
|
294
|
-
const server =
|
|
327
|
+
const { server, base } = await startProbeServer((req, res) => {
|
|
295
328
|
urls.push(req.url ?? "");
|
|
296
329
|
if (req.url === "/v4/items" || req.url === "/v3/items") {
|
|
297
330
|
res.writeHead(404);
|
|
@@ -300,9 +333,7 @@ describe("probe version walk (backward recovery)", () => {
|
|
|
300
333
|
}
|
|
301
334
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
302
335
|
res.end(json({ data: [{ id: 1 }] }));
|
|
303
|
-
});
|
|
304
|
-
await new Promise<void>((r) => server.listen(0, r));
|
|
305
|
-
const base = `http://127.0.0.1:${(server.address() as AddressInfo).port}/v4`;
|
|
336
|
+
}, "/v4");
|
|
306
337
|
try {
|
|
307
338
|
const result = await probe(base, "/items");
|
|
308
339
|
expect(result.status).toBe(200);
|
|
@@ -310,13 +341,12 @@ describe("probe version walk (backward recovery)", () => {
|
|
|
310
341
|
expect(result.draft).toContain("path: /v2/items");
|
|
311
342
|
expect(urls).toEqual(["/v4/items", "/v3/items", "/v2/items"]);
|
|
312
343
|
} finally {
|
|
313
|
-
server
|
|
314
|
-
server.closeAllConnections?.();
|
|
344
|
+
closeProbeServer(server);
|
|
315
345
|
}
|
|
316
346
|
});
|
|
317
347
|
|
|
318
348
|
it("walk hit that redirects notes finalUrl (draft version vs redirect target)", async () => {
|
|
319
|
-
const server =
|
|
349
|
+
const { server, base, port } = await startProbeServer((req, res) => {
|
|
320
350
|
if (req.url === "/v3/items") {
|
|
321
351
|
res.writeHead(404);
|
|
322
352
|
res.end();
|
|
@@ -329,10 +359,7 @@ describe("probe version walk (backward recovery)", () => {
|
|
|
329
359
|
}
|
|
330
360
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
331
361
|
res.end(JSON.stringify({ data: [{ id: 1 }] }));
|
|
332
|
-
});
|
|
333
|
-
await new Promise<void>((r) => server.listen(0, r));
|
|
334
|
-
const port = (server.address() as AddressInfo).port;
|
|
335
|
-
const base = `http://127.0.0.1:${port}/v3`;
|
|
362
|
+
}, "/v3");
|
|
336
363
|
try {
|
|
337
364
|
const result = await probe(base, "/items");
|
|
338
365
|
expect(result.status).toBe(200);
|
|
@@ -340,106 +367,90 @@ describe("probe version walk (backward recovery)", () => {
|
|
|
340
367
|
expect(result.finalUrl).toBe(`http://127.0.0.1:${port}/real/items`);
|
|
341
368
|
expect(result.note ?? "").toContain("verify finalUrl (redirect target)");
|
|
342
369
|
} finally {
|
|
343
|
-
server
|
|
344
|
-
server.closeAllConnections?.();
|
|
370
|
+
closeProbeServer(server);
|
|
345
371
|
}
|
|
346
372
|
});
|
|
347
373
|
|
|
348
374
|
it("same-version 200: no extra requests fired", async () => {
|
|
349
375
|
const urls: string[] = [];
|
|
350
|
-
const server =
|
|
376
|
+
const { server, base } = await startProbeServer((req, res) => {
|
|
351
377
|
urls.push(req.url ?? "");
|
|
352
378
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
353
379
|
res.end(json({ data: [{ id: 1 }] }));
|
|
354
|
-
});
|
|
355
|
-
await new Promise<void>((r) => server.listen(0, r));
|
|
356
|
-
const base = `http://127.0.0.1:${(server.address() as AddressInfo).port}/v3`;
|
|
380
|
+
}, "/v3");
|
|
357
381
|
try {
|
|
358
382
|
const result = await probe(base, "/items");
|
|
359
383
|
expect(result.status).toBe(200);
|
|
360
384
|
expect(urls).toEqual(["/v3/items"]);
|
|
361
385
|
} finally {
|
|
362
|
-
server
|
|
363
|
-
server.closeAllConnections?.();
|
|
386
|
+
closeProbeServer(server);
|
|
364
387
|
}
|
|
365
388
|
});
|
|
366
389
|
|
|
367
390
|
it("walk cap: /v10 with all lower 404 fires at most MAX_VERSION_WALK walks", async () => {
|
|
368
391
|
const urls: string[] = [];
|
|
369
|
-
const server =
|
|
392
|
+
const { server, base } = await startProbeServer((req, res) => {
|
|
370
393
|
urls.push(req.url ?? "");
|
|
371
394
|
res.writeHead(404);
|
|
372
395
|
res.end();
|
|
373
|
-
});
|
|
374
|
-
await new Promise<void>((r) => server.listen(0, r));
|
|
375
|
-
const base = `http://127.0.0.1:${(server.address() as AddressInfo).port}/v10`;
|
|
396
|
+
}, "/v10");
|
|
376
397
|
try {
|
|
377
398
|
const result = await probe(base, "/items");
|
|
378
399
|
expect(result.status).toBe(404);
|
|
379
400
|
// base (v10) + MAX_VERSION_WALK walks (v9…v5); floor = max(10-5,1) = 5.
|
|
380
401
|
expect(urls.length).toBe(MAX_VERSION_WALK + 1);
|
|
381
402
|
} finally {
|
|
382
|
-
server
|
|
383
|
-
server.closeAllConnections?.();
|
|
403
|
+
closeProbeServer(server);
|
|
384
404
|
}
|
|
385
405
|
});
|
|
386
406
|
|
|
387
407
|
it("bare host 404: no probing, walk-skip note (regression of removed forward-guess)", async () => {
|
|
388
408
|
const urls: string[] = [];
|
|
389
|
-
const server =
|
|
409
|
+
const { server, base } = await startProbeServer((req, res) => {
|
|
390
410
|
urls.push(req.url ?? "");
|
|
391
411
|
res.writeHead(404);
|
|
392
412
|
res.end();
|
|
393
413
|
});
|
|
394
|
-
await new Promise<void>((r) => server.listen(0, r));
|
|
395
|
-
const base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
|
|
396
414
|
try {
|
|
397
415
|
const result = await probe(base, "/items");
|
|
398
416
|
expect(result.status).toBe(404);
|
|
399
417
|
expect(result.note ?? "").toContain("no version walk");
|
|
400
418
|
expect(urls).toEqual(["/items"]); // bare fetch only
|
|
401
419
|
} finally {
|
|
402
|
-
server
|
|
403
|
-
server.closeAllConnections?.();
|
|
420
|
+
closeProbeServer(server);
|
|
404
421
|
}
|
|
405
422
|
});
|
|
406
423
|
|
|
407
424
|
it("non-integer pathname host: no walk, walk-skip note", async () => {
|
|
408
425
|
const urls: string[] = [];
|
|
409
|
-
const server =
|
|
426
|
+
const { server, base } = await startProbeServer((req, res) => {
|
|
410
427
|
urls.push(req.url ?? "");
|
|
411
428
|
res.writeHead(404);
|
|
412
429
|
res.end();
|
|
413
|
-
});
|
|
414
|
-
await new Promise<void>((r) => server.listen(0, r));
|
|
415
|
-
const base = `http://127.0.0.1:${(server.address() as AddressInfo).port}/api`;
|
|
430
|
+
}, "/api");
|
|
416
431
|
try {
|
|
417
432
|
const result = await probe(base, "/items");
|
|
418
433
|
expect(result.status).toBe(404);
|
|
419
434
|
expect(result.note ?? "").toContain("no version walk");
|
|
420
435
|
expect(urls).toEqual(["/api/items"]);
|
|
421
436
|
} finally {
|
|
422
|
-
server
|
|
423
|
-
server.closeAllConnections?.();
|
|
437
|
+
closeProbeServer(server);
|
|
424
438
|
}
|
|
425
439
|
});
|
|
426
440
|
|
|
427
441
|
it("version-in-path (mixed convention): no host walk", async () => {
|
|
428
442
|
const urls: string[] = [];
|
|
429
|
-
const server =
|
|
443
|
+
const { server, base } = await startProbeServer((req, res) => {
|
|
430
444
|
urls.push(req.url ?? "");
|
|
431
445
|
res.writeHead(404);
|
|
432
446
|
res.end();
|
|
433
|
-
});
|
|
434
|
-
await new Promise<void>((r) => server.listen(0, r));
|
|
435
|
-
const base = `http://127.0.0.1:${(server.address() as AddressInfo).port}/v3`;
|
|
447
|
+
}, "/v3");
|
|
436
448
|
try {
|
|
437
449
|
const result = await probe(base, "/v3/data");
|
|
438
450
|
expect(result.status).toBe(404);
|
|
439
451
|
expect(urls).toEqual(["/v3/v3/data"]); // single fetch, no host walk
|
|
440
452
|
} finally {
|
|
441
|
-
server
|
|
442
|
-
server.closeAllConnections?.();
|
|
453
|
+
closeProbeServer(server);
|
|
443
454
|
}
|
|
444
455
|
});
|
|
445
456
|
});
|
|
@@ -479,7 +490,7 @@ describe("formatProbeResult docs-first nudge", () => {
|
|
|
479
490
|
|
|
480
491
|
describe("probe redirect handling (live localhost)", () => {
|
|
481
492
|
it("follows a 301 and reports the final URL + parsed body (the /packs → /packs/ case)", async () => {
|
|
482
|
-
const server =
|
|
493
|
+
const { server, base } = await startProbeServer((req, res) => {
|
|
483
494
|
if (req.url === "/packs") {
|
|
484
495
|
res.writeHead(301, { Location: "/packs/" });
|
|
485
496
|
res.end();
|
|
@@ -488,8 +499,6 @@ describe("probe redirect handling (live localhost)", () => {
|
|
|
488
499
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
489
500
|
res.end(JSON.stringify({ data: [{ id: 1 }], meta: { total: 1 } }));
|
|
490
501
|
});
|
|
491
|
-
await new Promise<void>((r) => server.listen(0, r));
|
|
492
|
-
const base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
|
|
493
502
|
try {
|
|
494
503
|
const result = await probe(base, "/packs");
|
|
495
504
|
expect(result.status).toBe(200);
|
|
@@ -500,8 +509,7 @@ describe("probe redirect handling (live localhost)", () => {
|
|
|
500
509
|
expect(result.shape?.suggestedItemsPath).toBe("data");
|
|
501
510
|
expect(result.draft).toContain("path: /packs");
|
|
502
511
|
} finally {
|
|
503
|
-
server
|
|
504
|
-
server.closeAllConnections?.();
|
|
512
|
+
closeProbeServer(server);
|
|
505
513
|
}
|
|
506
514
|
});
|
|
507
515
|
|
|
@@ -509,16 +517,16 @@ describe("probe redirect handling (live localhost)", () => {
|
|
|
509
517
|
describe("probe store-miss note lists provisioned domains", () => {
|
|
510
518
|
it("a missing ref names the other provisioned domains", async () => {
|
|
511
519
|
// Isolated store with one generic provisioned domain (no real hosts).
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
520
|
+
const cleanupStore = stageStoreDir(
|
|
521
|
+
"host-probe-miss-secrets-",
|
|
522
|
+
setSecretsDir,
|
|
523
|
+
getSecretsDir,
|
|
524
|
+
);
|
|
515
525
|
writeSecret("example.com", "api_key", "K");
|
|
516
|
-
const server =
|
|
526
|
+
const { server, base } = await startProbeServer((_req, res) => {
|
|
517
527
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
518
528
|
res.end(JSON.stringify({ data: [{ id: 1 }] }));
|
|
519
529
|
});
|
|
520
|
-
await new Promise<void>((r) => server.listen(0, r));
|
|
521
|
-
const base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
|
|
522
530
|
try {
|
|
523
531
|
const result = await probe(
|
|
524
532
|
base,
|
|
@@ -536,10 +544,8 @@ describe("probe redirect handling (live localhost)", () => {
|
|
|
536
544
|
// Prescriptive: a miss tells the author to pass domain: <one>.
|
|
537
545
|
expect(result.note ?? "").toContain("pass domain:");
|
|
538
546
|
} finally {
|
|
539
|
-
server
|
|
540
|
-
|
|
541
|
-
setSecretsDir(prevDir);
|
|
542
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
547
|
+
closeProbeServer(server);
|
|
548
|
+
cleanupStore();
|
|
543
549
|
}
|
|
544
550
|
});
|
|
545
551
|
});
|
|
@@ -548,9 +554,11 @@ describe("probe redirect handling (live localhost)", () => {
|
|
|
548
554
|
// store — the value never enters the transcript; miss/expiry nudge /api oauth.
|
|
549
555
|
describe("probe useTokenStore (oauth2 bearer injection)", () => {
|
|
550
556
|
it("injects Authorization: Bearer from the token store; value never surfaces", async () => {
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
|
|
557
|
+
const cleanupStore = stageStoreDir(
|
|
558
|
+
"host-probe-token-ok-",
|
|
559
|
+
setOAuthDir,
|
|
560
|
+
getOAuthDir,
|
|
561
|
+
);
|
|
554
562
|
writeToken(
|
|
555
563
|
"example.com",
|
|
556
564
|
"client_credentials",
|
|
@@ -561,14 +569,12 @@ describe("probe redirect handling (live localhost)", () => {
|
|
|
561
569
|
},
|
|
562
570
|
);
|
|
563
571
|
let seenAuth: string | undefined;
|
|
564
|
-
const server =
|
|
572
|
+
const { server, base } = await startProbeServer((req, res) => {
|
|
565
573
|
seenAuth = req.headers.authorization;
|
|
566
574
|
// Echo the header back so the scrub path is exercised too.
|
|
567
575
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
568
576
|
res.end(JSON.stringify({ auth: req.headers.authorization ?? null }));
|
|
569
577
|
});
|
|
570
|
-
await new Promise<void>((r) => server.listen(0, r));
|
|
571
|
-
const base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
|
|
572
578
|
try {
|
|
573
579
|
const result = await probe(
|
|
574
580
|
base,
|
|
@@ -589,25 +595,23 @@ describe("probe redirect handling (live localhost)", () => {
|
|
|
589
595
|
expect(result.raw).not.toContain("tok_secret_value");
|
|
590
596
|
expect(result.note ?? "").not.toContain("tok_secret_value");
|
|
591
597
|
} finally {
|
|
592
|
-
server
|
|
593
|
-
|
|
594
|
-
setOAuthDir(prevDir);
|
|
595
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
598
|
+
closeProbeServer(server);
|
|
599
|
+
cleanupStore();
|
|
596
600
|
}
|
|
597
601
|
});
|
|
598
602
|
|
|
599
603
|
it("a missing token proceeds unauthenticated and nudges /api oauth", async () => {
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
|
|
604
|
+
const cleanupStore = stageStoreDir(
|
|
605
|
+
"host-probe-token-miss-",
|
|
606
|
+
setOAuthDir,
|
|
607
|
+
getOAuthDir,
|
|
608
|
+
);
|
|
603
609
|
let sawAuthHeader = false;
|
|
604
|
-
const server =
|
|
610
|
+
const { server, base } = await startProbeServer((req, res) => {
|
|
605
611
|
sawAuthHeader = req.headers.authorization !== undefined;
|
|
606
612
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
607
613
|
res.end(JSON.stringify({ data: [{ id: 1 }] }));
|
|
608
614
|
});
|
|
609
|
-
await new Promise<void>((r) => server.listen(0, r));
|
|
610
|
-
const base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
|
|
611
615
|
try {
|
|
612
616
|
const result = await probe(
|
|
613
617
|
base,
|
|
@@ -629,17 +633,17 @@ describe("probe redirect handling (live localhost)", () => {
|
|
|
629
633
|
'no cached token for "example.com"; run /api oauth example.com to mint one',
|
|
630
634
|
);
|
|
631
635
|
} finally {
|
|
632
|
-
server
|
|
633
|
-
|
|
634
|
-
setOAuthDir(prevDir);
|
|
635
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
636
|
+
closeProbeServer(server);
|
|
637
|
+
cleanupStore();
|
|
636
638
|
}
|
|
637
639
|
});
|
|
638
640
|
|
|
639
641
|
it("an expired token is not injected; the note nudges --refresh", async () => {
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
642
|
+
const cleanupStore = stageStoreDir(
|
|
643
|
+
"host-probe-token-exp-",
|
|
644
|
+
setOAuthDir,
|
|
645
|
+
getOAuthDir,
|
|
646
|
+
);
|
|
643
647
|
writeToken(
|
|
644
648
|
"example.com",
|
|
645
649
|
"client_credentials",
|
|
@@ -650,13 +654,11 @@ describe("probe redirect handling (live localhost)", () => {
|
|
|
650
654
|
},
|
|
651
655
|
);
|
|
652
656
|
let sawAuthHeader = false;
|
|
653
|
-
const server =
|
|
657
|
+
const { server, base } = await startProbeServer((req, res) => {
|
|
654
658
|
sawAuthHeader = req.headers.authorization !== undefined;
|
|
655
659
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
656
660
|
res.end(JSON.stringify({ data: [{ id: 1 }] }));
|
|
657
661
|
});
|
|
658
|
-
await new Promise<void>((r) => server.listen(0, r));
|
|
659
|
-
const base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
|
|
660
662
|
try {
|
|
661
663
|
const result = await probe(
|
|
662
664
|
base,
|
|
@@ -677,17 +679,17 @@ describe("probe redirect handling (live localhost)", () => {
|
|
|
677
679
|
'token for "example.com" is expired; run /api oauth example.com --refresh',
|
|
678
680
|
);
|
|
679
681
|
} finally {
|
|
680
|
-
server
|
|
681
|
-
|
|
682
|
-
setOAuthDir(prevDir);
|
|
683
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
682
|
+
closeProbeServer(server);
|
|
683
|
+
cleanupStore();
|
|
684
684
|
}
|
|
685
685
|
});
|
|
686
686
|
|
|
687
687
|
it("useTokenStore without grant + tokenUrl is a loud validation error, not a silent miss", async () => {
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
688
|
+
const cleanupStore = stageStoreDir(
|
|
689
|
+
"host-probe-token-badkey-",
|
|
690
|
+
setOAuthDir,
|
|
691
|
+
getOAuthDir,
|
|
692
|
+
);
|
|
691
693
|
try {
|
|
692
694
|
await expect(
|
|
693
695
|
probe(
|
|
@@ -701,8 +703,7 @@ describe("probe redirect handling (live localhost)", () => {
|
|
|
701
703
|
),
|
|
702
704
|
).rejects.toThrow(/auth\.useTokenStore requires auth\.grant/);
|
|
703
705
|
} finally {
|
|
704
|
-
|
|
705
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
706
|
+
cleanupStore();
|
|
706
707
|
}
|
|
707
708
|
});
|
|
708
709
|
|
|
@@ -710,11 +711,16 @@ describe("probe redirect handling (live localhost)", () => {
|
|
|
710
711
|
// secretRefs: { Authorization } + a store token would emit two
|
|
711
712
|
// same-named headers (case-split keys) — fetch merges them into one
|
|
712
713
|
// garbled header. Loud validation, same posture as the slot-key check.
|
|
713
|
-
const
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
714
|
+
const cleanupOAuth = stageStoreDir(
|
|
715
|
+
"host-probe-token-clash-oauth-",
|
|
716
|
+
setOAuthDir,
|
|
717
|
+
getOAuthDir,
|
|
718
|
+
);
|
|
719
|
+
const cleanupStore = stageStoreDir(
|
|
720
|
+
"host-probe-token-clash-secrets-",
|
|
721
|
+
setSecretsDir,
|
|
722
|
+
getSecretsDir,
|
|
723
|
+
);
|
|
718
724
|
writeSecret("example.com", "api_key", "K");
|
|
719
725
|
writeToken(
|
|
720
726
|
"example.com",
|
|
@@ -740,9 +746,8 @@ describe("probe redirect handling (live localhost)", () => {
|
|
|
740
746
|
),
|
|
741
747
|
).rejects.toThrow(/collides with the static 'Authorization' header/);
|
|
742
748
|
} finally {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
749
|
+
cleanupOAuth();
|
|
750
|
+
cleanupStore();
|
|
746
751
|
}
|
|
747
752
|
});
|
|
748
753
|
});
|
|
@@ -752,16 +757,21 @@ describe("probe redirect handling (live localhost)", () => {
|
|
|
752
757
|
// injects the fresh Bearer. Failures ride the note — never fail-closed.
|
|
753
758
|
describe("probe mint-on-demand (client-credentials bootstrap)", () => {
|
|
754
759
|
it("absent token + mint fields → POSTs tokenUrl, stamps the store, injects the Bearer", async () => {
|
|
755
|
-
const
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
+
const cleanupOAuth = stageStoreDir(
|
|
761
|
+
"host-probe-mint-ok-oauth-",
|
|
762
|
+
setOAuthDir,
|
|
763
|
+
getOAuthDir,
|
|
764
|
+
);
|
|
765
|
+
const cleanupStore = stageStoreDir(
|
|
766
|
+
"host-probe-mint-ok-secrets-",
|
|
767
|
+
setSecretsDir,
|
|
768
|
+
getSecretsDir,
|
|
769
|
+
);
|
|
760
770
|
writeSecret("example.com", "client_id", "cid_value");
|
|
761
771
|
writeSecret("example.com", "client_secret", "cs_value");
|
|
762
772
|
let tokenPosts = 0;
|
|
763
773
|
let seenAuth: string | undefined;
|
|
764
|
-
const server =
|
|
774
|
+
const { server, base } = await startProbeServer((req, res) => {
|
|
765
775
|
if (req.method === "POST" && req.url === "/token") {
|
|
766
776
|
tokenPosts++;
|
|
767
777
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -778,8 +788,6 @@ describe("probe redirect handling (live localhost)", () => {
|
|
|
778
788
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
779
789
|
res.end(JSON.stringify({ data: [{ id: 1 }] }));
|
|
780
790
|
});
|
|
781
|
-
await new Promise<void>((r) => server.listen(0, r));
|
|
782
|
-
const base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
|
|
783
791
|
try {
|
|
784
792
|
const result = await probe(
|
|
785
793
|
base,
|
|
@@ -805,26 +813,24 @@ describe("probe redirect handling (live localhost)", () => {
|
|
|
805
813
|
// Output-channel audit: the minted token never surfaces.
|
|
806
814
|
expect(result.raw).not.toContain("minted_tok");
|
|
807
815
|
} finally {
|
|
808
|
-
server
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
setSecretsDir(prevSecrets);
|
|
812
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
816
|
+
closeProbeServer(server);
|
|
817
|
+
cleanupOAuth();
|
|
818
|
+
cleanupStore();
|
|
813
819
|
}
|
|
814
820
|
});
|
|
815
821
|
|
|
816
822
|
it("mint fields + non-cc grant → loud note, no mint, no slot stamp", async () => {
|
|
817
|
-
const
|
|
818
|
-
|
|
819
|
-
|
|
823
|
+
const cleanupStore = stageStoreDir(
|
|
824
|
+
"host-probe-mint-grant-",
|
|
825
|
+
setOAuthDir,
|
|
826
|
+
getOAuthDir,
|
|
827
|
+
);
|
|
820
828
|
let tokenPosts = 0;
|
|
821
|
-
const server =
|
|
829
|
+
const { server, base } = await startProbeServer((req, res) => {
|
|
822
830
|
if (req.method === "POST") tokenPosts++;
|
|
823
831
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
824
832
|
res.end(JSON.stringify({ data: [{ id: 1 }] }));
|
|
825
833
|
});
|
|
826
|
-
await new Promise<void>((r) => server.listen(0, r));
|
|
827
|
-
const base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
|
|
828
834
|
try {
|
|
829
835
|
const result = await probe(
|
|
830
836
|
base,
|
|
@@ -848,25 +854,23 @@ describe("probe redirect handling (live localhost)", () => {
|
|
|
848
854
|
readToken("example.com", "client_credentials", `${base}/token`) ?? null,
|
|
849
855
|
).toBeNull();
|
|
850
856
|
} finally {
|
|
851
|
-
server
|
|
852
|
-
|
|
853
|
-
setOAuthDir(prevDir);
|
|
854
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
857
|
+
closeProbeServer(server);
|
|
858
|
+
cleanupStore();
|
|
855
859
|
}
|
|
856
860
|
});
|
|
857
861
|
|
|
858
862
|
it("bad mint field combo (cc without client-secret) degrades to a note, not a throw", async () => {
|
|
859
|
-
const
|
|
860
|
-
|
|
861
|
-
|
|
863
|
+
const cleanupStore = stageStoreDir(
|
|
864
|
+
"host-probe-mint-invalid-",
|
|
865
|
+
setOAuthDir,
|
|
866
|
+
getOAuthDir,
|
|
867
|
+
);
|
|
862
868
|
let tokenPosts = 0;
|
|
863
|
-
const server =
|
|
869
|
+
const { server, base } = await startProbeServer((req, res) => {
|
|
864
870
|
if (req.method === "POST") tokenPosts++;
|
|
865
871
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
866
872
|
res.end(JSON.stringify({ data: [{ id: 1 }] }));
|
|
867
873
|
});
|
|
868
|
-
await new Promise<void>((r) => server.listen(0, r));
|
|
869
|
-
const base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
|
|
870
874
|
try {
|
|
871
875
|
// client_credentials without clientSecret: buildSyntheticOAuth2Auth
|
|
872
876
|
// throws — but the probe's mint arm must ride the note instead.
|
|
@@ -883,20 +887,20 @@ describe("probe redirect handling (live localhost)", () => {
|
|
|
883
887
|
expect(result.ok).toBe(true);
|
|
884
888
|
expect(result.note ?? "").toContain("client-secret");
|
|
885
889
|
} finally {
|
|
886
|
-
server
|
|
887
|
-
|
|
888
|
-
setOAuthDir(prevDir);
|
|
889
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
890
|
+
closeProbeServer(server);
|
|
891
|
+
cleanupStore();
|
|
890
892
|
}
|
|
891
893
|
});
|
|
892
894
|
|
|
893
895
|
it("a fresh cached token short-circuits the mint (no POST)", async () => {
|
|
894
|
-
const
|
|
895
|
-
|
|
896
|
-
|
|
896
|
+
const cleanupStore = stageStoreDir(
|
|
897
|
+
"host-probe-mint-cache-",
|
|
898
|
+
setOAuthDir,
|
|
899
|
+
getOAuthDir,
|
|
900
|
+
);
|
|
897
901
|
let tokenPosts = 0;
|
|
898
902
|
let seenAuth: string | undefined;
|
|
899
|
-
const server =
|
|
903
|
+
const { server, base } = await startProbeServer((req, res) => {
|
|
900
904
|
if (req.method === "POST") {
|
|
901
905
|
tokenPosts++;
|
|
902
906
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -907,8 +911,6 @@ describe("probe redirect handling (live localhost)", () => {
|
|
|
907
911
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
908
912
|
res.end(JSON.stringify({ data: [{ id: 1 }] }));
|
|
909
913
|
});
|
|
910
|
-
await new Promise<void>((r) => server.listen(0, r));
|
|
911
|
-
const base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
|
|
912
914
|
try {
|
|
913
915
|
// Same slot the mint stamps: (client_credentials, ${base}/token).
|
|
914
916
|
writeToken("example.com", "client_credentials", `${base}/token`, {
|
|
@@ -932,21 +934,24 @@ describe("probe redirect handling (live localhost)", () => {
|
|
|
932
934
|
expect(seenAuth).toBe("Bearer cached_tok");
|
|
933
935
|
expect(result.ok).toBe(true);
|
|
934
936
|
} finally {
|
|
935
|
-
server
|
|
936
|
-
|
|
937
|
-
setOAuthDir(prevDir);
|
|
938
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
937
|
+
closeProbeServer(server);
|
|
938
|
+
cleanupStore();
|
|
939
939
|
}
|
|
940
940
|
});
|
|
941
941
|
|
|
942
942
|
it("mint failure rides the note; the probe proceeds unauthenticated", async () => {
|
|
943
|
-
const
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
943
|
+
const cleanupOAuth = stageStoreDir(
|
|
944
|
+
"host-probe-mint-miss-oauth-",
|
|
945
|
+
setOAuthDir,
|
|
946
|
+
getOAuthDir,
|
|
947
|
+
);
|
|
948
|
+
const cleanupStore = stageStoreDir(
|
|
949
|
+
"host-probe-mint-miss-secrets-",
|
|
950
|
+
setSecretsDir,
|
|
951
|
+
getSecretsDir,
|
|
952
|
+
);
|
|
948
953
|
let sawAuthHeader = false;
|
|
949
|
-
const server =
|
|
954
|
+
const { server, base } = await startProbeServer((req, res) => {
|
|
950
955
|
if (req.method === "POST") {
|
|
951
956
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
952
957
|
res.end(JSON.stringify({ access_token: "x" }));
|
|
@@ -956,8 +961,6 @@ describe("probe redirect handling (live localhost)", () => {
|
|
|
956
961
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
957
962
|
res.end(JSON.stringify({ data: [{ id: 1 }] }));
|
|
958
963
|
});
|
|
959
|
-
await new Promise<void>((r) => server.listen(0, r));
|
|
960
|
-
const base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
|
|
961
964
|
try {
|
|
962
965
|
const result = await probe(
|
|
963
966
|
base,
|
|
@@ -976,11 +979,9 @@ describe("probe redirect handling (live localhost)", () => {
|
|
|
976
979
|
expect(result.ok).toBe(true);
|
|
977
980
|
expect(result.note ?? "").toContain("client id 'client_id'");
|
|
978
981
|
} finally {
|
|
979
|
-
server
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
setSecretsDir(prevSecrets);
|
|
983
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
982
|
+
closeProbeServer(server);
|
|
983
|
+
cleanupOAuth();
|
|
984
|
+
cleanupStore();
|
|
984
985
|
}
|
|
985
986
|
});
|
|
986
987
|
});
|
|
@@ -989,36 +990,40 @@ describe("probe redirect handling (live localhost)", () => {
|
|
|
989
990
|
// registrable domain must be found when the probe hits an api subdomain.
|
|
990
991
|
describe("resolveProbeStoreDomain (secret-domain fallback)", () => {
|
|
991
992
|
it("falls back to the provisioned parent domain (pro-api → registrable) when the hostname isn't provisioned", () => {
|
|
992
|
-
const
|
|
993
|
-
|
|
994
|
-
|
|
993
|
+
const cleanupStore = stageStoreDir(
|
|
994
|
+
"host-probe-fallback-secrets-",
|
|
995
|
+
setSecretsDir,
|
|
996
|
+
getSecretsDir,
|
|
997
|
+
);
|
|
995
998
|
writeSecret("example.dev", "api_key", "K");
|
|
996
999
|
try {
|
|
997
1000
|
expect(resolveProbeStoreDomain("pro-api.example.dev")).toBe("example.dev");
|
|
998
1001
|
} finally {
|
|
999
|
-
|
|
1000
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
1002
|
+
cleanupStore();
|
|
1001
1003
|
}
|
|
1002
1004
|
});
|
|
1003
1005
|
|
|
1004
1006
|
it("exact-match hostname beats the parent fallback", () => {
|
|
1005
|
-
const
|
|
1006
|
-
|
|
1007
|
-
|
|
1007
|
+
const cleanupStore = stageStoreDir(
|
|
1008
|
+
"host-probe-exact-secrets-",
|
|
1009
|
+
setSecretsDir,
|
|
1010
|
+
getSecretsDir,
|
|
1011
|
+
);
|
|
1008
1012
|
writeSecret("api.example.com", "api_key", "EXACT");
|
|
1009
1013
|
writeSecret("example.com", "api_key", "PARENT");
|
|
1010
1014
|
try {
|
|
1011
1015
|
expect(resolveProbeStoreDomain("api.example.com")).toBe("api.example.com");
|
|
1012
1016
|
} finally {
|
|
1013
|
-
|
|
1014
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
1017
|
+
cleanupStore();
|
|
1015
1018
|
}
|
|
1016
1019
|
});
|
|
1017
1020
|
|
|
1018
1021
|
it("picks the longest matching parent when several are provisioned", () => {
|
|
1019
|
-
const
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
+
const cleanupStore = stageStoreDir(
|
|
1023
|
+
"host-probe-longest-secrets-",
|
|
1024
|
+
setSecretsDir,
|
|
1025
|
+
getSecretsDir,
|
|
1026
|
+
);
|
|
1022
1027
|
writeSecret("example.com", "api_key", "A");
|
|
1023
1028
|
writeSecret("api.example.com", "api_key", "B");
|
|
1024
1029
|
try {
|
|
@@ -1026,37 +1031,38 @@ describe("probe redirect handling (live localhost)", () => {
|
|
|
1026
1031
|
"api.example.com",
|
|
1027
1032
|
);
|
|
1028
1033
|
} finally {
|
|
1029
|
-
|
|
1030
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
1034
|
+
cleanupStore();
|
|
1031
1035
|
}
|
|
1032
1036
|
});
|
|
1033
1037
|
|
|
1034
1038
|
it("no provisioned parent → returns the hostname as-is", () => {
|
|
1035
|
-
const
|
|
1036
|
-
|
|
1037
|
-
|
|
1039
|
+
const cleanupStore = stageStoreDir(
|
|
1040
|
+
"host-probe-noparent-secrets-",
|
|
1041
|
+
setSecretsDir,
|
|
1042
|
+
getSecretsDir,
|
|
1043
|
+
);
|
|
1038
1044
|
try {
|
|
1039
1045
|
expect(resolveProbeStoreDomain("api.unknown.test")).toBe(
|
|
1040
1046
|
"api.unknown.test",
|
|
1041
1047
|
);
|
|
1042
1048
|
} finally {
|
|
1043
|
-
|
|
1044
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
1049
|
+
cleanupStore();
|
|
1045
1050
|
}
|
|
1046
1051
|
});
|
|
1047
1052
|
|
|
1048
1053
|
it("leading-dot guard: a sibling hostname never matches (malicious-example.com ≠ example.com)", () => {
|
|
1049
|
-
const
|
|
1050
|
-
|
|
1051
|
-
|
|
1054
|
+
const cleanupStore = stageStoreDir(
|
|
1055
|
+
"host-probe-dotguard-secrets-",
|
|
1056
|
+
setSecretsDir,
|
|
1057
|
+
getSecretsDir,
|
|
1058
|
+
);
|
|
1052
1059
|
writeSecret("example.com", "api_key", "K");
|
|
1053
1060
|
try {
|
|
1054
1061
|
expect(resolveProbeStoreDomain("malicious-example.com")).toBe(
|
|
1055
1062
|
"malicious-example.com",
|
|
1056
1063
|
);
|
|
1057
1064
|
} finally {
|
|
1058
|
-
|
|
1059
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
1065
|
+
cleanupStore();
|
|
1060
1066
|
}
|
|
1061
1067
|
});
|
|
1062
1068
|
});
|
|
@@ -1098,19 +1104,19 @@ describe("probe redirect handling (live localhost)", () => {
|
|
|
1098
1104
|
|
|
1099
1105
|
describe("probe inline auth.headerPrefixes", () => {
|
|
1100
1106
|
it("prepends the prefix on the wire and scrubs the raw token from the body", async () => {
|
|
1101
|
-
const
|
|
1102
|
-
|
|
1103
|
-
|
|
1107
|
+
const cleanupStore = stageStoreDir(
|
|
1108
|
+
"host-probe-prefix-secrets-",
|
|
1109
|
+
setSecretsDir,
|
|
1110
|
+
getSecretsDir,
|
|
1111
|
+
);
|
|
1104
1112
|
writeSecret("api.example.com", "api_key", "RAW-TOKEN");
|
|
1105
1113
|
let sawAuthHeader = "";
|
|
1106
|
-
const server =
|
|
1114
|
+
const { server, base } = await startProbeServer((req, res) => {
|
|
1107
1115
|
sawAuthHeader = (req.headers["x-api-key"] ?? "") as string;
|
|
1108
1116
|
// Echo the BARE token in the body — the probe scrub must redact it.
|
|
1109
1117
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1110
1118
|
res.end(JSON.stringify({ data: [{ id: 1, echoed: "RAW-TOKEN" }] }));
|
|
1111
1119
|
});
|
|
1112
|
-
await new Promise<void>((r) => server.listen(0, r));
|
|
1113
|
-
const base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
|
|
1114
1120
|
try {
|
|
1115
1121
|
const result = await probe(
|
|
1116
1122
|
base,
|
|
@@ -1127,10 +1133,8 @@ describe("probe redirect handling (live localhost)", () => {
|
|
|
1127
1133
|
expect(sawAuthHeader).toBe("Bearer RAW-TOKEN");
|
|
1128
1134
|
expect(result.raw).not.toContain("RAW-TOKEN");
|
|
1129
1135
|
} finally {
|
|
1130
|
-
server
|
|
1131
|
-
|
|
1132
|
-
setSecretsDir(prevDir);
|
|
1133
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
1136
|
+
closeProbeServer(server);
|
|
1137
|
+
cleanupStore();
|
|
1134
1138
|
}
|
|
1135
1139
|
});
|
|
1136
1140
|
|
|
@@ -1160,12 +1164,10 @@ async function stubProbeServer(
|
|
|
1160
1164
|
status: number,
|
|
1161
1165
|
body: unknown = { data: [{ id: 1 }] },
|
|
1162
1166
|
): Promise<{ server: http.Server; base: string }> {
|
|
1163
|
-
const server =
|
|
1167
|
+
const { server, base } = await startProbeServer((_req, res) => {
|
|
1164
1168
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
1165
1169
|
res.end(JSON.stringify(body));
|
|
1166
1170
|
});
|
|
1167
|
-
await new Promise<void>((r) => server.listen(0, r));
|
|
1168
|
-
const base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
|
|
1169
1171
|
return { server, base };
|
|
1170
1172
|
}
|
|
1171
1173
|
|
|
@@ -1179,15 +1181,16 @@ describe("api-probe 401/403 wording", () => {
|
|
|
1179
1181
|
);
|
|
1180
1182
|
expect(result.note ?? "").not.toContain("guide is auth:none");
|
|
1181
1183
|
} finally {
|
|
1182
|
-
server
|
|
1183
|
-
server.closeAllConnections?.();
|
|
1184
|
+
closeProbeServer(server);
|
|
1184
1185
|
}
|
|
1185
1186
|
});
|
|
1186
1187
|
|
|
1187
1188
|
it("auth injected (secret provisioned) but server 401 → injected-but-rejected wording", async () => {
|
|
1188
|
-
const
|
|
1189
|
-
|
|
1190
|
-
|
|
1189
|
+
const cleanupStore = stageStoreDir(
|
|
1190
|
+
"host-probe-401-secrets-",
|
|
1191
|
+
setSecretsDir,
|
|
1192
|
+
getSecretsDir,
|
|
1193
|
+
);
|
|
1191
1194
|
writeSecret("api.example.com", "api_key", "K");
|
|
1192
1195
|
const { server, base } = await stubProbeServer(401);
|
|
1193
1196
|
try {
|
|
@@ -1204,17 +1207,17 @@ describe("api-probe 401/403 wording", () => {
|
|
|
1204
1207
|
expect(note).toContain("auth injected but rejected; verify header name");
|
|
1205
1208
|
expect(note).not.toContain("not found in store");
|
|
1206
1209
|
} finally {
|
|
1207
|
-
server
|
|
1208
|
-
|
|
1209
|
-
setSecretsDir(prevDir);
|
|
1210
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
1210
|
+
closeProbeServer(server);
|
|
1211
|
+
cleanupStore();
|
|
1211
1212
|
}
|
|
1212
1213
|
});
|
|
1213
1214
|
|
|
1214
1215
|
it("auth injected, secret missing from store → auth rejected; names the secret", async () => {
|
|
1215
|
-
const
|
|
1216
|
-
|
|
1217
|
-
|
|
1216
|
+
const cleanupStore = stageStoreDir(
|
|
1217
|
+
"host-probe-401-miss-",
|
|
1218
|
+
setSecretsDir,
|
|
1219
|
+
getSecretsDir,
|
|
1220
|
+
);
|
|
1218
1221
|
const { server, base } = await stubProbeServer(401);
|
|
1219
1222
|
try {
|
|
1220
1223
|
const result = await probe(
|
|
@@ -1232,17 +1235,17 @@ describe("api-probe 401/403 wording", () => {
|
|
|
1232
1235
|
'secret "api_key" not found in store for domain "api.example.com"',
|
|
1233
1236
|
);
|
|
1234
1237
|
} finally {
|
|
1235
|
-
server
|
|
1236
|
-
|
|
1237
|
-
setSecretsDir(prevDir);
|
|
1238
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
1238
|
+
closeProbeServer(server);
|
|
1239
|
+
cleanupStore();
|
|
1239
1240
|
}
|
|
1240
1241
|
});
|
|
1241
1242
|
|
|
1242
1243
|
it("auth injected, nothing missing, server 403 with no message → bare status, not verify-header", async () => {
|
|
1243
|
-
const
|
|
1244
|
-
|
|
1245
|
-
|
|
1244
|
+
const cleanupStore = stageStoreDir(
|
|
1245
|
+
"host-probe-403-secrets-",
|
|
1246
|
+
setSecretsDir,
|
|
1247
|
+
getSecretsDir,
|
|
1248
|
+
);
|
|
1246
1249
|
writeSecret("api.example.com", "api_key", "K");
|
|
1247
1250
|
const { server, base } = await stubProbeServer(403);
|
|
1248
1251
|
try {
|
|
@@ -1260,17 +1263,17 @@ describe("api-probe 401/403 wording", () => {
|
|
|
1260
1263
|
expect(result.note ?? "").toBe("403");
|
|
1261
1264
|
expect(result.note ?? "").not.toContain("verify header name");
|
|
1262
1265
|
} finally {
|
|
1263
|
-
server
|
|
1264
|
-
|
|
1265
|
-
setSecretsDir(prevDir);
|
|
1266
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
1266
|
+
closeProbeServer(server);
|
|
1267
|
+
cleanupStore();
|
|
1267
1268
|
}
|
|
1268
1269
|
});
|
|
1269
1270
|
|
|
1270
1271
|
it("auth injected, 403 → server's own message surfaced, not verify-header", async () => {
|
|
1271
|
-
const
|
|
1272
|
-
|
|
1273
|
-
|
|
1272
|
+
const cleanupStore = stageStoreDir(
|
|
1273
|
+
"host-probe-403-msg-",
|
|
1274
|
+
setSecretsDir,
|
|
1275
|
+
getSecretsDir,
|
|
1276
|
+
);
|
|
1274
1277
|
writeSecret("api.example.com", "api_key", "sk-abc123");
|
|
1275
1278
|
const { server, base } = await stubProbeServer(403, {
|
|
1276
1279
|
status: {
|
|
@@ -1299,17 +1302,17 @@ describe("api-probe 401/403 wording", () => {
|
|
|
1299
1302
|
expect(note).toContain("plan/subscription limitation");
|
|
1300
1303
|
expect(note).not.toContain("verify header name");
|
|
1301
1304
|
} finally {
|
|
1302
|
-
server
|
|
1303
|
-
|
|
1304
|
-
setSecretsDir(prevDir);
|
|
1305
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
1305
|
+
closeProbeServer(server);
|
|
1306
|
+
cleanupStore();
|
|
1306
1307
|
}
|
|
1307
1308
|
});
|
|
1308
1309
|
|
|
1309
1310
|
it("auth injected, 403 with message field → server's words surfaced", async () => {
|
|
1310
|
-
const
|
|
1311
|
-
|
|
1312
|
-
|
|
1311
|
+
const cleanupStore = stageStoreDir(
|
|
1312
|
+
"host-probe-403-plantext-",
|
|
1313
|
+
setSecretsDir,
|
|
1314
|
+
getSecretsDir,
|
|
1315
|
+
);
|
|
1313
1316
|
writeSecret("api.example.com", "api_key", "sk-abc123");
|
|
1314
1317
|
const { server, base } = await stubProbeServer(403, {
|
|
1315
1318
|
error: "Your plan does not include access to this endpoint",
|
|
@@ -1328,17 +1331,17 @@ describe("api-probe 401/403 wording", () => {
|
|
|
1328
1331
|
expect(note).toContain("Your plan does not include access to this endpoint");
|
|
1329
1332
|
expect(note).not.toContain("verify header name");
|
|
1330
1333
|
} finally {
|
|
1331
|
-
server
|
|
1332
|
-
|
|
1333
|
-
setSecretsDir(prevDir);
|
|
1334
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
1334
|
+
closeProbeServer(server);
|
|
1335
|
+
cleanupStore();
|
|
1335
1336
|
}
|
|
1336
1337
|
});
|
|
1337
1338
|
|
|
1338
1339
|
it("auth injected, 403 with detail field (Django/FastAPI) → server's words surfaced", async () => {
|
|
1339
|
-
const
|
|
1340
|
-
|
|
1341
|
-
|
|
1340
|
+
const cleanupStore = stageStoreDir(
|
|
1341
|
+
"host-probe-403-detail-",
|
|
1342
|
+
setSecretsDir,
|
|
1343
|
+
getSecretsDir,
|
|
1344
|
+
);
|
|
1342
1345
|
writeSecret("api.example.com", "api_key", "sk-abc123");
|
|
1343
1346
|
const { server, base } = await stubProbeServer(403, {
|
|
1344
1347
|
detail: "You do not have permission to perform this action.",
|
|
@@ -1357,17 +1360,17 @@ describe("api-probe 401/403 wording", () => {
|
|
|
1357
1360
|
expect(note).toContain("You do not have permission to perform this action.");
|
|
1358
1361
|
expect(note).not.toContain("verify header name");
|
|
1359
1362
|
} finally {
|
|
1360
|
-
server
|
|
1361
|
-
|
|
1362
|
-
setSecretsDir(prevDir);
|
|
1363
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
1363
|
+
closeProbeServer(server);
|
|
1364
|
+
cleanupStore();
|
|
1364
1365
|
}
|
|
1365
1366
|
});
|
|
1366
1367
|
|
|
1367
1368
|
it("auth injected, 403 echoing the secret → scrubbed to *** in the note", async () => {
|
|
1368
|
-
const
|
|
1369
|
-
|
|
1370
|
-
|
|
1369
|
+
const cleanupStore = stageStoreDir(
|
|
1370
|
+
"host-probe-403-scrub-",
|
|
1371
|
+
setSecretsDir,
|
|
1372
|
+
getSecretsDir,
|
|
1373
|
+
);
|
|
1371
1374
|
writeSecret("api.example.com", "api_key", "sk-abc123");
|
|
1372
1375
|
const { server, base } = await stubProbeServer(403, {
|
|
1373
1376
|
error: "Invalid key: sk-abc123",
|
|
@@ -1386,17 +1389,17 @@ describe("api-probe 401/403 wording", () => {
|
|
|
1386
1389
|
expect(note).toContain("Invalid key: ***");
|
|
1387
1390
|
expect(note).not.toContain("sk-abc123");
|
|
1388
1391
|
} finally {
|
|
1389
|
-
server
|
|
1390
|
-
|
|
1391
|
-
setSecretsDir(prevDir);
|
|
1392
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
1392
|
+
closeProbeServer(server);
|
|
1393
|
+
cleanupStore();
|
|
1393
1394
|
}
|
|
1394
1395
|
});
|
|
1395
1396
|
|
|
1396
1397
|
it("auth injected, 401 → stays verify-header (message surfacing is 403-only)", async () => {
|
|
1397
|
-
const
|
|
1398
|
-
|
|
1399
|
-
|
|
1398
|
+
const cleanupStore = stageStoreDir(
|
|
1399
|
+
"host-probe-401-plan-",
|
|
1400
|
+
setSecretsDir,
|
|
1401
|
+
getSecretsDir,
|
|
1402
|
+
);
|
|
1400
1403
|
writeSecret("api.example.com", "api_key", "K");
|
|
1401
1404
|
const { server, base } = await stubProbeServer(401, {
|
|
1402
1405
|
error: "Your plan does not include access to this endpoint",
|
|
@@ -1415,10 +1418,8 @@ describe("api-probe 401/403 wording", () => {
|
|
|
1415
1418
|
expect(note).toContain("auth injected but rejected; verify header name");
|
|
1416
1419
|
expect(note).not.toContain("key accepted");
|
|
1417
1420
|
} finally {
|
|
1418
|
-
server
|
|
1419
|
-
|
|
1420
|
-
setSecretsDir(prevDir);
|
|
1421
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
1421
|
+
closeProbeServer(server);
|
|
1422
|
+
cleanupStore();
|
|
1422
1423
|
}
|
|
1423
1424
|
});
|
|
1424
1425
|
});
|
|
@@ -1437,8 +1438,7 @@ describe("api-probe headerPrefixes without secretRefs", () => {
|
|
|
1437
1438
|
);
|
|
1438
1439
|
expect(result.note ?? "").toContain(ROOT_CAUSE);
|
|
1439
1440
|
} finally {
|
|
1440
|
-
server
|
|
1441
|
-
server.closeAllConnections?.();
|
|
1441
|
+
closeProbeServer(server);
|
|
1442
1442
|
}
|
|
1443
1443
|
});
|
|
1444
1444
|
|
|
@@ -1455,8 +1455,7 @@ describe("api-probe headerPrefixes without secretRefs", () => {
|
|
|
1455
1455
|
expect(note).toContain("endpoint requires auth; configure auth injection");
|
|
1456
1456
|
expect(note).toContain(ROOT_CAUSE);
|
|
1457
1457
|
} finally {
|
|
1458
|
-
server
|
|
1459
|
-
server.closeAllConnections?.();
|
|
1458
|
+
closeProbeServer(server);
|
|
1460
1459
|
}
|
|
1461
1460
|
});
|
|
1462
1461
|
|
|
@@ -1471,8 +1470,7 @@ describe("api-probe headerPrefixes without secretRefs", () => {
|
|
|
1471
1470
|
);
|
|
1472
1471
|
expect(result.note ?? "").toContain(ROOT_CAUSE);
|
|
1473
1472
|
} finally {
|
|
1474
|
-
server
|
|
1475
|
-
server.closeAllConnections?.();
|
|
1473
|
+
closeProbeServer(server);
|
|
1476
1474
|
}
|
|
1477
1475
|
});
|
|
1478
1476
|
|
|
@@ -1492,15 +1490,16 @@ describe("api-probe headerPrefixes without secretRefs", () => {
|
|
|
1492
1490
|
);
|
|
1493
1491
|
expect(result.note ?? "").toContain(ROOT_CAUSE);
|
|
1494
1492
|
} finally {
|
|
1495
|
-
server
|
|
1496
|
-
server.closeAllConnections?.();
|
|
1493
|
+
closeProbeServer(server);
|
|
1497
1494
|
}
|
|
1498
1495
|
});
|
|
1499
1496
|
|
|
1500
1497
|
it("correct shape (secretRefs + headerPrefixes) with secret provisioned → no warning", async () => {
|
|
1501
|
-
const
|
|
1502
|
-
|
|
1503
|
-
|
|
1498
|
+
const cleanupStore = stageStoreDir(
|
|
1499
|
+
"host-probe-prefix-ok-",
|
|
1500
|
+
setSecretsDir,
|
|
1501
|
+
getSecretsDir,
|
|
1502
|
+
);
|
|
1504
1503
|
writeSecret("api.example.com", "api_key", "K");
|
|
1505
1504
|
const { server, base } = await stubProbeServer(200);
|
|
1506
1505
|
try {
|
|
@@ -1518,10 +1517,8 @@ describe("api-probe headerPrefixes without secretRefs", () => {
|
|
|
1518
1517
|
);
|
|
1519
1518
|
expect(result.note ?? "").not.toContain("headerPrefixes ignored");
|
|
1520
1519
|
} finally {
|
|
1521
|
-
server
|
|
1522
|
-
|
|
1523
|
-
setSecretsDir(prevDir);
|
|
1524
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
1520
|
+
closeProbeServer(server);
|
|
1521
|
+
cleanupStore();
|
|
1525
1522
|
}
|
|
1526
1523
|
});
|
|
1527
1524
|
});
|