@apifuse/provider-sdk 2.1.0-beta.4 → 2.1.0-beta.6
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/AUTHORING.md +24 -0
- package/CHANGELOG.md +11 -0
- package/README.md +23 -2
- package/SUBMISSION.md +2 -1
- package/bin/apifuse-check.ts +60 -6
- package/bin/apifuse-dev.ts +48 -5
- package/bin/apifuse-perf.ts +106 -26
- package/bin/apifuse-record.ts +142 -52
- package/bin/apifuse-submit-check.ts +1489 -3
- package/package.json +107 -92
- package/src/ceremonies/index.ts +8 -2
- package/src/choice-token.ts +1 -0
- package/src/cli/commands.ts +10 -8
- package/src/cli/create.ts +49 -1
- package/src/cli/templates/provider/.dockerignore.tpl +22 -0
- package/src/cli/templates/provider/.gitignore.tpl +22 -0
- package/src/cli/templates/provider/README.md.tpl +18 -0
- package/src/cli/templates/provider/operations/ping.ts.tpl +3 -2
- package/src/cli/templates/provider/schemas/ping.ts.tpl +8 -0
- package/src/config/loader.ts +19 -1
- package/src/contract-json.ts +75 -0
- package/src/contract-serialization.ts +89 -0
- package/src/contract-types.ts +52 -0
- package/src/contract.ts +215 -0
- package/src/define.ts +40 -5
- package/src/errors.ts +15 -0
- package/src/i18n/catalog.ts +156 -0
- package/src/index.ts +22 -1
- package/src/lint.ts +265 -46
- package/src/provider.ts +45 -2
- package/src/runtime/browser.ts +685 -30
- package/src/runtime/cache.ts +35 -89
- package/src/runtime/choice.ts +760 -0
- package/src/runtime/executor.ts +19 -2
- package/src/runtime/redis.ts +116 -0
- package/src/runtime/state.ts +487 -0
- package/src/runtime/stealth.ts +8 -1
- package/src/runtime/trace.ts +1 -1
- package/src/server/serve.ts +361 -46
- package/src/server/types.ts +2 -0
- package/src/testing/run.ts +16 -3
- package/src/types.ts +225 -18
package/package.json
CHANGED
|
@@ -1,94 +1,109 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
2
|
+
"name": "@apifuse/provider-sdk",
|
|
3
|
+
"version": "2.1.0-beta.6",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "APIFuse Provider SDK \u2014 Build providers with zero architectural constraints",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"main": "./src/index.ts",
|
|
9
|
+
"types": "./src/index.ts",
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"src",
|
|
15
|
+
"!src/__tests__",
|
|
16
|
+
"!src/__tests__/**",
|
|
17
|
+
"!src/**/*.test.ts",
|
|
18
|
+
"!src/index.test.ts",
|
|
19
|
+
"bin",
|
|
20
|
+
"README.md",
|
|
21
|
+
"AUTHORING.md",
|
|
22
|
+
"CHANGELOG.md",
|
|
23
|
+
"SUBMISSION.md"
|
|
24
|
+
],
|
|
25
|
+
"keywords": [
|
|
26
|
+
"apifuse",
|
|
27
|
+
"provider",
|
|
28
|
+
"sdk",
|
|
29
|
+
"api",
|
|
30
|
+
"zod",
|
|
31
|
+
"hono"
|
|
32
|
+
],
|
|
33
|
+
"bin": {
|
|
34
|
+
"apifuse": "./bin/apifuse.ts"
|
|
35
|
+
},
|
|
36
|
+
"exports": {
|
|
37
|
+
".": {
|
|
38
|
+
"default": "./src/index.ts",
|
|
39
|
+
"import": "./src/index.ts",
|
|
40
|
+
"types": "./src/index.ts"
|
|
41
|
+
},
|
|
42
|
+
"./provider": {
|
|
43
|
+
"default": "./src/provider.ts",
|
|
44
|
+
"import": "./src/provider.ts",
|
|
45
|
+
"types": "./src/provider.ts"
|
|
46
|
+
},
|
|
47
|
+
"./contract": {
|
|
48
|
+
"default": "./src/contract.ts",
|
|
49
|
+
"import": "./src/contract.ts",
|
|
50
|
+
"types": "./src/contract.ts"
|
|
51
|
+
},
|
|
52
|
+
"./server": {
|
|
53
|
+
"default": "./src/server/index.ts",
|
|
54
|
+
"import": "./src/server/index.ts",
|
|
55
|
+
"types": "./src/server/index.ts"
|
|
56
|
+
},
|
|
57
|
+
"./testing": {
|
|
58
|
+
"default": "./src/testing/index.ts",
|
|
59
|
+
"import": "./src/testing/index.ts",
|
|
60
|
+
"types": "./src/testing/index.ts"
|
|
61
|
+
},
|
|
62
|
+
"./create": {
|
|
63
|
+
"default": "./src/cli/create.ts",
|
|
64
|
+
"import": "./src/cli/create.ts",
|
|
65
|
+
"types": "./src/cli/create.ts"
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"scripts": {
|
|
69
|
+
"lint": "biome lint .",
|
|
70
|
+
"lint:fix": "biome lint --write",
|
|
71
|
+
"format": "biome format --write",
|
|
72
|
+
"type-check": "tsgo --noEmit",
|
|
73
|
+
"test": "bun test",
|
|
74
|
+
"check": "bun run lint && bun run type-check",
|
|
75
|
+
"pack:check": "bun bin/apifuse-pack-check.ts",
|
|
76
|
+
"pack:smoke": "bun bin/apifuse-pack-smoke.ts",
|
|
77
|
+
"release:guard": "bun scripts/guard-release-pr.ts",
|
|
78
|
+
"format:check": "biome format ."
|
|
79
|
+
},
|
|
80
|
+
"devDependencies": {
|
|
81
|
+
"@biomejs/biome": "^2.5.0",
|
|
82
|
+
"@types/bun": "latest",
|
|
83
|
+
"@types/node": "^25.9.3",
|
|
84
|
+
"@typescript/native-preview": "7.0.0-dev.20260419.1"
|
|
85
|
+
},
|
|
86
|
+
"dependencies": {
|
|
87
|
+
"@clack/prompts": "^1.5.1",
|
|
88
|
+
"@types/ms": "^2.1.0",
|
|
89
|
+
"ajv": "^8.17",
|
|
90
|
+
"hono": "^4.12.25",
|
|
91
|
+
"impit": "0.14.1",
|
|
92
|
+
"ioredis": "^5.11.1",
|
|
93
|
+
"ms": "^2.1.3",
|
|
94
|
+
"playwright": "^1.55.1",
|
|
95
|
+
"playwright-extra": "^4.3.6",
|
|
96
|
+
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
|
97
|
+
"re2-wasm": "^1.0",
|
|
98
|
+
"safe-regex": "^2.1",
|
|
99
|
+
"zod": "^4.4.3"
|
|
100
|
+
},
|
|
101
|
+
"repository": {
|
|
102
|
+
"type": "git",
|
|
103
|
+
"url": "git+https://github.com/APIFuseHQ/provider-sdk.git"
|
|
104
|
+
},
|
|
105
|
+
"bugs": {
|
|
106
|
+
"url": "https://github.com/APIFuseHQ/provider-sdk/issues"
|
|
107
|
+
},
|
|
108
|
+
"homepage": "https://github.com/APIFuseHQ/provider-sdk#readme"
|
|
94
109
|
}
|
package/src/ceremonies/index.ts
CHANGED
|
@@ -8,7 +8,12 @@ import {
|
|
|
8
8
|
TurnValidationError,
|
|
9
9
|
ValidationError,
|
|
10
10
|
} from "../errors";
|
|
11
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
AuthFlowDefinition,
|
|
13
|
+
AuthFlowInputHandler,
|
|
14
|
+
AuthTurn,
|
|
15
|
+
FlowContext,
|
|
16
|
+
} from "../types";
|
|
12
17
|
|
|
13
18
|
type TurnKind =
|
|
14
19
|
| "abort"
|
|
@@ -21,7 +26,7 @@ type TurnKind =
|
|
|
21
26
|
| "redirect"
|
|
22
27
|
| "retry";
|
|
23
28
|
|
|
24
|
-
type CeremonyHandler =
|
|
29
|
+
type CeremonyHandler = AuthFlowInputHandler;
|
|
25
30
|
|
|
26
31
|
type JsonObject = Record<string, unknown>;
|
|
27
32
|
|
|
@@ -44,6 +49,7 @@ const authTurnSchema = {
|
|
|
44
49
|
additionalProperties: true,
|
|
45
50
|
},
|
|
46
51
|
hint: { type: "string" },
|
|
52
|
+
hintKey: { type: "string" },
|
|
47
53
|
timing: {
|
|
48
54
|
type: "object",
|
|
49
55
|
additionalProperties: false,
|
package/src/choice-token.ts
CHANGED
package/src/cli/commands.ts
CHANGED
|
@@ -36,14 +36,14 @@ export const COMMAND_MANIFEST: Record<
|
|
|
36
36
|
summary:
|
|
37
37
|
"Start the local provider dev server with the standard provider server contract.",
|
|
38
38
|
usage: "apifuse dev [path]",
|
|
39
|
-
examples: ["apifuse dev .", "apifuse dev providers/
|
|
39
|
+
examples: ["apifuse dev .", "apifuse dev providers/korea-air-quality"],
|
|
40
40
|
modulePath: "./apifuse-dev",
|
|
41
41
|
},
|
|
42
42
|
check: {
|
|
43
43
|
name: "check",
|
|
44
44
|
summary: "Validate provider structure, metadata, fixtures, and schemas.",
|
|
45
45
|
usage: "apifuse check [path]",
|
|
46
|
-
examples: ["apifuse check .", "apifuse check providers/
|
|
46
|
+
examples: ["apifuse check .", "apifuse check providers/korea-air-quality"],
|
|
47
47
|
modulePath: "./apifuse-check",
|
|
48
48
|
},
|
|
49
49
|
"submit-check": {
|
|
@@ -72,8 +72,7 @@ export const COMMAND_MANIFEST: Record<
|
|
|
72
72
|
usage:
|
|
73
73
|
'apifuse record [path] --operation <operation> --params \'{"value":"hello"}\'',
|
|
74
74
|
examples: [
|
|
75
|
-
'apifuse record
|
|
76
|
-
'apifuse record providers/airkorea --operation realtime --params \'{"stationName":"종로구"}\'',
|
|
75
|
+
'apifuse record providers/korea-air-quality --operation realtime --params \'{"stationName":"종로구"}\'',
|
|
77
76
|
],
|
|
78
77
|
modulePath: "./apifuse-record",
|
|
79
78
|
},
|
|
@@ -81,17 +80,20 @@ export const COMMAND_MANIFEST: Record<
|
|
|
81
80
|
name: "test",
|
|
82
81
|
summary: "Run provider-focused tests and surface actionable failures.",
|
|
83
82
|
usage: "apifuse test [path] [--json] [--verbose]",
|
|
84
|
-
examples: [
|
|
83
|
+
examples: [
|
|
84
|
+
"apifuse test .",
|
|
85
|
+
"apifuse test providers/korea-air-quality --json",
|
|
86
|
+
],
|
|
85
87
|
modulePath: "./apifuse-test",
|
|
86
88
|
},
|
|
87
89
|
perf: {
|
|
88
90
|
name: "perf",
|
|
89
91
|
summary:
|
|
90
92
|
"Profile a provider operation and export latency/trace diagnostics.",
|
|
91
|
-
usage:
|
|
93
|
+
usage:
|
|
94
|
+
"apifuse perf <path> --operation <operation> [--params '<json>'] [options]",
|
|
92
95
|
examples: [
|
|
93
|
-
|
|
94
|
-
"apifuse perf providers/airkorea --operation realtime --runs 5",
|
|
96
|
+
'apifuse perf providers/korea-air-quality --operation realtime --params \'{"stationName":"종로구"}\' --runs 5',
|
|
95
97
|
],
|
|
96
98
|
modulePath: "./apifuse-perf",
|
|
97
99
|
},
|
package/src/cli/create.ts
CHANGED
|
@@ -474,6 +474,14 @@ export async function buildProviderCreatePlan(
|
|
|
474
474
|
};
|
|
475
475
|
|
|
476
476
|
const files: ProviderPlanFile[] = [
|
|
477
|
+
{
|
|
478
|
+
path: resolve(providerRoot, ".dockerignore"),
|
|
479
|
+
content: await renderTemplate(".dockerignore.tpl", {}),
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
path: resolve(providerRoot, ".gitignore"),
|
|
483
|
+
content: await renderTemplate(".gitignore.tpl", {}),
|
|
484
|
+
},
|
|
477
485
|
{
|
|
478
486
|
path: resolve(providerRoot, "index.ts"),
|
|
479
487
|
content: await renderTemplate("index.ts.tpl", templateValues),
|
|
@@ -494,6 +502,15 @@ export async function buildProviderCreatePlan(
|
|
|
494
502
|
path: resolve(providerRoot, "operations", "ping.ts"),
|
|
495
503
|
content: await renderTemplate("operations/ping.ts.tpl", {
|
|
496
504
|
DISPLAY_NAME: escapeTemplate(options.displayName),
|
|
505
|
+
HANDLER_CTX: options.runtime === "browser" ? "ctx" : "_ctx",
|
|
506
|
+
BROWSER_HANDLER_BLOCK:
|
|
507
|
+
options.runtime === "browser"
|
|
508
|
+
? '\n const page = await ctx.browser.newPage();\n await page.goto("https://example.com");\n const title = await page.title();\n const frames = await page.frames();\n await page.close();\n'
|
|
509
|
+
: "",
|
|
510
|
+
BROWSER_RESPONSE_FIELDS:
|
|
511
|
+
options.runtime === "browser"
|
|
512
|
+
? ",\n pageTitle: title,\n frameCount: frames.length"
|
|
513
|
+
: "",
|
|
497
514
|
}),
|
|
498
515
|
},
|
|
499
516
|
{
|
|
@@ -574,6 +591,7 @@ export async function buildProviderCreatePlan(
|
|
|
574
591
|
providerRoot,
|
|
575
592
|
validationCommands: [
|
|
576
593
|
"bun run check",
|
|
594
|
+
"bun run type-check",
|
|
577
595
|
"bun run submit-check",
|
|
578
596
|
"bun run test",
|
|
579
597
|
],
|
|
@@ -605,7 +623,8 @@ function renderPackageJson(input: {
|
|
|
605
623
|
main: "./index.ts",
|
|
606
624
|
scripts: {
|
|
607
625
|
dev: "apifuse dev .",
|
|
608
|
-
check: "apifuse check .",
|
|
626
|
+
check: "apifuse check . && bun run type-check",
|
|
627
|
+
"type-check": "tsc --noEmit",
|
|
609
628
|
"submit-check":
|
|
610
629
|
"apifuse submit-check . --markdown submission-report.md",
|
|
611
630
|
test: "apifuse test .",
|
|
@@ -618,6 +637,7 @@ function renderPackageJson(input: {
|
|
|
618
637
|
},
|
|
619
638
|
devDependencies: {
|
|
620
639
|
"@types/bun": "latest",
|
|
640
|
+
typescript: "^6.0.3",
|
|
621
641
|
},
|
|
622
642
|
},
|
|
623
643
|
null,
|
|
@@ -636,6 +656,7 @@ function renderTsconfig(): string {
|
|
|
636
656
|
noEmit: true,
|
|
637
657
|
skipLibCheck: true,
|
|
638
658
|
resolveJsonModule: true,
|
|
659
|
+
types: ["bun"],
|
|
639
660
|
},
|
|
640
661
|
include: ["**/*.ts"],
|
|
641
662
|
exclude: ["node_modules"],
|
|
@@ -689,6 +710,14 @@ function renderStarterLocaleCatalog(
|
|
|
689
710
|
locale === "ko"
|
|
690
711
|
? "생성된 provider가 샘플 payload를 round-trip했음을 보여주는 사람이 읽을 수 있는 확인 메시지"
|
|
691
712
|
: "Human-readable confirmation that the generated provider round-tripped the sample payload.",
|
|
713
|
+
pageTitle:
|
|
714
|
+
locale === "ko"
|
|
715
|
+
? "browser 런타임 provider일 때 로드된 페이지의 제목 (해당되지 않으면 생략)"
|
|
716
|
+
: "Title of the loaded page when the provider uses the browser runtime; omitted otherwise.",
|
|
717
|
+
frameCount:
|
|
718
|
+
locale === "ko"
|
|
719
|
+
? "browser 런타임 provider일 때 로드된 페이지의 frame 개수 (해당되지 않으면 생략)"
|
|
720
|
+
: "Number of frames in the loaded page when the provider uses the browser runtime; omitted otherwise.",
|
|
692
721
|
},
|
|
693
722
|
},
|
|
694
723
|
};
|
|
@@ -732,6 +761,17 @@ function renderAuthBlock(authMode: CreateAuthMode): string {
|
|
|
732
761
|
},
|
|
733
762
|
hint: "Generated placeholder credential flow completed. Replace this with real auth logic.",
|
|
734
763
|
}),
|
|
764
|
+
refresh: async () => ({
|
|
765
|
+
kind: "complete",
|
|
766
|
+
turnId: crypto.randomUUID(),
|
|
767
|
+
data: {
|
|
768
|
+
credential: {
|
|
769
|
+
username: "replace-with-refreshed-username",
|
|
770
|
+
password: "replace-with-refreshed-password",
|
|
771
|
+
},
|
|
772
|
+
},
|
|
773
|
+
hint: "Return refreshed credential data here, or throw AuthError with code AUTH_REQUIRED when silent refresh is not possible.",
|
|
774
|
+
}),
|
|
735
775
|
},
|
|
736
776
|
}`;
|
|
737
777
|
case "oauth2":
|
|
@@ -952,6 +992,14 @@ function printResult(
|
|
|
952
992
|
console.log(`Validation: (cd ${plan.providerRoot} && ${command})`);
|
|
953
993
|
}
|
|
954
994
|
console.log(`Next local dev: ${plan.nextDevCommand}`);
|
|
995
|
+
console.log(
|
|
996
|
+
"Submission evidence: run `bun run submit-check`, save the generated report, and note `/health` plus `POST /v1/{operation}` smoke results.",
|
|
997
|
+
);
|
|
998
|
+
if (plan.files.some((file) => file.content.includes('runtime: "browser"'))) {
|
|
999
|
+
console.log(
|
|
1000
|
+
"Browser runtime: run `bunx playwright install chromium` locally or set `APIFUSE__CDP_POOL__URL` before browser-backed smoke tests.",
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
955
1003
|
}
|
|
956
1004
|
|
|
957
1005
|
function escapeTemplate(value: string): string {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
node_modules/
|
|
2
|
+
.git/
|
|
3
|
+
.github/
|
|
4
|
+
|
|
5
|
+
# Environment and local secrets
|
|
6
|
+
.env
|
|
7
|
+
.env.*
|
|
8
|
+
!.env.example
|
|
9
|
+
|
|
10
|
+
# Local reports and generated artifacts
|
|
11
|
+
submission-report.md
|
|
12
|
+
coverage/
|
|
13
|
+
dist/
|
|
14
|
+
.cache/
|
|
15
|
+
.turbo/
|
|
16
|
+
.bun/
|
|
17
|
+
*.tsbuildinfo
|
|
18
|
+
|
|
19
|
+
# OS/editor junk
|
|
20
|
+
.DS_Store
|
|
21
|
+
Thumbs.db
|
|
22
|
+
*.swp
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
node_modules/
|
|
2
|
+
|
|
3
|
+
# Environment and local secrets
|
|
4
|
+
.env
|
|
5
|
+
.env.*
|
|
6
|
+
!.env.example
|
|
7
|
+
|
|
8
|
+
# Build, coverage, cache, and local runtime artifacts
|
|
9
|
+
coverage/
|
|
10
|
+
dist/
|
|
11
|
+
.cache/
|
|
12
|
+
.turbo/
|
|
13
|
+
.bun/
|
|
14
|
+
*.tsbuildinfo
|
|
15
|
+
|
|
16
|
+
# Bounty submission output
|
|
17
|
+
submission-report.md
|
|
18
|
+
|
|
19
|
+
# OS/editor junk
|
|
20
|
+
.DS_Store
|
|
21
|
+
Thumbs.db
|
|
22
|
+
*.swp
|
|
@@ -140,3 +140,21 @@ Every operation must declare exactly one of:
|
|
|
140
140
|
|
|
141
141
|
The generated `ping` operation uses `healthCheckUnsupported` only because it is
|
|
142
142
|
a local scaffold check, not a real upstream API probe.
|
|
143
|
+
|
|
144
|
+
`healthCheck.cases[].assertions` receives `{ data, status, durationMs, meta }`.
|
|
145
|
+
`data` is the parsed operation output. Use this shape in real operations:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
healthCheck: {
|
|
149
|
+
interval: "5m",
|
|
150
|
+
cases: [{
|
|
151
|
+
name: "lookup baseline",
|
|
152
|
+
input: { q: "btc" },
|
|
153
|
+
assertions: ({ data, status, durationMs }) => {
|
|
154
|
+
if (status !== 200 || data.results.length === 0 || durationMs > 3000) {
|
|
155
|
+
return { status: "degraded", label: "lookup baseline changed" };
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
}],
|
|
159
|
+
}
|
|
160
|
+
```
|
|
@@ -6,10 +6,11 @@ export const pingOperation = defineOperation({
|
|
|
6
6
|
descriptionKey: "operations.ping.description",
|
|
7
7
|
input: pingInputSchema,
|
|
8
8
|
output: pingOutputSchema,
|
|
9
|
-
handler: async (
|
|
9
|
+
handler: async ({{HANDLER_CTX}}, input) => {
|
|
10
|
+
{{BROWSER_HANDLER_BLOCK}}
|
|
10
11
|
return {
|
|
11
12
|
ok: true,
|
|
12
|
-
message: "{{DISPLAY_NAME}} received: " + input.value,
|
|
13
|
+
message: "{{DISPLAY_NAME}} received: " + input.value{{BROWSER_RESPONSE_FIELDS}},
|
|
13
14
|
};
|
|
14
15
|
},
|
|
15
16
|
fixtures: {
|
|
@@ -11,6 +11,14 @@ export const pingOutputSchema = describeKey(
|
|
|
11
11
|
z.object({
|
|
12
12
|
ok: describeKey(z.boolean(), "schemaDescriptions.output.ok"),
|
|
13
13
|
message: describeKey(z.string(), "schemaDescriptions.output.message"),
|
|
14
|
+
pageTitle: describeKey(
|
|
15
|
+
z.string().optional(),
|
|
16
|
+
"schemaDescriptions.output.pageTitle",
|
|
17
|
+
),
|
|
18
|
+
frameCount: describeKey(
|
|
19
|
+
z.number().int().nonnegative().optional(),
|
|
20
|
+
"schemaDescriptions.output.frameCount",
|
|
21
|
+
),
|
|
14
22
|
}),
|
|
15
23
|
"schemaDescriptions.output.root",
|
|
16
24
|
);
|
package/src/config/loader.ts
CHANGED
|
@@ -16,6 +16,8 @@ export const DEFAULT_PROXY_LIFETIME_ENV =
|
|
|
16
16
|
"APIFUSE__PROXY__DEFAULT_LIFETIME_MINUTES";
|
|
17
17
|
export const PROVIDER_CACHE_REDIS_URL_ENV =
|
|
18
18
|
"APIFUSE__PROVIDER__CACHE_REDIS_URL";
|
|
19
|
+
export const PROVIDER_STATE_REDIS_URL_ENV =
|
|
20
|
+
"APIFUSE__PROVIDER__STATE_REDIS_URL";
|
|
19
21
|
export const REDIS_URL_ENV = "APIFUSE__REDIS__URL";
|
|
20
22
|
|
|
21
23
|
export type ProxyOptions = {
|
|
@@ -175,6 +177,19 @@ const SMARTPROXY_EXTRACTION_SOFT_REFRESH_MS = 10_000;
|
|
|
175
177
|
|
|
176
178
|
function redisUrlFromEnv(): string | undefined {
|
|
177
179
|
return (
|
|
180
|
+
process.env.APIFUSE__PROVIDER__CACHE_REDIS_URL?.trim() ||
|
|
181
|
+
process.env[REDIS_URL_ENV]?.trim() ||
|
|
182
|
+
undefined
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function providerCacheRedisUrlFromEnv(): string | undefined {
|
|
187
|
+
return redisUrlFromEnv();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function providerStateRedisUrlFromEnv(): string | undefined {
|
|
191
|
+
return (
|
|
192
|
+
process.env[PROVIDER_STATE_REDIS_URL_ENV]?.trim() ||
|
|
178
193
|
process.env[PROVIDER_CACHE_REDIS_URL_ENV]?.trim() ||
|
|
179
194
|
process.env[REDIS_URL_ENV]?.trim() ||
|
|
180
195
|
undefined
|
|
@@ -620,7 +635,10 @@ function selectProxyPoolIndex(poolSize: number, attempt = 0): number {
|
|
|
620
635
|
if (poolSize <= 1) {
|
|
621
636
|
return 0;
|
|
622
637
|
}
|
|
623
|
-
|
|
638
|
+
const normalizedAttempt = Number.isFinite(attempt)
|
|
639
|
+
? Math.max(0, Math.floor(attempt))
|
|
640
|
+
: 0;
|
|
641
|
+
return normalizedAttempt % poolSize;
|
|
624
642
|
}
|
|
625
643
|
|
|
626
644
|
function buildSmartproxyCacheKey(
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export type JsonPrimitive = string | number | boolean | null;
|
|
2
|
+
export type JsonValue =
|
|
3
|
+
| JsonPrimitive
|
|
4
|
+
| readonly JsonValue[]
|
|
5
|
+
| { readonly [key: string]: JsonValue };
|
|
6
|
+
|
|
7
|
+
export function canonicalJson(value: unknown): string {
|
|
8
|
+
return JSON.stringify(canonicalize(toJsonValue(value) ?? null));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function toJsonValue(value: unknown): JsonValue | undefined {
|
|
12
|
+
if (
|
|
13
|
+
value === null ||
|
|
14
|
+
typeof value === "string" ||
|
|
15
|
+
typeof value === "boolean"
|
|
16
|
+
) {
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
if (typeof value === "number") {
|
|
20
|
+
return Number.isFinite(value) ? value : undefined;
|
|
21
|
+
}
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
return value.flatMap((item) => {
|
|
24
|
+
const json = toJsonValue(item);
|
|
25
|
+
return json === undefined ? [] : [json];
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
if (!isRecord(value)) return undefined;
|
|
29
|
+
return compactObject(
|
|
30
|
+
Object.fromEntries(
|
|
31
|
+
Object.entries(value).flatMap(([key, item]) => {
|
|
32
|
+
const json = toJsonValue(item);
|
|
33
|
+
return json === undefined ? [] : [[key, json]];
|
|
34
|
+
}),
|
|
35
|
+
),
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function compactObject(
|
|
40
|
+
value: Record<string, JsonValue | undefined>,
|
|
41
|
+
): JsonValue {
|
|
42
|
+
return Object.fromEntries(
|
|
43
|
+
Object.entries(value).filter((entry): entry is [string, JsonValue] => {
|
|
44
|
+
const [, item] = entry;
|
|
45
|
+
return item !== undefined;
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function copyRecordWithout(
|
|
51
|
+
value: unknown,
|
|
52
|
+
ignoredKeys: ReadonlySet<string>,
|
|
53
|
+
): Record<string, unknown> {
|
|
54
|
+
if (!isRecord(value)) return {};
|
|
55
|
+
return Object.fromEntries(
|
|
56
|
+
Object.entries(value).filter(([key]) => !ignoredKeys.has(key)),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
61
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function canonicalize(value: JsonValue): JsonValue {
|
|
65
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
66
|
+
if (!isRecord(value)) return value;
|
|
67
|
+
return Object.fromEntries(
|
|
68
|
+
Object.entries(value)
|
|
69
|
+
.sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey))
|
|
70
|
+
.flatMap(([key, item]) => {
|
|
71
|
+
const json = toJsonValue(item);
|
|
72
|
+
return json === undefined ? [] : [[key, canonicalize(json)]];
|
|
73
|
+
}),
|
|
74
|
+
);
|
|
75
|
+
}
|