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
@@ -12,13 +12,7 @@
12
12
  */
13
13
 
14
14
  import { describe, it, expect, vi, beforeAll, afterAll } from "vitest";
15
- import {
16
- mkdtempSync,
17
- mkdirSync,
18
- writeFileSync,
19
- rmSync,
20
- readFileSync,
21
- } from "node:fs";
15
+ import { mkdtempSync, rmSync } from "node:fs";
22
16
  import { tmpdir } from "node:os";
23
17
  import { join } from "node:path";
24
18
 
@@ -30,11 +24,8 @@ vi.mock("../../core/transport.js", async () => ({
30
24
  fetchUrl: vi.fn(),
31
25
  }));
32
26
 
33
- import {
34
- findGuidesByDomain,
35
- setUserGuidesDir,
36
- invalidateCache,
37
- } from "../../core/guide-store.js";
27
+ import { findGuidesByDomain } from "../../core/guide-store.js";
28
+ import { stageGuides } from "../../__tests__/test-utils.js";
38
29
 
39
30
  // Folder keys (slug(shortName)) for the two guides claiming archive.org.
40
31
  const DOMAINS = ["internet-archive", "wayback-availability"] as const;
@@ -42,20 +33,9 @@ const DOMAINS = ["internet-archive", "wayback-availability"] as const;
42
33
  let tmpBase: string;
43
34
 
44
35
  async function setupRecipe(): Promise<void> {
45
- const guidesDir = mkdtempSync(join(tmpBase, "guides-"));
46
- for (const d of DOMAINS) {
47
- const dir = join(guidesDir, d);
48
- mkdirSync(dir, { recursive: true });
49
- // The test lives in api-guides/archive.org/, so the guide dirs are one
50
- // level up: ../<d>/guide.md.
51
- const source = readFileSync(
52
- new URL(`../${d}/guide.md`, import.meta.url),
53
- "utf-8",
54
- );
55
- writeFileSync(join(dir, "guide.md"), source, "utf-8");
56
- }
57
- setUserGuidesDir(guidesDir);
58
- invalidateCache();
36
+ // The test lives in api-guides/archive.org/; the guide dirs are siblings
37
+ // under api-guides/ — exactly what stageGuides copies from.
38
+ stageGuides(tmpBase, new URL("../", import.meta.url), DOMAINS);
59
39
  }
60
40
 
61
41
  /** Resolve an op across all guides claiming the domain — mirrors api-fetch. */
@@ -19,13 +19,7 @@ import {
19
19
  afterAll,
20
20
  beforeEach,
21
21
  } from "vitest";
22
- import {
23
- mkdtempSync,
24
- mkdirSync,
25
- writeFileSync,
26
- rmSync,
27
- readFileSync,
28
- } from "node:fs";
22
+ import { mkdtempSync, rmSync } from "node:fs";
29
23
  import { tmpdir } from "node:os";
30
24
  import { join } from "node:path";
31
25
  import type { ApiGuide } from "../../core/api-guide-types.js";
@@ -40,8 +34,7 @@ vi.mock("../../core/transport.js", async () => ({
40
34
 
41
35
  import { fetchUrl } from "../../core/transport.js";
42
36
  import { paginate } from "../../core/helpers.js";
43
- import { loadApiGuidesFromDir } from "../../core/guide-catalog.js";
44
- import { setUserGuidesDir, invalidateCache } from "../../core/guide-store.js";
37
+ import { stageGuides } from "../../__tests__/test-utils.js";
45
38
 
46
39
  // Drain any under-consumed Once-queue so a leaky test can't shift its
47
40
  // leftover pages into the next test (mockReset also clears calls).
@@ -80,15 +73,10 @@ const PAGE3_EMPTY = page([], false);
80
73
  let tmpBase: string;
81
74
 
82
75
  async function setupRecipe(): Promise<{ guide: ApiGuide }> {
83
- const guidesDir = mkdtempSync(join(tmpBase, "guides-"));
84
- const domainDir = join(guidesDir, "stripe");
85
- mkdirSync(domainDir, { recursive: true });
86
- const source = readFileSync(new URL("./guide.md", import.meta.url), "utf-8");
87
- writeFileSync(join(domainDir, "guide.md"), source, "utf-8");
88
- setUserGuidesDir(guidesDir);
89
- invalidateCache();
90
- const loaded = loadApiGuidesFromDir(guidesDir);
91
- return { guide: loaded.guides["stripe"]! };
76
+ const { guides } = stageGuides(tmpBase, new URL("../", import.meta.url), [
77
+ "stripe",
78
+ ]);
79
+ return { guide: guides["stripe"]! };
92
80
  }
93
81
 
94
82
  beforeAll(() => {
@@ -28,16 +28,10 @@ import {
28
28
  beforeEach,
29
29
  afterAll,
30
30
  } from "vitest";
31
- import {
32
- mkdtempSync,
33
- mkdirSync,
34
- writeFileSync,
35
- rmSync,
36
- readFileSync,
37
- } from "node:fs";
31
+ import { mkdtempSync, rmSync } from "node:fs";
38
32
  import { tmpdir } from "node:os";
39
33
  import { join } from "node:path";
40
- import type { ApiGuide, Operation } from "../../core/api-guide-types.js";
34
+ import type { ApiGuide } from "../../core/api-guide-types.js";
41
35
 
42
36
  // Mock the transport layer BEFORE any imports that use it.
43
37
  vi.mock("../../core/transport.js", async () => ({
@@ -50,8 +44,7 @@ vi.mock("../../core/transport.js", async () => ({
50
44
  import { fetchUrl } from "../../core/transport.js";
51
45
  const fetchUrlMock = vi.mocked(fetchUrl);
52
46
 
53
- import { loadApiGuidesFromDir } from "../../core/guide-catalog.js";
54
- import { setUserGuidesDir, invalidateCache } from "../../core/guide-store.js";
47
+ import { stageGuides, findOp } from "../../__tests__/test-utils.js";
55
48
  import { resolveOpForExecution } from "../../core/resolve-op.js";
56
49
  import { setSecretsDir, writeSecret } from "../../core/secrets-store.js";
57
50
 
@@ -65,21 +58,10 @@ let tmpBase: string;
65
58
 
66
59
  /** Stage the on-disk guide into a tmp guides dir and load it. */
67
60
  async function setupRecipe(): Promise<{ guide: ApiGuide }> {
68
- const guidesDir = mkdtempSync(join(tmpBase, "guides-"));
69
- const domainDir = join(guidesDir, "telegram-bot");
70
- mkdirSync(domainDir, { recursive: true });
71
- const source = readFileSync(new URL("./guide.md", import.meta.url), "utf-8");
72
- writeFileSync(join(domainDir, "guide.md"), source, "utf-8");
73
- setUserGuidesDir(guidesDir);
74
- invalidateCache();
75
- const loaded = loadApiGuidesFromDir(guidesDir);
76
- return { guide: loaded.guides["telegram-bot"]! };
77
- }
78
-
79
- function findOp(guide: ApiGuide, name: string): Operation {
80
- const op = guide.operations.find((o) => o.name === name);
81
- if (!op) throw new Error(`op ${name} not found`);
82
- return op;
61
+ const { guides } = stageGuides(tmpBase, new URL("../", import.meta.url), [
62
+ "telegram-bot",
63
+ ]);
64
+ return { guide: guides["telegram-bot"]! };
83
65
  }
84
66
 
85
67
  beforeAll(() => {
@@ -16,20 +16,10 @@
16
16
  */
17
17
 
18
18
  import { describe, it, expect, vi, beforeAll, afterAll } from "vitest";
19
- import {
20
- mkdtempSync,
21
- mkdirSync,
22
- writeFileSync,
23
- rmSync,
24
- readFileSync,
25
- } from "node:fs";
19
+ import { mkdtempSync, rmSync } from "node:fs";
26
20
  import { tmpdir } from "node:os";
27
21
  import { join } from "node:path";
28
- import type {
29
- ApiGuide,
30
- OAuth2Auth,
31
- Operation,
32
- } from "../../core/api-guide-types.js";
22
+ import type { ApiGuide, OAuth2Auth } from "../../core/api-guide-types.js";
33
23
 
34
24
  // Mock the transport layer BEFORE any imports that use it.
35
25
  vi.mock("../../core/transport.js", async () => ({
@@ -39,52 +29,27 @@ vi.mock("../../core/transport.js", async () => ({
39
29
  fetchUrl: vi.fn(),
40
30
  }));
41
31
 
42
- import { loadApiGuidesFromDir } from "../../core/guide-catalog.js";
43
- import { setUserGuidesDir, invalidateCache } from "../../core/guide-store.js";
44
32
  import { resolveOpForExecution } from "../../core/resolve-op.js";
45
33
  import { setSecretsDir, writeSecret } from "../../core/secrets-store.js";
46
34
  import { setOAuthDir, readToken, writeToken } from "../../core/oauth-store.js";
35
+ import {
36
+ stageGuides,
37
+ findOp,
38
+ stubTokenEndpoint,
39
+ tokenResponse,
40
+ } from "../../__tests__/test-utils.js";
47
41
 
48
42
  const TT = "https://id.twitch.tv/oauth2/token";
49
43
  const STORE_DOMAIN = "twitch.tv"; // canonicalStoreDomain = guide.domains[0]
50
44
 
51
- function stubTokenEndpoint(
52
- handler: (url: string, init: RequestInit) => Response,
53
- ): void {
54
- vi.stubGlobal(
55
- "fetch",
56
- vi.fn((url: unknown, init?: RequestInit) =>
57
- Promise.resolve(handler(String(url), init ?? {})),
58
- ),
59
- );
60
- }
61
-
62
- function tokenResponse(body: unknown, status = 200): Response {
63
- return new Response(JSON.stringify(body), {
64
- status,
65
- headers: { "content-type": "application/json" },
66
- });
67
- }
68
-
69
45
  let tmpBase: string;
70
46
 
71
47
  /** Stage the on-disk twitch guide into a tmp guides dir and load it. */
72
48
  async function setupRecipe(): Promise<{ guide: ApiGuide }> {
73
- const guidesDir = mkdtempSync(join(tmpBase, "guides-"));
74
- const domainDir = join(guidesDir, "twitch");
75
- mkdirSync(domainDir, { recursive: true });
76
- const source = readFileSync(new URL("./guide.md", import.meta.url), "utf-8");
77
- writeFileSync(join(domainDir, "guide.md"), source, "utf-8");
78
- setUserGuidesDir(guidesDir);
79
- invalidateCache();
80
- const loaded = loadApiGuidesFromDir(guidesDir);
81
- return { guide: loaded.guides["twitch"]! };
82
- }
83
-
84
- function findOp(guide: ApiGuide, name: string): Operation {
85
- const op = guide.operations.find((o) => o.name === name);
86
- if (!op) throw new Error(`op ${name} not found`);
87
- return op;
49
+ const { guides } = stageGuides(tmpBase, new URL("../", import.meta.url), [
50
+ "twitch",
51
+ ]);
52
+ return { guide: guides["twitch"]! };
88
53
  }
89
54
 
90
55
  beforeAll(() => {
@@ -19,20 +19,10 @@
19
19
  */
20
20
 
21
21
  import { describe, it, expect, vi, beforeAll, afterAll } from "vitest";
22
- import {
23
- mkdtempSync,
24
- mkdirSync,
25
- writeFileSync,
26
- rmSync,
27
- readFileSync,
28
- } from "node:fs";
22
+ import { mkdtempSync, rmSync } from "node:fs";
29
23
  import { tmpdir } from "node:os";
30
24
  import { join } from "node:path";
31
- import type {
32
- ApiGuide,
33
- OAuth2Auth,
34
- Operation,
35
- } from "../../core/api-guide-types.js";
25
+ import type { ApiGuide, OAuth2Auth } from "../../core/api-guide-types.js";
36
26
 
37
27
  // Mock the transport layer BEFORE any imports that use it.
38
28
  vi.mock("../../core/transport.js", async () => ({
@@ -42,59 +32,28 @@ vi.mock("../../core/transport.js", async () => ({
42
32
  fetchUrl: vi.fn(),
43
33
  }));
44
34
 
45
- import { loadApiGuidesFromDir } from "../../core/guide-catalog.js";
46
- import { setUserGuidesDir, invalidateCache } from "../../core/guide-store.js";
47
35
  import { resolveOpForExecution } from "../../core/resolve-op.js";
48
36
  import { setSecretsDir, writeSecret } from "../../core/secrets-store.js";
49
37
  import { setOAuthDir, readToken, writeToken } from "../../core/oauth-store.js";
38
+ import {
39
+ stageGuides,
40
+ findOp,
41
+ stubTokenEndpoint,
42
+ tokenResponse,
43
+ } from "../../__tests__/test-utils.js";
50
44
 
51
45
  const TT = "https://id.twitch.tv/oauth2/token";
52
46
  const STORE_DOMAIN = "twitch.tv"; // canonicalStoreDomain = guide.domains[0]
53
47
 
54
- function stubTokenEndpoint(
55
- handler: (url: string, init: RequestInit) => Response,
56
- ): void {
57
- vi.stubGlobal(
58
- "fetch",
59
- vi.fn((url: unknown, init?: RequestInit) =>
60
- Promise.resolve(handler(String(url), init ?? {})),
61
- ),
62
- );
63
- }
64
-
65
- function tokenResponse(body: unknown, status = 200): Response {
66
- return new Response(JSON.stringify(body), {
67
- status,
68
- headers: { "content-type": "application/json" },
69
- });
70
- }
71
-
72
48
  let tmpBase: string;
73
49
 
74
50
  /** Stage on-disk guides into a tmp guides dir and load them. */
75
51
  async function setupRecipes(
76
52
  dirs: string[],
77
53
  ): Promise<{ guides: Record<string, ApiGuide> }> {
78
- const guidesDir = mkdtempSync(join(tmpBase, "guides-"));
79
- for (const dir of dirs) {
80
- const domainDir = join(guidesDir, dir);
81
- mkdirSync(domainDir, { recursive: true });
82
- const source = readFileSync(
83
- new URL(`../${dir}/guide.md`, import.meta.url),
84
- "utf-8",
85
- );
86
- writeFileSync(join(domainDir, "guide.md"), source, "utf-8");
87
- }
88
- setUserGuidesDir(guidesDir);
89
- invalidateCache();
90
- const loaded = loadApiGuidesFromDir(guidesDir);
91
- return { guides: loaded.guides as Record<string, ApiGuide> };
92
- }
93
-
94
- function findOp(guide: ApiGuide, name: string): Operation {
95
- const op = guide.operations.find((o) => o.name === name);
96
- if (!op) throw new Error(`op ${name} not found`);
97
- return op;
54
+ // The test lives in api-guides/twitch-user/; sibling guide dirs resolve
55
+ // from the api-guides/ root.
56
+ return stageGuides(tmpBase, new URL("../", import.meta.url), dirs);
98
57
  }
99
58
 
100
59
  function provisionCreds(): void {
@@ -6,16 +6,10 @@
6
6
  */
7
7
 
8
8
  import { describe, it, expect, vi, beforeAll, afterAll } from "vitest";
9
- import {
10
- mkdtempSync,
11
- mkdirSync,
12
- writeFileSync,
13
- rmSync,
14
- readFileSync,
15
- } from "node:fs";
9
+ import { mkdtempSync, rmSync } from "node:fs";
16
10
  import { tmpdir } from "node:os";
17
11
  import { join } from "node:path";
18
- import type { ApiGuide, Operation } from "../../core/api-guide-types.js";
12
+ import type { ApiGuide } from "../../core/api-guide-types.js";
19
13
 
20
14
  // Mock the transport layer BEFORE any imports that use it.
21
15
  vi.mock("../../core/transport.js", async () => ({
@@ -27,8 +21,7 @@ vi.mock("../../core/transport.js", async () => ({
27
21
 
28
22
  import { restGet, paginate } from "../../core/helpers.js";
29
23
  import { loadTransform } from "../../core/local-helpers.js";
30
- import { loadApiGuidesFromDir } from "../../core/guide-catalog.js";
31
- import { setUserGuidesDir, invalidateCache } from "../../core/guide-store.js";
24
+ import { stageGuides, findOp } from "../../__tests__/test-utils.js";
32
25
  import { transform } from "./helper.ts";
33
26
 
34
27
  // ── Fixture — a GeoJSON FeatureCollection ───────────────────────────
@@ -77,23 +70,13 @@ const FC_BODY = JSON.stringify(FC);
77
70
  let tmpBase: string;
78
71
 
79
72
  async function setupRecipe(): Promise<{ guide: ApiGuide }> {
80
- const guidesDir = mkdtempSync(join(tmpBase, "guides-"));
81
- const domainDir = join(guidesDir, "usgs");
82
- mkdirSync(domainDir, { recursive: true });
83
- for (const file of ["guide.md", "helper.ts"] as const) {
84
- const source = readFileSync(new URL(`./${file}`, import.meta.url), "utf-8");
85
- writeFileSync(join(domainDir, file), source, "utf-8");
86
- }
87
- setUserGuidesDir(guidesDir);
88
- invalidateCache();
89
- const loaded = loadApiGuidesFromDir(guidesDir);
90
- return { guide: loaded.guides["usgs"]! };
91
- }
92
-
93
- function findOp(guide: ApiGuide, name: string): Operation {
94
- const op = guide.operations.find((o) => o.name === name);
95
- if (!op) throw new Error(`op ${name} not found`);
96
- return op;
73
+ const { guides } = stageGuides(
74
+ tmpBase,
75
+ new URL("../", import.meta.url),
76
+ ["usgs"],
77
+ ["guide.md", "helper.ts"],
78
+ );
79
+ return { guide: guides["usgs"]! };
97
80
  }
98
81
 
99
82
  beforeAll(() => {
@@ -19,13 +19,7 @@ import {
19
19
  afterAll,
20
20
  beforeEach,
21
21
  } from "vitest";
22
- import {
23
- mkdtempSync,
24
- mkdirSync,
25
- writeFileSync,
26
- rmSync,
27
- readFileSync,
28
- } from "node:fs";
22
+ import { mkdtempSync, rmSync } from "node:fs";
29
23
  import { tmpdir } from "node:os";
30
24
  import { join } from "node:path";
31
25
  import type { ApiGuide } from "../../core/api-guide-types.js";
@@ -39,8 +33,7 @@ vi.mock("../../core/transport.js", async () => ({
39
33
  }));
40
34
 
41
35
  import { paginate } from "../../core/helpers.js";
42
- import { loadApiGuidesFromDir } from "../../core/guide-catalog.js";
43
- import { setUserGuidesDir, invalidateCache } from "../../core/guide-store.js";
36
+ import { stageGuides } from "../../__tests__/test-utils.js";
44
37
  import { fetchUrl } from "../../core/transport.js";
45
38
 
46
39
  // Drain any under-consumed Once-queue so a leaky test can't shift its
@@ -100,15 +93,10 @@ const PAGE2 = JSON.stringify({
100
93
  let tmpBase: string;
101
94
 
102
95
  async function setupRecipe(): Promise<{ guide: ApiGuide }> {
103
- const guidesDir = mkdtempSync(join(tmpBase, "guides-"));
104
- const domainDir = join(guidesDir, "wikidata-search");
105
- mkdirSync(domainDir, { recursive: true });
106
- const source = readFileSync(new URL("./guide.md", import.meta.url), "utf-8");
107
- writeFileSync(join(domainDir, "guide.md"), source, "utf-8");
108
- setUserGuidesDir(guidesDir);
109
- invalidateCache();
110
- const loaded = loadApiGuidesFromDir(guidesDir);
111
- return { guide: loaded.guides["wikidata-search"]! };
96
+ const { guides } = stageGuides(tmpBase, new URL("../", import.meta.url), [
97
+ "wikidata-search",
98
+ ]);
99
+ return { guide: guides["wikidata-search"]! };
112
100
  }
113
101
 
114
102
  beforeAll(() => {
@@ -7,13 +7,7 @@
7
7
  */
8
8
 
9
9
  import { describe, it, expect, vi, beforeAll, afterAll } from "vitest";
10
- import {
11
- mkdtempSync,
12
- mkdirSync,
13
- writeFileSync,
14
- rmSync,
15
- readFileSync,
16
- } from "node:fs";
10
+ import { mkdtempSync, rmSync } from "node:fs";
17
11
  import { tmpdir } from "node:os";
18
12
  import { join } from "node:path";
19
13
  import type { ApiGuide } from "../../core/api-guide-types.js";
@@ -28,8 +22,7 @@ vi.mock("../../core/transport.js", async () => ({
28
22
 
29
23
  import { paginate } from "../../core/helpers.js";
30
24
  import { loadTransform } from "../../core/local-helpers.js";
31
- import { loadApiGuidesFromDir } from "../../core/guide-catalog.js";
32
- import { setUserGuidesDir, invalidateCache } from "../../core/guide-store.js";
25
+ import { stageGuides } from "../../__tests__/test-utils.js";
33
26
  import { transform } from "./helper.ts";
34
27
 
35
28
  const PAGE1 = JSON.stringify({
@@ -55,17 +48,13 @@ const PAGE2 = JSON.stringify({
55
48
  let tmpBase: string;
56
49
 
57
50
  async function setupRecipe(): Promise<{ guide: ApiGuide }> {
58
- const guidesDir = mkdtempSync(join(tmpBase, "guides-"));
59
- const domainDir = join(guidesDir, "wikimedia-action");
60
- mkdirSync(domainDir, { recursive: true });
61
- for (const file of ["guide.md", "helper.ts"] as const) {
62
- const source = readFileSync(new URL(`./${file}`, import.meta.url), "utf-8");
63
- writeFileSync(join(domainDir, file), source, "utf-8");
64
- }
65
- setUserGuidesDir(guidesDir);
66
- invalidateCache();
67
- const loaded = loadApiGuidesFromDir(guidesDir);
68
- return { guide: loaded.guides["wikimedia-action"]! };
51
+ const { guides } = stageGuides(
52
+ tmpBase,
53
+ new URL("../", import.meta.url),
54
+ ["wikimedia-action"],
55
+ ["guide.md", "helper.ts"],
56
+ );
57
+ return { guide: guides["wikimedia-action"]! };
69
58
  }
70
59
 
71
60
  beforeAll(() => {
@@ -698,7 +698,8 @@ function applyClientAuth(
698
698
  );
699
699
  headers["authorization"] = "Basic " + cred;
700
700
  return [cred, `Basic ${cred}`];
701
- } else if (method === "client_secret_post") {
701
+ }
702
+ if (method === "client_secret_post") {
702
703
  form["client_secret"] = clientSecret.secret;
703
704
  }
704
705
  return [];
@@ -372,13 +372,13 @@ export function resolveJsonPath(obj: unknown, path: string): unknown {
372
372
  if (current === null || typeof current !== "object") return undefined;
373
373
  if (Array.isArray(current)) {
374
374
  const idx = parseInt(part, 10);
375
- if (isNaN(idx)) return undefined;
375
+ if (Number.isNaN(idx)) return undefined;
376
376
  // Negative index addresses from the end; out of bounds → miss.
377
377
  const j = idx < 0 ? idx + current.length : idx;
378
378
  if (j < 0 || j >= current.length) return undefined;
379
- current = current[j];
379
+ current = current[j]; // nosemgrep: javascript.lang.security.audit.prototype-pollution.prototype-pollution-loop.prototype-pollution-loop
380
380
  } else {
381
- current = (current as Record<string, unknown>)[part];
381
+ current = (current as Record<string, unknown>)[part]; // nosemgrep: javascript.lang.security.audit.prototype-pollution.prototype-pollution-loop.prototype-pollution-loop
382
382
  }
383
383
  }
384
384
  return current;
@@ -730,16 +730,25 @@ function buildExecutorEnv(
730
730
  const literalHeaders =
731
731
  guide.auth.kind === "oauth2" ? undefined : guide.auth.headers;
732
732
  const extraHeaders = { ...literalHeaders, ...opts?.authHeaders };
733
+ // Shared fetch-args base: the env-owned slice of the FetchWithOptsArgs
734
+ // literal, assembled once so a new secret channel can't be wired into one
735
+ // executor and missed in the other. Callers add the situational fields
736
+ // (accept, fresh, guardRedirects, fallbackCharset).
737
+ const baseFetchArgs: Omit<FetchWithOptsArgs, "accept"> = {
738
+ extraHeaders,
739
+ secretHeaderNames: opts?.secretHeaderNames,
740
+ hasQuerySecret,
741
+ secretQueryParamNames: opts?.secretQueryParamNames,
742
+ hasPathSecret: hasPathSecrets,
743
+ redactPathSecret,
744
+ };
733
745
  return {
734
746
  fillParams,
735
747
  queryParamsForBuild,
736
748
  pathValues,
737
749
  secretParamNames,
738
750
  secretParams,
739
- hasQuerySecret,
740
- hasPathSecrets,
741
- redactPathSecret,
742
- extraHeaders,
751
+ baseFetchArgs,
743
752
  };
744
753
  }
745
754
 
@@ -807,10 +816,7 @@ export async function restGet(
807
816
  pathValues,
808
817
  secretParamNames,
809
818
  secretParams,
810
- hasQuerySecret,
811
- hasPathSecrets,
812
- redactPathSecret,
813
- extraHeaders,
819
+ baseFetchArgs,
814
820
  } = buildExecutorEnv(guide, params, opts);
815
821
 
816
822
  // Steps 1/2: fill path, build query (agent-supplied only — secret
@@ -847,15 +853,10 @@ export async function restGet(
847
853
  // APIs); an explicit header charset always wins.
848
854
  const shape = operation.parse ?? guide.responseShape;
849
855
  const result = await fetchWithOpts(fetchUrlRaw, {
856
+ ...baseFetchArgs,
850
857
  accept,
851
- extraHeaders,
852
858
  fresh: opts?.fresh,
853
859
  fallbackCharset: shape.charset,
854
- secretHeaderNames: opts?.secretHeaderNames,
855
- hasQuerySecret,
856
- secretQueryParamNames: opts?.secretQueryParamNames,
857
- hasPathSecret: hasPathSecrets,
858
- redactPathSecret,
859
860
  });
860
861
 
861
862
  // 7. Check HTTP status before attempting to parse the body.
@@ -973,10 +974,7 @@ export async function paginate(
973
974
  pathValues,
974
975
  secretParamNames,
975
976
  secretParams,
976
- hasQuerySecret,
977
- hasPathSecrets,
978
- redactPathSecret,
979
- extraHeaders,
977
+ baseFetchArgs,
980
978
  } = buildExecutorEnv(guide, params, opts);
981
979
 
982
980
  // State for the styles.
@@ -1003,7 +1001,7 @@ export async function paginate(
1003
1001
  operation.params[pagCfg.pageParam!]?.default ??
1004
1002
  fallback;
1005
1003
  page = typeof rawPage === "number" ? rawPage : parseInt(String(rawPage), 10);
1006
- if (isNaN(page)) page = fallback;
1004
+ if (Number.isNaN(page)) page = fallback;
1007
1005
  }
1008
1006
 
1009
1007
  // Compute effective params once — used for both per-page building and result transparency.
@@ -1028,7 +1026,7 @@ export async function paginate(
1028
1026
  : (params[pagCfg.pageSizeParam] ?? effectiveParams[pagCfg.pageSizeParam]);
1029
1027
  effectivePageSize =
1030
1028
  rawSize === undefined ? (pagCfg.pageSize ?? 50) : Number(rawSize);
1031
- if (isNaN(effectivePageSize)) effectivePageSize = 50;
1029
+ if (Number.isNaN(effectivePageSize)) effectivePageSize = 50;
1032
1030
  } else if (style === "cursor") {
1033
1031
  // Terminal fallback for cursor is OMIT (server default applies) — no
1034
1032
  // fabricated 50 like offset-limit (which needs a real number to advance
@@ -1092,8 +1090,10 @@ export async function paginate(
1092
1090
 
1093
1091
  // Every surfaced URL (incl. a server-supplied nextUrl that may
1094
1092
  // already carry the secret) is redacted at the capture point — query
1095
- // params by name, path tokens by value.
1096
- urls.push(redactSurfacedUrl(url, secretParamNames, pathValues));
1093
+ // params by name, path tokens by value. One redaction serves the
1094
+ // surfaced list, the status check, and the error-envelope URL below.
1095
+ const redactedUrl = redactSurfacedUrl(url, secretParamNames, pathValues);
1096
+ urls.push(redactedUrl);
1097
1097
 
1098
1098
  // NextLink guard — the URL comes from the remote server, so this is
1099
1099
  // the one place SSRF protection is load-bearing. `guardThisFetch`
@@ -1102,7 +1102,7 @@ export async function paginate(
1102
1102
  // nextLink to an internal host; when auth headers ship, the
1103
1103
  // Authorization header would attach to the redirect).
1104
1104
  const guardThisFetch =
1105
- style === "nextLink" && !!nextUrl && !opts?.skipSsrfGuard;
1105
+ style === "nextLink" && Boolean(nextUrl) && !opts?.skipSsrfGuard;
1106
1106
  if (guardThisFetch) {
1107
1107
  const guard = ssrfGuard(url);
1108
1108
  if (!guard.ok) {
@@ -1122,23 +1122,17 @@ export async function paginate(
1122
1122
 
1123
1123
  // Fetch.
1124
1124
  const result = await fetchWithOpts(url, {
1125
+ ...baseFetchArgs,
1125
1126
  accept,
1126
- extraHeaders,
1127
1127
  fresh: opts?.fresh,
1128
1128
  guardRedirects: guardThisFetch,
1129
1129
  fallbackCharset: shape.charset,
1130
- secretHeaderNames: opts?.secretHeaderNames,
1131
- hasQuerySecret,
1132
- secretQueryParamNames: opts?.secretQueryParamNames,
1133
- hasPathSecret: hasPathSecrets,
1134
- redactPathSecret,
1135
1130
  });
1136
1131
 
1137
1132
  // Check HTTP status before attempting to parse. Secret values scrubbed
1138
1133
  // from the error excerpt (output-channel audit). The URL stored on
1139
1134
  // the error object is redacted, computed upstream of checkResponseStatus.
1140
- const redactedPageUrl = redactSurfacedUrl(url, secretParamNames, pathValues);
1141
- checkResponseStatus({ ...result, url: redactedPageUrl }, opts?.secretValues);
1135
+ checkResponseStatus({ ...result, url: redactedUrl }, opts?.secretValues);
1142
1136
 
1143
1137
  // Parse.
1144
1138
  const data = parseResponse(result.body, shape);
@@ -1152,7 +1146,7 @@ export async function paginate(
1152
1146
  checkErrorEnvelope(
1153
1147
  data,
1154
1148
  operation.errorPath,
1155
- redactedPageUrl,
1149
+ redactedUrl,
1156
1150
  opts?.secretValues,
1157
1151
  );
1158
1152
  }