@apifuse/provider-sdk 2.1.0-beta.1 → 2.1.0-beta.3
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 +27 -0
- package/CHANGELOG.md +14 -0
- package/README.md +83 -1
- package/SUBMISSION.md +86 -0
- package/bin/apifuse-check.ts +30 -2
- package/bin/apifuse-dev.ts +30 -6
- package/bin/apifuse-pack-check.ts +58 -0
- package/bin/apifuse-pack-smoke.ts +181 -1
- package/bin/apifuse-submit-check.ts +880 -0
- package/package.json +8 -7
- package/src/cli/commands.ts +23 -0
- package/src/cli/create.ts +9 -4
- package/src/cli/templates/provider/README.md.tpl +85 -1
- package/src/cli/templates/provider/index.ts.tpl +0 -1
- package/src/define.ts +92 -1
- package/src/index.ts +3 -0
- package/src/server/serve.ts +12 -3
- package/src/types.ts +20 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apifuse/provider-sdk",
|
|
3
|
-
"version": "2.1.0-beta.
|
|
3
|
+
"version": "2.1.0-beta.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "ApiFuse Provider SDK — Build providers with zero architectural constraints",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"bin",
|
|
20
20
|
"README.md",
|
|
21
21
|
"AUTHORING.md",
|
|
22
|
-
"CHANGELOG.md"
|
|
22
|
+
"CHANGELOG.md",
|
|
23
|
+
"SUBMISSION.md"
|
|
23
24
|
],
|
|
24
25
|
"keywords": [
|
|
25
26
|
"apifuse",
|
|
@@ -65,20 +66,20 @@
|
|
|
65
66
|
"pack:smoke": "bun bin/apifuse-pack-smoke.ts"
|
|
66
67
|
},
|
|
67
68
|
"devDependencies": {
|
|
68
|
-
"@biomejs/biome": "^2.4.
|
|
69
|
+
"@biomejs/biome": "^2.4.15",
|
|
69
70
|
"@types/bun": "latest",
|
|
70
|
-
"@types/node": "^25.
|
|
71
|
+
"@types/node": "^25.8.0",
|
|
71
72
|
"typescript": "^6.0.3"
|
|
72
73
|
},
|
|
73
74
|
"dependencies": {
|
|
74
|
-
"@clack/prompts": "^1.
|
|
75
|
+
"@clack/prompts": "^1.4.0",
|
|
75
76
|
"ajv": "^8.17",
|
|
76
|
-
"hono": "^4.12.
|
|
77
|
+
"hono": "^4.12.19",
|
|
77
78
|
"playwright": "^1.55.1",
|
|
78
79
|
"playwright-stealth": "^0.0.1",
|
|
79
80
|
"re2-wasm": "^1.0",
|
|
80
81
|
"safe-regex": "^2.1",
|
|
81
82
|
"tlsclientwrapper": "^4.2.0",
|
|
82
|
-
"zod": "^4.3
|
|
83
|
+
"zod": "^4.4.3"
|
|
83
84
|
}
|
|
84
85
|
}
|
package/src/cli/commands.ts
CHANGED
|
@@ -2,6 +2,8 @@ export type ApifuseCommandName =
|
|
|
2
2
|
| "create"
|
|
3
3
|
| "dev"
|
|
4
4
|
| "check"
|
|
5
|
+
| "submit-check"
|
|
6
|
+
| "bounty-check"
|
|
5
7
|
| "record"
|
|
6
8
|
| "test"
|
|
7
9
|
| "perf";
|
|
@@ -46,6 +48,26 @@ export const COMMAND_MANIFEST: Record<
|
|
|
46
48
|
examples: ["apifuse check .", "apifuse check providers/airkorea"],
|
|
47
49
|
modulePath: "./apifuse-check",
|
|
48
50
|
},
|
|
51
|
+
"submit-check": {
|
|
52
|
+
name: "submit-check",
|
|
53
|
+
summary:
|
|
54
|
+
"Score provider bounty submission readiness and emit checklist evidence.",
|
|
55
|
+
usage:
|
|
56
|
+
"apifuse submit-check [path] [--tier bronze|silver|gold|diamond] [--json] [--markdown <path>] [--smoke-note <text>]",
|
|
57
|
+
examples: [
|
|
58
|
+
"apifuse submit-check .",
|
|
59
|
+
"apifuse submit-check . --tier silver --markdown submission-report.md",
|
|
60
|
+
],
|
|
61
|
+
modulePath: "./apifuse-submit-check",
|
|
62
|
+
},
|
|
63
|
+
"bounty-check": {
|
|
64
|
+
name: "bounty-check",
|
|
65
|
+
summary: "Alias for submit-check.",
|
|
66
|
+
usage:
|
|
67
|
+
"apifuse bounty-check [path] [--tier bronze|silver|gold|diamond] [--json] [--markdown <path>] [--smoke-note <text>]",
|
|
68
|
+
examples: ["apifuse bounty-check . --markdown submission-report.md"],
|
|
69
|
+
modulePath: "./apifuse-submit-check",
|
|
70
|
+
},
|
|
49
71
|
record: {
|
|
50
72
|
name: "record",
|
|
51
73
|
summary: "Call a provider operation and capture upstream raw fixture data.",
|
|
@@ -81,6 +103,7 @@ export const COMMAND_ORDER: ApifuseCommandName[] = [
|
|
|
81
103
|
"create",
|
|
82
104
|
"dev",
|
|
83
105
|
"check",
|
|
106
|
+
"submit-check",
|
|
84
107
|
"record",
|
|
85
108
|
"test",
|
|
86
109
|
"perf",
|
package/src/cli/create.ts
CHANGED
|
@@ -479,7 +479,7 @@ export async function buildProviderCreatePlan(
|
|
|
479
479
|
RUNTIME: options.runtime,
|
|
480
480
|
BROWSER_BLOCK:
|
|
481
481
|
options.runtime === "browser"
|
|
482
|
-
? ',\n browser: {\n engine: "
|
|
482
|
+
? ',\n browser: {\n engine: "playwright-stealth",\n }'
|
|
483
483
|
: "",
|
|
484
484
|
SECRETS_BLOCK: renderSecretsBlock(options.authMode),
|
|
485
485
|
CREDENTIAL_BLOCK: renderCredentialBlock(options.authMode),
|
|
@@ -538,7 +538,11 @@ export async function buildProviderCreatePlan(
|
|
|
538
538
|
packageName,
|
|
539
539
|
preset: options.preset,
|
|
540
540
|
providerRoot,
|
|
541
|
-
validationCommands: [
|
|
541
|
+
validationCommands: [
|
|
542
|
+
"bun run check",
|
|
543
|
+
"bun run submit-check",
|
|
544
|
+
"bun run test",
|
|
545
|
+
],
|
|
542
546
|
workspaceRoot: resolvedWorkspaceRoot,
|
|
543
547
|
};
|
|
544
548
|
}
|
|
@@ -568,9 +572,10 @@ function renderPackageJson(input: {
|
|
|
568
572
|
scripts: {
|
|
569
573
|
dev: "apifuse dev .",
|
|
570
574
|
check: "apifuse check .",
|
|
571
|
-
"
|
|
572
|
-
|
|
575
|
+
"submit-check":
|
|
576
|
+
"apifuse submit-check . --markdown submission-report.md",
|
|
573
577
|
test: "apifuse test .",
|
|
578
|
+
record: "apifuse record .",
|
|
574
579
|
"perf:sample": "apifuse perf . --operation ping --runs 3",
|
|
575
580
|
start: "bun start.ts",
|
|
576
581
|
},
|
|
@@ -8,6 +8,48 @@ Generated with `apifuse create`.
|
|
|
8
8
|
bun run dev
|
|
9
9
|
bun run check
|
|
10
10
|
bun run test
|
|
11
|
+
bun run submit-check
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## Pre-submission report
|
|
16
|
+
|
|
17
|
+
Before posting bounty evidence, run:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bun run submit-check
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This writes `submission-report.md` with a review-readiness score, blockers,
|
|
24
|
+
warnings, health coverage notes, fixture/schema evidence, and remediation. A
|
|
25
|
+
score is not a payout guarantee; blockers must be fixed before maintainer
|
|
26
|
+
review. The generated `ping` starter intentionally warns until you replace it
|
|
27
|
+
with real upstream-backed Operations. The full public-only checklist is shipped
|
|
28
|
+
in `node_modules/@apifuse/provider-sdk/SUBMISSION.md`.
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
## Operation guide
|
|
32
|
+
|
|
33
|
+
### Parameters
|
|
34
|
+
|
|
35
|
+
Starter `ping` accepts `{ "value": string }`. Replace this section with each
|
|
36
|
+
real operation's input schema, required fields, formats, limits, and examples
|
|
37
|
+
before submitting bounty evidence.
|
|
38
|
+
|
|
39
|
+
### Response
|
|
40
|
+
|
|
41
|
+
Starter `ping` returns `{ "ok": boolean, "message": string }`. Replace this
|
|
42
|
+
section with the normalized response fields, units, enum values, pagination,
|
|
43
|
+
and upstream caveats for each real operation.
|
|
44
|
+
|
|
45
|
+
### Example
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"requestId": "req_local_ping",
|
|
50
|
+
"input": { "value": "hello" },
|
|
51
|
+
"headers": {}
|
|
52
|
+
}
|
|
11
53
|
```
|
|
12
54
|
|
|
13
55
|
## Provider server contract
|
|
@@ -21,13 +63,55 @@ bun run test
|
|
|
21
63
|
- `POST /auth/poll`
|
|
22
64
|
- `POST /auth/disconnect`
|
|
23
65
|
|
|
66
|
+
## Local smoke
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
curl -s http://localhost:3900/health
|
|
70
|
+
curl -s -X POST http://localhost:3900/v1/ping \
|
|
71
|
+
-H 'Content-Type: application/json' \
|
|
72
|
+
-d '{"requestId":"req_local_ping","input":{"value":"hello"},"headers":{}}'
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The `POST /v1/{operation}` body is a request envelope:
|
|
76
|
+
|
|
77
|
+
- `requestId` is required and can be any unique local debugging string.
|
|
78
|
+
- `input` contains the operation input shape.
|
|
79
|
+
- `headers` is optional.
|
|
80
|
+
- `connection` is optional; omit it for no-auth/public operations. For
|
|
81
|
+
credential debugging, pass `{ "id", "mode", "secrets", "metadata",
|
|
82
|
+
"externalRef" }` with local-only secret values.
|
|
83
|
+
|
|
84
|
+
Structured errors return an `error` object with `code`, `message`,
|
|
85
|
+
`requestId`, and optional `details`; validation failures include field paths in
|
|
86
|
+
`details`, and the `apifuse dev` terminal prints a structured provider log.
|
|
87
|
+
|
|
88
|
+
## Debugging checklist
|
|
89
|
+
|
|
90
|
+
- `invalid_request`: include `requestId` and `input`; omit `connection` for
|
|
91
|
+
public/no-auth operations and never send `connection: null`.
|
|
92
|
+
- Credentials: declare `credential.keys`, pass local-only values through
|
|
93
|
+
`connection.secrets`, and read them with `ctx.credential`.
|
|
94
|
+
- Auth flow: call `/auth/start`, then `/auth/continue` with the same `flowId`;
|
|
95
|
+
carry returned `contextPatch` values into the next request's `context`.
|
|
96
|
+
- TLS/browser runtime: if Bun blocks native dependency lifecycle scripts, run
|
|
97
|
+
`bun pm untrusted` and trust SDK dependencies such as `koffi` before debugging
|
|
98
|
+
`ctx.tls`; for TypeScript browser Providers use
|
|
99
|
+
`browser.engine: "playwright-stealth"` (`nodriver` is Python-runtime only),
|
|
100
|
+
then install local Chromium with `bunx playwright install chromium` or set
|
|
101
|
+
`CDP_POOL_URL`.
|
|
102
|
+
|
|
24
103
|
## Next steps
|
|
25
104
|
|
|
26
105
|
1. Replace the sample `ping` operation with real upstream logic.
|
|
27
|
-
2.
|
|
106
|
+
2. Once the real operation declares `upstream.baseUrl` and uses `ctx.http` or
|
|
107
|
+
`ctx.tls`, record a fixture with:
|
|
108
|
+
`bun run record -- --operation <operation> --params '<json-input>'`.
|
|
28
109
|
3. Replace the starter `healthCheckUnsupported` with a real `healthCheck` for read-only upstream operations when safe.
|
|
29
110
|
4. Extend tests and operation metadata until the provider is bounty-ready.
|
|
30
111
|
|
|
112
|
+
`apifuse record` is not expected to work with the generated local-only `ping`
|
|
113
|
+
operation because it intentionally has no upstream response to capture.
|
|
114
|
+
|
|
31
115
|
## Health-check authorship
|
|
32
116
|
|
|
33
117
|
Every operation must declare exactly one of:
|
package/src/define.ts
CHANGED
|
@@ -185,6 +185,96 @@ function validateOperationIds(
|
|
|
185
185
|
);
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
|
+
const OPERATION_CONTRACT_VERSION_REGEX =
|
|
189
|
+
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
190
|
+
const VALID_OPERATION_LIFECYCLES = [
|
|
191
|
+
"stable",
|
|
192
|
+
"beta",
|
|
193
|
+
"deprecated",
|
|
194
|
+
"removed",
|
|
195
|
+
] as const;
|
|
196
|
+
|
|
197
|
+
function assertNonEmptyString(
|
|
198
|
+
value: unknown,
|
|
199
|
+
field: string,
|
|
200
|
+
providerId: string,
|
|
201
|
+
operationName: string,
|
|
202
|
+
): asserts value is string {
|
|
203
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
204
|
+
throw new ValidationError(
|
|
205
|
+
`Provider "${providerId}" operation "${operationName}" has invalid ${field}: must be a non-empty string.`,
|
|
206
|
+
{ fix: `Set ${field} to a non-empty customer-facing value.` },
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function validateOperationContracts(
|
|
212
|
+
providerId: string,
|
|
213
|
+
operations: Record<string, ProviderOperation>,
|
|
214
|
+
): void {
|
|
215
|
+
for (const [operationName, operation] of Object.entries(operations)) {
|
|
216
|
+
const contract = operation.contract;
|
|
217
|
+
if (contract === undefined) continue;
|
|
218
|
+
if (!contract || typeof contract !== "object") {
|
|
219
|
+
throw new ValidationError(
|
|
220
|
+
`Provider "${providerId}" operation "${operationName}" has invalid operations.${operationName}.contract: must be an object.`,
|
|
221
|
+
{
|
|
222
|
+
fix: `Remove operations.${operationName}.contract or provide { version, lifecycle, deprecation }.`,
|
|
223
|
+
},
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
if (
|
|
227
|
+
contract.version !== undefined &&
|
|
228
|
+
(typeof contract.version !== "string" ||
|
|
229
|
+
!OPERATION_CONTRACT_VERSION_REGEX.test(contract.version))
|
|
230
|
+
) {
|
|
231
|
+
throw new ValidationError(
|
|
232
|
+
`Provider "${providerId}" operation "${operationName}" has invalid operations.${operationName}.contract.version: expected semver major.minor.patch.`,
|
|
233
|
+
{ fix: `Use an operation contract version such as "1.0.0".` },
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
if (contract.lifecycle !== undefined) {
|
|
237
|
+
assertLiteralField(
|
|
238
|
+
contract.lifecycle,
|
|
239
|
+
`operations.${operationName}.contract.lifecycle`,
|
|
240
|
+
VALID_OPERATION_LIFECYCLES,
|
|
241
|
+
providerId,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
if (
|
|
245
|
+
contract.lifecycle === "deprecated" ||
|
|
246
|
+
contract.lifecycle === "removed"
|
|
247
|
+
) {
|
|
248
|
+
if (!contract.deprecation || typeof contract.deprecation !== "object") {
|
|
249
|
+
throw new ValidationError(
|
|
250
|
+
`Provider "${providerId}" operation "${operationName}" is ${contract.lifecycle} but lacks operations.${operationName}.contract.deprecation metadata.`,
|
|
251
|
+
{
|
|
252
|
+
fix: `Add announcedAt, removalAfter, and migrationGuide to operations.${operationName}.contract.deprecation.`,
|
|
253
|
+
},
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
assertNonEmptyString(
|
|
257
|
+
contract.deprecation.announcedAt,
|
|
258
|
+
`operations.${operationName}.contract.deprecation.announcedAt`,
|
|
259
|
+
providerId,
|
|
260
|
+
operationName,
|
|
261
|
+
);
|
|
262
|
+
assertNonEmptyString(
|
|
263
|
+
contract.deprecation.removalAfter,
|
|
264
|
+
`operations.${operationName}.contract.deprecation.removalAfter`,
|
|
265
|
+
providerId,
|
|
266
|
+
operationName,
|
|
267
|
+
);
|
|
268
|
+
assertNonEmptyString(
|
|
269
|
+
contract.deprecation.migrationGuide,
|
|
270
|
+
`operations.${operationName}.contract.deprecation.migrationGuide`,
|
|
271
|
+
providerId,
|
|
272
|
+
operationName,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
188
278
|
function validateOperationAnnotations(
|
|
189
279
|
providerId: string,
|
|
190
280
|
operations: Record<string, ProviderOperation>,
|
|
@@ -622,6 +712,7 @@ export function defineProvider<
|
|
|
622
712
|
);
|
|
623
713
|
validateOperationIds(config.id, config.operations);
|
|
624
714
|
validateOperationAnnotations(config.id, config.operations);
|
|
715
|
+
validateOperationContracts(config.id, config.operations);
|
|
625
716
|
validateOperationHealthChecks(config.id, config.operations);
|
|
626
717
|
validateProviderHealthMonitor(config.id, config.healthMonitor);
|
|
627
718
|
validateOperationFixtures(config.id, config.operations);
|
|
@@ -629,7 +720,7 @@ export function defineProvider<
|
|
|
629
720
|
throw new ProviderError(
|
|
630
721
|
`Provider "${config.id}" must define browser.engine when runtime is "browser"`,
|
|
631
722
|
{
|
|
632
|
-
fix: 'Add browser: { engine: "
|
|
723
|
+
fix: 'Add browser: { engine: "playwright-stealth" } for TypeScript providers, or another supported engine for your runtime',
|
|
633
724
|
},
|
|
634
725
|
);
|
|
635
726
|
if (config.browser && config.runtime !== "browser")
|
package/src/index.ts
CHANGED
|
@@ -75,10 +75,13 @@ export type {
|
|
|
75
75
|
HttpResponse,
|
|
76
76
|
InferSchemaOutput,
|
|
77
77
|
OperationAnnotations,
|
|
78
|
+
OperationContractMetadata,
|
|
78
79
|
OperationDefinition,
|
|
80
|
+
OperationDeprecationMetadata,
|
|
79
81
|
OperationDocMeta,
|
|
80
82
|
OperationErrorCode,
|
|
81
83
|
OperationInputExample,
|
|
84
|
+
OperationLifecycle,
|
|
82
85
|
OperationRelationships,
|
|
83
86
|
ProbeInterval,
|
|
84
87
|
ProviderContext,
|
package/src/server/serve.ts
CHANGED
|
@@ -259,7 +259,7 @@ function publicProviderErrorMessage(error: ProviderError): string {
|
|
|
259
259
|
return error.message;
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
function toStatusCode(error: unknown): 400 | 404 | 500 | 502 | 504 {
|
|
262
|
+
function toStatusCode(error: unknown): 400 | 404 | 429 | 500 | 502 | 504 {
|
|
263
263
|
if (error instanceof z.ZodError) {
|
|
264
264
|
return 400;
|
|
265
265
|
}
|
|
@@ -269,8 +269,17 @@ function toStatusCode(error: unknown): 400 | 404 | 500 | 502 | 504 {
|
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
if (error instanceof ProviderError) {
|
|
272
|
-
|
|
273
|
-
|
|
272
|
+
switch (error.code) {
|
|
273
|
+
case "NOT_FOUND":
|
|
274
|
+
case "not_found":
|
|
275
|
+
case "NO_DATA":
|
|
276
|
+
return 404;
|
|
277
|
+
case "RATE_LIMITED":
|
|
278
|
+
case "UPSTREAM_RATE_LIMIT":
|
|
279
|
+
case "LIMITED_NUMBER_OF_SERVICE_REQUESTS_EXCEEDS_ERROR":
|
|
280
|
+
return 429;
|
|
281
|
+
case "UPSTREAM_ERROR":
|
|
282
|
+
return 502;
|
|
274
283
|
}
|
|
275
284
|
|
|
276
285
|
return 400;
|
package/src/types.ts
CHANGED
|
@@ -537,6 +537,25 @@ export interface ContextDeclaration {
|
|
|
537
537
|
keys: string[];
|
|
538
538
|
}
|
|
539
539
|
|
|
540
|
+
export type OperationLifecycle = "stable" | "beta" | "deprecated" | "removed";
|
|
541
|
+
|
|
542
|
+
export interface OperationDeprecationMetadata {
|
|
543
|
+
announcedAt: string;
|
|
544
|
+
removalAfter: string;
|
|
545
|
+
replacement?: string;
|
|
546
|
+
migrationGuide: string;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export interface OperationContractMetadata {
|
|
550
|
+
/**
|
|
551
|
+
* Callable operation contract version. Defaults to 1.0.0 for the clean
|
|
552
|
+
* pre-GA baseline; it intentionally does not fall back to provider.version.
|
|
553
|
+
*/
|
|
554
|
+
version?: string;
|
|
555
|
+
lifecycle?: OperationLifecycle;
|
|
556
|
+
deprecation?: OperationDeprecationMetadata;
|
|
557
|
+
}
|
|
558
|
+
|
|
540
559
|
export interface OperationDefinition<
|
|
541
560
|
TInput extends SchemaLike = SchemaLike,
|
|
542
561
|
TOutput extends SchemaLike = SchemaLike,
|
|
@@ -548,6 +567,7 @@ export interface OperationDefinition<
|
|
|
548
567
|
derivations?: Record<string, string>;
|
|
549
568
|
inputExamples?: OperationInputExample[];
|
|
550
569
|
annotations?: OperationAnnotations;
|
|
570
|
+
contract?: OperationContractMetadata;
|
|
551
571
|
tags?: string[];
|
|
552
572
|
relatedOperations?: OperationRelationships;
|
|
553
573
|
input: TInput;
|