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
|
@@ -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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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 {
|
|
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
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
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 {
|
|
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
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
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 {
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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 {
|
|
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
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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 {
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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" &&
|
|
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
|
-
|
|
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
|
-
|
|
1149
|
+
redactedUrl,
|
|
1156
1150
|
opts?.secretValues,
|
|
1157
1151
|
);
|
|
1158
1152
|
}
|