@apifuse/provider-sdk 2.1.0-beta.0 → 2.1.0-beta.2

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  - Canonical scaffolding command: `apifuse create`
4
4
  - Monorepo contributors should use `apifuse create <name> --preset monorepo`
5
- - Standalone users should use `bunx @apifuse/provider-sdk create <name>`
5
+ - Standalone bounty contributors should use `bunx @apifuse/provider-sdk@beta create <name> --yes` until this release is promoted to `latest`
6
6
  - Provider server contract is:
7
7
  - dev default `3900`
8
8
  - start/Docker/container `3000`
@@ -47,6 +47,7 @@ description:
47
47
  - `description` — 150+ chars English (error-level rule)
48
48
  - Every Zod field in input AND output has `.describe()` including nested objects + array items (error-level rule)
49
49
  - `fixtures.request` + `fixtures.response` both present (error-level rule)
50
+ - Exactly one of `healthCheck` or `healthCheckUnsupported` per operation. Prefer `healthCheck` for safe read-only upstream probes; use `healthCheckUnsupported` only with a specific reason for destructive, paid, credential-sensitive, flaky, or otherwise unsafe probes.
50
51
 
51
52
  ### Factored operations
52
53
 
@@ -62,31 +63,39 @@ Use `defineOperation()` when an operation is large enough to live beside helper
62
63
 
63
64
  - `annotations`: `{ readOnly, destructive, idempotent, openWorld, rateLimit }` — agentic safety signals
64
65
  - `tags`: operation-level semantic tags for retrieval (e.g., `["weather", "korea", "realtime"]`)
65
- - `relatedOperations`: `{ alternatives?: string[]; chainsWith?: string[] }` — links to related operations for composite/fallback suggestions
66
-
67
- ### Composite operations
68
-
69
- For multi-step chains (e.g., geocode transform forecast), use `defineCompositeOperation()` from `@apifuse/provider-sdk`:
70
-
71
- ```ts
72
- import { defineCompositeOperation, z } from "@apifuse/provider-sdk"
73
-
74
- export const weatherByAddress = defineCompositeOperation({
75
- id: "kma:weather-by-address",
76
- description: "...",
77
- input: z.object({ address: z.string().describe("...") }),
78
- output: ResultSchema,
79
- tags: ["weather", "korea", "composite"],
80
- chainsWith: ["kakaomap:geocode", "kma:short-forecast"],
81
- steps: async (ctx, input) => {
82
- const geo = await ctx.chain.call("kakaomap:geocode", { address: input.address })
83
- if (geo.items.length === 0) {
84
- return ctx.clarify({ question: "...", missing: [{ name: "address", description: "..." }] })
85
- }
86
- // ... continue chain
87
- },
88
- })
89
- ```
66
+ - `relatedOperations`: `{ alternatives?: string[] }` — links to fallback/sibling operations
67
+
68
+ ### External bounty submission evidence
69
+
70
+ External contributors are expected to submit standalone Provider source plus:
71
+
72
+ - SDK version/tag and create command used.
73
+ - Provider id, version, runtime, auth mode, and Operation list.
74
+ - Health coverage table for every Operation.
75
+ - `bun run check` output.
76
+ - `bun run test` output.
77
+ - Fixture evidence and known upstream constraints.
78
+
79
+ Maintainers own monorepo import under `providers/<id>/`, registry generation,
80
+ deployment projection checks, and release workflows.
81
+
82
+ ### Public local debugging checklist
83
+
84
+ - Operation smoke requests use the provider server envelope:
85
+ `{"requestId":"req_local_<operation>","input":{...},"headers":{}}`.
86
+ Omit `connection` for public/no-auth operations; do not send `connection: null`.
87
+ - Credential-backed smoke requests pass local-only credential material in
88
+ `connection.secrets`. Keep real values in shell env or `.env`, never in source
89
+ or fixtures.
90
+ - Auth-flow debugging starts with `/auth/start`, continues with
91
+ `/auth/continue`, and carries returned `contextPatch` values into the next
92
+ request's `context`.
93
+ - TLS/browser providers may require local runtime setup outside Provider code:
94
+ use `bun pm untrusted`/`bun pm trust koffi` for blocked native TLS dependency
95
+ scripts, `browser.engine: "playwright-stealth"` for TypeScript browser
96
+ Providers (`nodriver` is Python-runtime only), `bunx playwright install
97
+ chromium` for local Playwright browser assets, or
98
+ `CDP_POOL_URL`/`APIFUSE_CDP_POOL_URL` for remote browser debugging.
90
99
 
91
100
  ### Running the lint locally
92
101
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @apifuse/provider-sdk Changelog
2
2
 
3
+ ## 2.1.0-beta.2
4
+
5
+ - Harden public bounty contributor DX with server-contract accurate README and generated Provider smoke examples.
6
+ - Add packed-artifact regression checks so stale `connection: null` or missing `requestId` examples cannot ship again.
7
+ - Extend clean-room packed SDK smoke coverage to boot the generated dev server and call `/health` plus `POST /v1/ping`.
8
+ - Document credential, auth-flow, TLS, browser, and Bun trusted-dependency troubleshooting for SDK-only local development.
9
+
10
+ ## 2.1.0-beta.1
11
+
12
+ - Fix public `apifuse create` runtime packaging by publishing `@clack/prompts` as a production dependency.
13
+ - Update generated Provider starter templates so the sample operation declares a local-only `healthCheckUnsupported` and passes the current health coverage contract.
14
+ - Add packed-artifact smoke coverage for the public create/check/test flow before npm release publishing.
15
+ - Document the public SDK-only bounty contributor path and maintainer-owned monorepo import boundary.
16
+
3
17
  ## 2.1.0-beta.0
4
18
 
5
19
  - BREAKING: collapse the Chrome desktop stealth catalog to `chrome-146` plus the `chrome-desktop` alias. Removed/blocked `chrome-120`, `chrome-124`, `chrome-129`, `chrome-130`, `chrome-131`, `chrome-133`, `chrome-144`, `chrome-146-psk`, `chrome-131-psk`, `chrome-130-psk`, and `edge-131`; migrate callers to `chrome-146`.
package/README.md CHANGED
@@ -8,12 +8,19 @@ ApiFuse Provider SDK — build provider declarations and runtimes with one publi
8
8
  bun add @apifuse/provider-sdk
9
9
  ```
10
10
 
11
+ For external bounty scaffolding, use the beta tag until this release is promoted
12
+ to `latest`:
13
+
14
+ ```bash
15
+ bunx @apifuse/provider-sdk@beta create my-provider --yes
16
+ ```
17
+
11
18
  ## Create a provider
12
19
 
13
20
  ### Standalone (default)
14
21
 
15
22
  ```bash
16
- bunx @apifuse/provider-sdk create my-provider
23
+ bunx @apifuse/provider-sdk@beta create my-provider --yes
17
24
  ```
18
25
 
19
26
  The canonical `create` flow:
@@ -49,11 +56,84 @@ Removed legacy runtime paths are not supported:
49
56
 
50
57
  ```bash
51
58
  cd my-provider
52
- bun run dev
53
59
  bun run check
54
60
  bun run test
61
+ bun run dev
62
+ ```
63
+
64
+ Smoke the generated local server:
65
+
66
+ ```bash
67
+ curl -s http://localhost:3900/health
68
+ curl -s -X POST http://localhost:3900/v1/ping \
69
+ -H 'Content-Type: application/json' \
70
+ -d '{"requestId":"req_local_ping","input":{"value":"hello"},"headers":{}}'
55
71
  ```
56
72
 
73
+ The operation request body is the same envelope used by the ApiFuse gateway:
74
+
75
+ | Field | Required | Notes |
76
+ |---|---:|---|
77
+ | `requestId` | yes | Any unique string is fine for local debugging; it is echoed in structured errors. |
78
+ | `input` | yes | The operation input after schema validation. |
79
+ | `headers` | no | Extra caller headers to expose through `ctx.request.headers`. |
80
+ | `connection` | no | Omit for no-auth/public operations. For credential debugging, pass an object with `id`, `mode`, `secrets`, `metadata`, and `externalRef`. Do not pass `null`. |
81
+
82
+ Credential-bearing local smoke example:
83
+
84
+ ```bash
85
+ curl -s -X POST http://localhost:3900/v1/me \
86
+ -H 'Content-Type: application/json' \
87
+ -d '{
88
+ "requestId":"req_local_me",
89
+ "input":{},
90
+ "connection":{
91
+ "id":"conn_local_debug",
92
+ "mode":"credentials",
93
+ "secrets":{"apiKey":"dev-only-secret"},
94
+ "scopes":[],
95
+ "metadata":{},
96
+ "externalRef":"local-debug"
97
+ }
98
+ }'
99
+ ```
100
+
101
+ Auth-flow endpoints use the same `requestId` convention:
102
+
103
+ ```bash
104
+ curl -s -X POST http://localhost:3900/auth/start \
105
+ -H 'Content-Type: application/json' \
106
+ -d '{"requestId":"req_auth_start","flowId":"flow_local_1","providerId":"my-provider","context":{}}'
107
+ ```
108
+
109
+ If a local smoke returns `{"error":...}`, inspect the JSON error body and the
110
+ `apifuse dev` server log. Validation failures include a `details` array with
111
+ the bad request path; provider/runtime failures include `code`, `message`, and
112
+ `requestId`.
113
+
114
+ ### Local debugging checklist
115
+
116
+ - **`invalid_request` on `/v1/{operation}`**: confirm the request body includes
117
+ `requestId` and `input`. Omit `connection` for public/no-auth operations;
118
+ never send `connection: null`.
119
+ - **Credential-backed operations**: declare `credential.keys`, then pass matching
120
+ local-only values through `connection.secrets`. Read them in handlers with
121
+ `ctx.credential.get("key")` or `ctx.credential.getAccessToken()`.
122
+ - **Provider env secrets**: declare `secrets[]`, set values in your shell or
123
+ `.env`, and read only those names through `ctx.env.get("NAME")`.
124
+ - **Auth flows**: call `/auth/start`, then `/auth/continue` with the same
125
+ `flowId`; preserve any returned `contextPatch` in the next local request's
126
+ `context` object.
127
+ - **TLS-sensitive providers**: if Bun reports blocked lifecycle scripts after
128
+ install, run `bun pm untrusted`; when it lists trusted SDK native dependencies
129
+ such as `koffi`, run `bun pm trust koffi` (or `bun pm trust`) before debugging
130
+ `ctx.tls` failures.
131
+ - **Browser providers**: for TypeScript Providers use `runtime: "browser"` plus
132
+ `browser.engine: "playwright-stealth"`; `nodriver` is a Python-runtime path.
133
+ Install local browser assets with `bunx playwright install chromium` when
134
+ using the Playwright runtime, or set `CDP_POOL_URL`/`APIFUSE_CDP_POOL_URL`
135
+ when debugging against a remote browser pool.
136
+
57
137
  ## Authoring ergonomics
58
138
 
59
139
  `defineProvider()` infers each operation handler input from the operation `input` schema. For larger providers, factor operations with `defineOperation()` and compose them later:
@@ -80,6 +160,18 @@ export default defineProvider({
80
160
 
81
161
  Operation schemas may be Zod schemas or Standard Schema v1-compatible schemas. Invalid configs throw `ProviderError`/`ValidationError` messages that name the offending field, such as `auth.mode` or `operations.search.fixtures.request`.
82
162
 
163
+ ### Operation health coverage
164
+
165
+ Every operation must declare exactly one of:
166
+
167
+ - `healthCheck` — preferred for safe read-only upstream probes.
168
+ - `healthCheckUnsupported` — allowed only when a probe is destructive, paid,
169
+ credential-sensitive, flaky by design, or otherwise unsafe. The `reason` must
170
+ be specific.
171
+
172
+ The generated `ping` operation uses `healthCheckUnsupported` only because it is
173
+ a local scaffold check, not a real upstream API probe.
174
+
83
175
  ### Operation annotations
84
176
 
85
177
  Operations declare non-functional metadata via `annotations`:
@@ -106,6 +198,11 @@ apifuse test [path]
106
198
  apifuse perf <path> --operation <operation>
107
199
  ```
108
200
 
201
+ `apifuse record` is for real upstream-backed operations that declare
202
+ `upstream.baseUrl` and call the upstream through `ctx.http` or `ctx.tls`. The
203
+ generated local-only `ping` operation intentionally has no upstream and should
204
+ be replaced before recording fixtures.
205
+
109
206
  ## Scope boundary
110
207
 
111
208
  Generator v1 scaffolds **TypeScript providers only** for this redesign. Python generation remains future work.
@@ -114,3 +211,8 @@ Generator v1 scaffolds **TypeScript providers only** for this redesign. Python g
114
211
  ## Boundary
115
212
 
116
213
  Provider cataloging, deployment enrollment, docs indexing, and runtime discovery are internal platform-registry responsibilities and are not part of the public `@apifuse/provider-sdk` contract.
214
+
215
+ External bounty contributors should submit standalone Provider source plus
216
+ `bun run check` / `bun run test` evidence. ApiFuse maintainers own monorepo
217
+ import, registry generation, deployment projection checks, and release
218
+ publishing.
@@ -7,13 +7,14 @@ import { pathToFileURL } from "node:url";
7
7
  import { z } from "zod";
8
8
 
9
9
  import type { ProviderDefinition } from "../src";
10
+ import { lintProvider } from "../src/lint";
10
11
  import { safeParseSchemaSync } from "../src/schema";
11
12
 
12
13
  const HELP_TEXT = `Usage: apifuse check [path]
13
14
  Example: apifuse check providers/airkorea
14
15
  Default: apifuse check .`;
15
16
 
16
- type CheckResult = {
17
+ export type CheckResult = {
17
18
  message: string;
18
19
  passed: boolean;
19
20
  details?: string[];
@@ -103,7 +104,7 @@ function resolveFromParents(inputPath: string): string {
103
104
  }
104
105
  }
105
106
 
106
- async function runChecks(providerRoot: string): Promise<CheckResult[]> {
107
+ export async function runChecks(providerRoot: string): Promise<CheckResult[]> {
107
108
  const indexPath = resolve(providerRoot, "index.ts");
108
109
  const dockerfilePath = resolve(providerRoot, "Dockerfile");
109
110
  const packageJsonPath = resolve(providerRoot, "package.json");
@@ -118,6 +119,7 @@ async function runChecks(providerRoot: string): Promise<CheckResult[]> {
118
119
  checkOperations(provider),
119
120
  checkFixtures(provider),
120
121
  checkSchemas(provider),
122
+ checkAuthoringLint(provider),
121
123
  checkProviderMetadata(provider),
122
124
  checkDockerfile(dockerfilePath),
123
125
  checkPackageJson(packageJsonPath),
@@ -254,6 +256,32 @@ function checkSchemas(provider: ProviderDefinition | undefined): CheckResult {
254
256
  };
255
257
  }
256
258
 
259
+ function checkAuthoringLint(
260
+ provider: ProviderDefinition | undefined,
261
+ ): CheckResult {
262
+ if (!provider) {
263
+ return {
264
+ message: "Provider authoring lint has no error-level diagnostics",
265
+ passed: false,
266
+ };
267
+ }
268
+
269
+ const diagnostics = lintProvider(provider);
270
+ const errors = diagnostics.filter(
271
+ (diagnostic) => diagnostic.level === "error",
272
+ );
273
+ const details = diagnostics.map((diagnostic) => {
274
+ const field = diagnostic.field ? `${diagnostic.field}: ` : "";
275
+ return `${diagnostic.level.toUpperCase()} ${diagnostic.rule} ${field}${diagnostic.message}`;
276
+ });
277
+
278
+ return {
279
+ message: "Provider authoring lint has no error-level diagnostics",
280
+ passed: errors.length === 0,
281
+ details,
282
+ };
283
+ }
284
+
257
285
  function checkProviderMetadata(
258
286
  provider: ProviderDefinition | undefined,
259
287
  ): CheckResult {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  import { existsSync } from "node:fs";
4
- import { dirname, resolve } from "node:path";
4
+ import { dirname, relative, resolve } from "node:path";
5
5
 
6
6
  import type { ProviderDefinition } from "../src";
7
7
  import {
@@ -51,10 +51,24 @@ export async function main() {
51
51
  console.log(` POST http://localhost:${port}/auth/poll`);
52
52
  console.log(` POST http://localhost:${port}/auth/disconnect`);
53
53
 
54
+ const firstOperation = Object.keys(provider.operations)[0];
55
+ if (firstOperation) {
56
+ const sampleInput =
57
+ provider.operations[firstOperation]?.fixtures?.request ?? {};
58
+ const sampleBody = JSON.stringify({
59
+ requestId: `req_local_${firstOperation}`,
60
+ input: sampleInput,
61
+ headers: {},
62
+ });
63
+ console.log("\nSmoke:");
64
+ console.log(` curl -s http://localhost:${port}/health`);
65
+ console.log(
66
+ ` curl -s -X POST http://localhost:${port}/v1/${firstOperation} -H 'Content-Type: application/json' -d ${shellSingleQuote(sampleBody)}`,
67
+ );
68
+ }
69
+
54
70
  console.log("\nHot reload:");
55
- console.log(
56
- ` bun --hot ${resolveImportPath("apifuse-dev.ts")} ${args[0] ?? "."}`,
57
- );
71
+ console.log(` ${renderHotReloadCommand(providerPath, port)}`);
58
72
  }
59
73
 
60
74
  export function createProviderContext(provider: ProviderDefinition): {
@@ -111,8 +125,18 @@ function resolveFromParents(inputPath: string): string {
111
125
  }
112
126
  }
113
127
 
114
- function resolveImportPath(fileName: string): string {
115
- return resolve(process.cwd(), "bin", fileName);
128
+ function renderHotReloadCommand(providerPath: string, port: number): string {
129
+ const devEntry = resolve(providerPath, "dev.ts");
130
+ if (existsSync(devEntry)) {
131
+ const relativeDevEntry = relative(process.cwd(), devEntry) || "dev.ts";
132
+ const portPrefix = process.env.PORT ? `PORT=${port} ` : "";
133
+ return `${portPrefix}bun --hot ${relativeDevEntry}`;
134
+ }
135
+ return "rerun `apifuse dev` after edits (no dev.ts entrypoint found)";
136
+ }
137
+
138
+ function shellSingleQuote(value: string): string {
139
+ return `'${value.replaceAll("'", "'\\''")}'`;
116
140
  }
117
141
 
118
142
  function createUnsupportedBrowserStub(): BrowserClient {
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  import { execFileSync } from "node:child_process";
4
+ import { readFileSync } from "node:fs";
4
5
  import { z } from "zod";
5
6
 
6
7
  const PACK_RESULT_SCHEMA = z.array(
@@ -28,12 +29,23 @@ if (!first) {
28
29
  }
29
30
 
30
31
  const filePaths = (first.files ?? []).map((file) => file.path);
32
+ const requiredPaths = [
33
+ "bin/apifuse.ts",
34
+ "bin/apifuse-create.ts",
35
+ "bin/apifuse-pack-smoke.ts",
36
+ "src/cli/create.ts",
37
+ "src/cli/templates/provider/index.ts.tpl",
38
+ "src/cli/templates/provider/README.md.tpl",
39
+ ];
31
40
  const forbiddenMatches = filePaths.filter(
32
41
  (path) =>
33
42
  path.startsWith("src/__tests__/") ||
34
43
  path === "src/index.test.ts" ||
35
44
  path === "bin/apifuse-init.ts",
36
45
  );
46
+ const missingRequiredPaths = requiredPaths.filter(
47
+ (path) => !filePaths.includes(path),
48
+ );
37
49
 
38
50
  if (forbiddenMatches.length > 0) {
39
51
  throw new Error(
@@ -41,7 +53,85 @@ if (forbiddenMatches.length > 0) {
41
53
  );
42
54
  }
43
55
 
56
+ if (missingRequiredPaths.length > 0) {
57
+ throw new Error(
58
+ `Packed artifact is missing required public SDK files:\n${missingRequiredPaths.join("\n")}`,
59
+ );
60
+ }
61
+
62
+ const packageJsonInput: unknown = JSON.parse(
63
+ readFileSync("package.json", "utf8"),
64
+ );
65
+ const packageJson = z
66
+ .object({
67
+ dependencies: z.record(z.string(), z.string()).optional(),
68
+ devDependencies: z.record(z.string(), z.string()).optional(),
69
+ })
70
+ .parse(packageJsonInput);
71
+
72
+ if (!packageJson.dependencies?.["@clack/prompts"]) {
73
+ throw new Error(
74
+ "@clack/prompts is imported by the public create CLI and must be listed in dependencies.",
75
+ );
76
+ }
77
+
78
+ if (packageJson.devDependencies?.["@clack/prompts"]) {
79
+ throw new Error(
80
+ "@clack/prompts must not be devDependency-only because the published create CLI imports it at runtime.",
81
+ );
82
+ }
83
+
84
+ assertPublicSmokeDocs("README.md", readFileSync("README.md", "utf8"));
85
+ assertPublicSmokeDocs(
86
+ "src/cli/templates/provider/README.md.tpl",
87
+ readFileSync("src/cli/templates/provider/README.md.tpl", "utf8"),
88
+ );
89
+
44
90
  console.log(`Packed artifact OK: ${first.filename}`);
45
91
  for (const filePath of filePaths) {
46
92
  console.log(` - ${filePath}`);
47
93
  }
94
+
95
+ function assertPublicSmokeDocs(label: string, content: string): void {
96
+ if (!content.includes('"requestId":"req_local_ping"')) {
97
+ throw new Error(
98
+ `${label} must document the current provider server request envelope with requestId.`,
99
+ );
100
+ }
101
+
102
+ if (content.includes('"connection":null')) {
103
+ throw new Error(
104
+ `${label} must not tell public users to send connection:null; omit connection for no-auth operations.`,
105
+ );
106
+ }
107
+
108
+ if (!content.includes("bunx playwright install chromium")) {
109
+ throw new Error(
110
+ `${label} must include browser runtime troubleshooting for public SDK-only debugging.`,
111
+ );
112
+ }
113
+
114
+ if (!content.includes("bun pm untrusted")) {
115
+ throw new Error(
116
+ `${label} must include Bun trusted-dependency troubleshooting for TLS/browser bounties.`,
117
+ );
118
+ }
119
+
120
+ if (
121
+ !content.includes('browser.engine: "playwright-stealth"') ||
122
+ !content.includes("nodriver")
123
+ ) {
124
+ throw new Error(
125
+ `${label} must clarify that TypeScript browser providers use playwright-stealth and nodriver is not the TypeScript happy path.`,
126
+ );
127
+ }
128
+
129
+ if (
130
+ label.includes("templates/provider/README.md.tpl") &&
131
+ !content.includes("bun run record -- --operation <operation>")
132
+ ) {
133
+ throw new Error(
134
+ `${label} must document fixture recording through a generated package script, not a shell-global apifuse command.`,
135
+ );
136
+ }
137
+ }