@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apifuse/provider-sdk",
3
- "version": "2.1.0-beta.1",
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.12",
69
+ "@biomejs/biome": "^2.4.15",
69
70
  "@types/bun": "latest",
70
- "@types/node": "^25.1.0",
71
+ "@types/node": "^25.8.0",
71
72
  "typescript": "^6.0.3"
72
73
  },
73
74
  "dependencies": {
74
- "@clack/prompts": "^1.2.0",
75
+ "@clack/prompts": "^1.4.0",
75
76
  "ajv": "^8.17",
76
- "hono": "^4.12.14",
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.6"
83
+ "zod": "^4.4.3"
83
84
  }
84
85
  }
@@ -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: "nodriver",\n }'
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: ["bun run check", "bun run test"],
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
- "record:sample":
572
- 'apifuse record . --operation ping --params \'{"value":"hello"}\'',
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. Record a real fixture with `bun run record:sample` or a provider-specific equivalent.
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:
@@ -4,7 +4,6 @@ const InputSchema = z
4
4
  .object({
5
5
  value: z
6
6
  .string()
7
- .default("hello")
8
7
  .describe("Sample input value used to verify the generated provider scaffold is wired correctly."),
9
8
  })
10
9
  .describe("Input payload for the generated ping operation.");
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: "nodriver" } or another supported 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,
@@ -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
- if (error.code === "NOT_FOUND" || error.code === "NO_DATA") {
273
- return 404;
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;