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.
Files changed (104) hide show
  1. package/README.md +5 -3
  2. package/node_modules/pi-lean-host/AGENTS.md +13 -6
  3. package/node_modules/pi-lean-host/__tests__/api-learn-fetch-recipe.test.ts +282 -10
  4. package/node_modules/pi-lean-host/__tests__/api-learn-multi-file.test.ts +8 -16
  5. package/node_modules/pi-lean-host/__tests__/api-probe.test.ts +276 -279
  6. package/node_modules/pi-lean-host/__tests__/api-scaffold.test.ts +12 -33
  7. package/node_modules/pi-lean-host/__tests__/api-toggle.test.ts +14 -12
  8. package/node_modules/pi-lean-host/__tests__/bootstrap-command.test.ts +1 -7
  9. package/node_modules/pi-lean-host/__tests__/delete-command.test.ts +1 -12
  10. package/node_modules/pi-lean-host/__tests__/guide-catalog.test.ts +484 -0
  11. package/node_modules/pi-lean-host/__tests__/helpers.test.ts +0 -132
  12. package/node_modules/pi-lean-host/__tests__/oauth-command.test.ts +95 -0
  13. package/node_modules/pi-lean-host/__tests__/oauth-flow.test.ts +163 -0
  14. package/node_modules/pi-lean-host/__tests__/oauth-mint.test.ts +79 -6
  15. package/node_modules/pi-lean-host/__tests__/parse-api-guide.test.ts +4 -409
  16. package/node_modules/pi-lean-host/__tests__/response-spill.test.ts +0 -1
  17. package/node_modules/pi-lean-host/__tests__/secrets-command.test.ts +19 -35
  18. package/node_modules/pi-lean-host/__tests__/smoke.test.ts +1 -82
  19. package/node_modules/pi-lean-host/__tests__/ssrf-guard.test.ts +98 -0
  20. package/node_modules/pi-lean-host/__tests__/test-utils.ts +73 -0
  21. package/node_modules/pi-lean-host/__tests__/tools.test.ts +2 -518
  22. package/node_modules/pi-lean-host/__tests__/transport.test.ts +178 -31
  23. package/node_modules/pi-lean-host/__tests__/verify-command.test.ts +1 -8
  24. package/node_modules/pi-lean-host/__tests__/verify-stamp.test.ts +1 -4
  25. package/node_modules/pi-lean-host/api-guides/boe/local-helper.test.ts +9 -20
  26. package/node_modules/pi-lean-host/api-guides/dnb/error-envelope.test.ts +6 -18
  27. package/node_modules/pi-lean-host/api-guides/dnb/resumption-token.test.ts +6 -18
  28. package/node_modules/pi-lean-host/api-guides/frost-sensorthings/dotted-key.test.ts +6 -18
  29. package/node_modules/pi-lean-host/api-guides/github/static-key.test.ts +7 -25
  30. package/node_modules/pi-lean-host/api-guides/inaturalist/derived-id.test.ts +6 -18
  31. package/node_modules/pi-lean-host/api-guides/internet-archive/multi-recipe.test.ts +6 -26
  32. package/node_modules/pi-lean-host/api-guides/stripe/has-more.test.ts +6 -18
  33. package/node_modules/pi-lean-host/api-guides/telegram-bot/path-auth.test.ts +7 -25
  34. package/node_modules/pi-lean-host/api-guides/twitch/oauth2.test.ts +12 -47
  35. package/node_modules/pi-lean-host/api-guides/twitch-user/oauth-user.test.ts +11 -52
  36. package/node_modules/pi-lean-host/api-guides/usgs/transform.test.ts +10 -27
  37. package/node_modules/pi-lean-host/api-guides/wikidata-search/numeric-cursor.test.ts +6 -18
  38. package/node_modules/pi-lean-host/api-guides/wikimedia-action/token-bag.test.ts +9 -20
  39. package/node_modules/pi-lean-host/core/auth.ts +2 -1
  40. package/node_modules/pi-lean-host/core/helpers.ts +29 -35
  41. package/node_modules/pi-lean-host/core/oauth-command.ts +10 -8
  42. package/node_modules/pi-lean-host/core/oauth-flow.ts +20 -4
  43. package/node_modules/pi-lean-host/core/parse-api-guide.ts +3 -5
  44. package/node_modules/pi-lean-host/core/transport.ts +1 -1
  45. package/node_modules/pi-lean-host/package.json +1 -1
  46. package/node_modules/pi-lean-host/tools/api-guide.ts +18 -57
  47. package/node_modules/pi-lean-host/tools/api-learn.ts +20 -49
  48. package/node_modules/pi-lean-host/tools/api-probe.ts +17 -26
  49. package/node_modules/pi-lean-host/tools/api-scaffold.ts +20 -42
  50. package/node_modules/pi-lean-host/tools/utils.ts +69 -0
  51. package/node_modules/pi-lean-portal/AGENTS.md +4 -3
  52. package/node_modules/pi-lean-portal/README.md +9 -9
  53. package/node_modules/pi-lean-portal/__tests__/accessibility-tree.test.ts +0 -15
  54. package/node_modules/pi-lean-portal/__tests__/browser-install.test.ts +737 -0
  55. package/node_modules/pi-lean-portal/__tests__/browser-navigate.test.ts +33 -2
  56. package/node_modules/pi-lean-portal/__tests__/browser-status.test.ts +48 -25
  57. package/node_modules/pi-lean-portal/__tests__/browser-toggle-profile.test.ts +3 -9
  58. package/node_modules/pi-lean-portal/__tests__/browser-toggle.test.ts +3 -29
  59. package/node_modules/pi-lean-portal/__tests__/fetch-backend.test.ts +51 -0
  60. package/node_modules/pi-lean-portal/__tests__/helpers/__pycache__/mock-python-bridge.cpython-313-pytest-9.1.1.pyc +0 -0
  61. package/node_modules/pi-lean-portal/__tests__/helpers/__pycache__/mock-python-bridge.cpython-313.pyc +0 -0
  62. package/node_modules/pi-lean-portal/__tests__/helpers/mock-pi.ts +34 -0
  63. package/node_modules/pi-lean-portal/__tests__/helpers/mock-python-bridge.py +14 -24
  64. package/node_modules/pi-lean-portal/__tests__/plugin-registry.test.ts +0 -90
  65. package/node_modules/pi-lean-portal/__tests__/python-adapter.test.ts +32 -125
  66. package/node_modules/pi-lean-portal/__tests__/router-dispatch.test.ts +12 -170
  67. package/node_modules/pi-lean-portal/__tests__/router-session.test.ts +39 -151
  68. package/node_modules/pi-lean-portal/__tests__/web-guides.test.ts +1 -44
  69. package/node_modules/pi-lean-portal/backends/chromium/index.ts +1 -1
  70. package/node_modules/pi-lean-portal/backends/firefox/index.ts +1 -1
  71. package/node_modules/pi-lean-portal/backends/playwright-base/playwright-plugin.ts +1 -1
  72. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-312.pyc +0 -0
  73. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-313.pyc +0 -0
  74. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-312.pyc +0 -0
  75. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-313.pyc +0 -0
  76. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/playwright_base.py +43 -57
  77. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/transport.py +6 -21
  78. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_accessibility.cpython-313-pytest-9.1.1.pyc +0 -0
  79. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313-pytest-9.1.1.pyc +0 -0
  80. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313.pyc +0 -0
  81. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_transport.cpython-313-pytest-9.1.1.pyc +0 -0
  82. package/node_modules/pi-lean-portal/backends/python-base/tests/test_playwright_base_quirks.py +157 -73
  83. package/node_modules/pi-lean-portal/backends/python-base/tests/test_transport.py +15 -41
  84. package/node_modules/pi-lean-portal/browser-install.ts +454 -0
  85. package/node_modules/pi-lean-portal/browser-status.ts +21 -0
  86. package/node_modules/pi-lean-portal/browser-toggle.ts +13 -11
  87. package/node_modules/pi-lean-portal/core/fetch-backend.ts +2 -2
  88. package/node_modules/pi-lean-portal/core/router.ts +7 -32
  89. package/node_modules/pi-lean-portal/core/shared/session-manager.ts +1 -3
  90. package/node_modules/pi-lean-portal/index.ts +1 -18
  91. package/node_modules/pi-lean-portal/package.json +2 -1
  92. package/node_modules/pi-lean-portal/tools/browser-navigate.ts +6 -2
  93. package/node_modules/pi-lean-portal/tools/utils.ts +5 -6
  94. package/node_modules/pi-lean-search/AGENTS.md +5 -3
  95. package/node_modules/pi-lean-search/__tests__/web-search.test.ts +180 -127
  96. package/node_modules/pi-lean-search/index.ts +0 -3
  97. package/node_modules/pi-lean-search/package.json +1 -1
  98. package/node_modules/pi-lean-search/web-search-tool.ts +22 -13
  99. package/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +19 -18
  100. package/node_modules/yaml/browser/dist/nodes/Alias.js +25 -23
  101. package/node_modules/yaml/dist/compose/resolve-flow-scalar.js +19 -18
  102. package/node_modules/yaml/dist/nodes/Alias.js +25 -23
  103. package/node_modules/yaml/package.json +1 -1
  104. 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 = http.createServer((req, res) => {
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.close();
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 = http.createServer((_req, res) => {
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.close();
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 = http.createServer((req, res) => {
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.close();
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 = http.createServer((req, res) => {
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.close();
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 = http.createServer((req, res) => {
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.close();
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 = http.createServer((req, res) => {
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.close();
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 = http.createServer((req, res) => {
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.close();
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 = http.createServer((req, res) => {
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.close();
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 = http.createServer((req, res) => {
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.close();
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 = http.createServer((req, res) => {
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.close();
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 tmp = mkdtempSync(join(tmpdir(), "host-probe-miss-secrets-"));
513
- const prevDir = getSecretsDir();
514
- setSecretsDir(tmp);
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 = http.createServer((_req, res) => {
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.close();
540
- server.closeAllConnections?.();
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 tmp = mkdtempSync(join(tmpdir(), "host-probe-token-ok-"));
552
- const prevDir = getOAuthDir();
553
- setOAuthDir(tmp);
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 = http.createServer((req, res) => {
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.close();
593
- server.closeAllConnections?.();
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 tmp = mkdtempSync(join(tmpdir(), "host-probe-token-miss-"));
601
- const prevDir = getOAuthDir();
602
- setOAuthDir(tmp);
604
+ const cleanupStore = stageStoreDir(
605
+ "host-probe-token-miss-",
606
+ setOAuthDir,
607
+ getOAuthDir,
608
+ );
603
609
  let sawAuthHeader = false;
604
- const server = http.createServer((req, res) => {
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.close();
633
- server.closeAllConnections?.();
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 tmp = mkdtempSync(join(tmpdir(), "host-probe-token-exp-"));
641
- const prevDir = getOAuthDir();
642
- setOAuthDir(tmp);
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 = http.createServer((req, res) => {
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.close();
681
- server.closeAllConnections?.();
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 tmp = mkdtempSync(join(tmpdir(), "host-probe-token-badkey-"));
689
- const prevDir = getOAuthDir();
690
- setOAuthDir(tmp);
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
- setOAuthDir(prevDir);
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 tmp = mkdtempSync(join(tmpdir(), "host-probe-token-clash-"));
714
- const prevDir = getOAuthDir();
715
- const prevSecrets = getSecretsDir();
716
- setOAuthDir(join(tmp, "oauth"));
717
- setSecretsDir(join(tmp, "secrets"));
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
- setOAuthDir(prevDir);
744
- setSecretsDir(prevSecrets);
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 tmp = mkdtempSync(join(tmpdir(), "host-probe-mint-ok-"));
756
- const prevDir = getOAuthDir();
757
- const prevSecrets = getSecretsDir();
758
- setOAuthDir(tmp);
759
- setSecretsDir(tmp);
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 = http.createServer((req, res) => {
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.close();
809
- server.closeAllConnections?.();
810
- setOAuthDir(prevDir);
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 tmp = mkdtempSync(join(tmpdir(), "host-probe-mint-grant-"));
818
- const prevDir = getOAuthDir();
819
- setOAuthDir(tmp);
823
+ const cleanupStore = stageStoreDir(
824
+ "host-probe-mint-grant-",
825
+ setOAuthDir,
826
+ getOAuthDir,
827
+ );
820
828
  let tokenPosts = 0;
821
- const server = http.createServer((req, res) => {
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.close();
852
- server.closeAllConnections?.();
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 tmp = mkdtempSync(join(tmpdir(), "host-probe-mint-invalid-"));
860
- const prevDir = getOAuthDir();
861
- setOAuthDir(tmp);
863
+ const cleanupStore = stageStoreDir(
864
+ "host-probe-mint-invalid-",
865
+ setOAuthDir,
866
+ getOAuthDir,
867
+ );
862
868
  let tokenPosts = 0;
863
- const server = http.createServer((req, res) => {
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.close();
887
- server.closeAllConnections?.();
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 tmp = mkdtempSync(join(tmpdir(), "host-probe-mint-cache-"));
895
- const prevDir = getOAuthDir();
896
- setOAuthDir(tmp);
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 = http.createServer((req, res) => {
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.close();
936
- server.closeAllConnections?.();
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 tmp = mkdtempSync(join(tmpdir(), "host-probe-mint-miss-"));
944
- const prevDir = getOAuthDir();
945
- const prevSecrets = getSecretsDir();
946
- setOAuthDir(tmp);
947
- setSecretsDir(tmp); // isolated empty store — clientId will miss
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 = http.createServer((req, res) => {
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.close();
980
- server.closeAllConnections?.();
981
- setOAuthDir(prevDir);
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 tmp = mkdtempSync(join(tmpdir(), "host-probe-fallback-secrets-"));
993
- const prevDir = getSecretsDir();
994
- setSecretsDir(tmp);
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
- setSecretsDir(prevDir);
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 tmp = mkdtempSync(join(tmpdir(), "host-probe-exact-secrets-"));
1006
- const prevDir = getSecretsDir();
1007
- setSecretsDir(tmp);
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
- setSecretsDir(prevDir);
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 tmp = mkdtempSync(join(tmpdir(), "host-probe-longest-secrets-"));
1020
- const prevDir = getSecretsDir();
1021
- setSecretsDir(tmp);
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
- setSecretsDir(prevDir);
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 tmp = mkdtempSync(join(tmpdir(), "host-probe-noparent-secrets-"));
1036
- const prevDir = getSecretsDir();
1037
- setSecretsDir(tmp);
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
- setSecretsDir(prevDir);
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 tmp = mkdtempSync(join(tmpdir(), "host-probe-dotguard-secrets-"));
1050
- const prevDir = getSecretsDir();
1051
- setSecretsDir(tmp);
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
- setSecretsDir(prevDir);
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 tmp = mkdtempSync(join(tmpdir(), "host-probe-prefix-secrets-"));
1102
- const prevDir = getSecretsDir();
1103
- setSecretsDir(tmp);
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 = http.createServer((req, res) => {
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.close();
1131
- server.closeAllConnections?.();
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 = http.createServer((_req, res) => {
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.close();
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 tmp = mkdtempSync(join(tmpdir(), "host-probe-401-secrets-"));
1189
- const prevDir = getSecretsDir();
1190
- setSecretsDir(tmp);
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.close();
1208
- server.closeAllConnections?.();
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 tmp = mkdtempSync(join(tmpdir(), "host-probe-401-miss-"));
1216
- const prevDir = getSecretsDir();
1217
- setSecretsDir(tmp);
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.close();
1236
- server.closeAllConnections?.();
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 tmp = mkdtempSync(join(tmpdir(), "host-probe-403-secrets-"));
1244
- const prevDir = getSecretsDir();
1245
- setSecretsDir(tmp);
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.close();
1264
- server.closeAllConnections?.();
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 tmp = mkdtempSync(join(tmpdir(), "host-probe-403-msg-"));
1272
- const prevDir = getSecretsDir();
1273
- setSecretsDir(tmp);
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.close();
1303
- server.closeAllConnections?.();
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 tmp = mkdtempSync(join(tmpdir(), "host-probe-403-plantext-"));
1311
- const prevDir = getSecretsDir();
1312
- setSecretsDir(tmp);
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.close();
1332
- server.closeAllConnections?.();
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 tmp = mkdtempSync(join(tmpdir(), "host-probe-403-detail-"));
1340
- const prevDir = getSecretsDir();
1341
- setSecretsDir(tmp);
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.close();
1361
- server.closeAllConnections?.();
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 tmp = mkdtempSync(join(tmpdir(), "host-probe-403-scrub-"));
1369
- const prevDir = getSecretsDir();
1370
- setSecretsDir(tmp);
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.close();
1390
- server.closeAllConnections?.();
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 tmp = mkdtempSync(join(tmpdir(), "host-probe-401-plan-"));
1398
- const prevDir = getSecretsDir();
1399
- setSecretsDir(tmp);
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.close();
1419
- server.closeAllConnections?.();
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.close();
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.close();
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.close();
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.close();
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 tmp = mkdtempSync(join(tmpdir(), "host-probe-prefix-ok-"));
1502
- const prevDir = getSecretsDir();
1503
- setSecretsDir(tmp);
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.close();
1522
- server.closeAllConnections?.();
1523
- setSecretsDir(prevDir);
1524
- rmSync(tmp, { recursive: true, force: true });
1520
+ closeProbeServer(server);
1521
+ cleanupStore();
1525
1522
  }
1526
1523
  });
1527
1524
  });