@apifuse/provider-sdk 2.1.0-beta.1 → 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
@@ -79,6 +79,24 @@ External contributors are expected to submit standalone Provider source plus:
79
79
  Maintainers own monorepo import under `providers/<id>/`, registry generation,
80
80
  deployment projection checks, and release workflows.
81
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.
99
+
82
100
  ### Running the lint locally
83
101
 
84
102
  ```bash
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
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
+
3
10
  ## 2.1.0-beta.1
4
11
 
5
12
  - Fix public `apifuse create` runtime packaging by publishing `@clack/prompts` as a production dependency.
package/README.md CHANGED
@@ -67,9 +67,73 @@ Smoke the generated local server:
67
67
  curl -s http://localhost:3900/health
68
68
  curl -s -X POST http://localhost:3900/v1/ping \
69
69
  -H 'Content-Type: application/json' \
70
- -d '{"input":{"value":"hello"},"headers":{},"connection":null}'
70
+ -d '{"requestId":"req_local_ping","input":{"value":"hello"},"headers":{}}'
71
71
  ```
72
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
+
73
137
  ## Authoring ergonomics
74
138
 
75
139
  `defineProvider()` infers each operation handler input from the operation `input` schema. For larger providers, factor operations with `defineOperation()` and compose them later:
@@ -134,6 +198,11 @@ apifuse test [path]
134
198
  apifuse perf <path> --operation <operation>
135
199
  ```
136
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
+
137
206
  ## Scope boundary
138
207
 
139
208
  Generator v1 scaffolds **TypeScript providers only** for this redesign. Python generation remains future work.
@@ -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 {
@@ -81,7 +81,57 @@ if (packageJson.devDependencies?.["@clack/prompts"]) {
81
81
  );
82
82
  }
83
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
+
84
90
  console.log(`Packed artifact OK: ${first.filename}`);
85
91
  for (const filePath of filePaths) {
86
92
  console.log(` - ${filePath}`);
87
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
+ }
@@ -1,13 +1,20 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- import { execFileSync, spawnSync } from "node:child_process";
3
+ import {
4
+ type ChildProcess,
5
+ execFileSync,
6
+ spawn,
7
+ spawnSync,
8
+ } from "node:child_process";
4
9
  import {
5
10
  existsSync,
6
11
  mkdirSync,
7
12
  mkdtempSync,
13
+ readFileSync,
8
14
  rmSync,
9
15
  writeFileSync,
10
16
  } from "node:fs";
17
+ import { createServer } from "node:net";
11
18
  import { tmpdir } from "node:os";
12
19
  import { join, resolve } from "node:path";
13
20
  import { z } from "zod";
@@ -17,6 +24,20 @@ const PACK_RESULT_SCHEMA = z.array(
17
24
  filename: z.string(),
18
25
  }),
19
26
  );
27
+ const HEALTH_RESPONSE_SCHEMA = z.object({
28
+ status: z.string(),
29
+ provider: z.string(),
30
+ version: z.string().optional(),
31
+ });
32
+ const PING_RESPONSE_SCHEMA = z.object({
33
+ data: z
34
+ .object({
35
+ ok: z.boolean(),
36
+ message: z.string(),
37
+ })
38
+ .optional(),
39
+ error: z.unknown().optional(),
40
+ });
20
41
 
21
42
  const KEEP_TEMP = process.env.APIFUSE_PACK_SMOKE_KEEP_TEMP === "1";
22
43
 
@@ -73,6 +94,8 @@ try {
73
94
  const generatedProviderDir = join(consumerDir, "dx-smoke");
74
95
  run("bun", ["run", "check"], generatedProviderDir);
75
96
  run("bun", ["run", "test"], generatedProviderDir);
97
+ assertGeneratedReadme(generatedProviderDir);
98
+ await smokeGeneratedDevServer(generatedProviderDir);
76
99
 
77
100
  console.log(
78
101
  `Provider SDK packed-artifact smoke passed: ${tarballPath} -> ${generatedProviderDir}`,
@@ -120,3 +143,154 @@ function run(command: string, args: string[], cwd: string): void {
120
143
  );
121
144
  }
122
145
  }
146
+
147
+ function assertGeneratedReadme(providerDir: string): void {
148
+ const readme = readFileSync(join(providerDir, "README.md"), "utf8");
149
+ if (!readme.includes('"requestId":"req_local_ping"')) {
150
+ throw new Error(
151
+ "Generated README is missing requestId in local smoke docs.",
152
+ );
153
+ }
154
+ if (readme.includes('"connection":null')) {
155
+ throw new Error(
156
+ "Generated README must not document connection:null for no-auth local smoke.",
157
+ );
158
+ }
159
+ if (!readme.includes("bunx playwright install chromium")) {
160
+ throw new Error(
161
+ "Generated README is missing browser runtime troubleshooting guidance.",
162
+ );
163
+ }
164
+ if (!readme.includes("bun pm untrusted")) {
165
+ throw new Error(
166
+ "Generated README is missing Bun trusted-dependency troubleshooting guidance.",
167
+ );
168
+ }
169
+ if (!readme.includes("bun run record -- --operation <operation>")) {
170
+ throw new Error(
171
+ "Generated README must document fixture recording through the generated record script.",
172
+ );
173
+ }
174
+ }
175
+
176
+ async function smokeGeneratedDevServer(providerDir: string): Promise<void> {
177
+ const port = await getAvailablePort();
178
+ const server = spawn("bun", ["run", "dev"], {
179
+ cwd: providerDir,
180
+ env: { ...process.env, PORT: String(port) },
181
+ stdio: ["ignore", "pipe", "pipe"],
182
+ });
183
+ let output = "";
184
+ server.stdout?.on("data", (chunk) => {
185
+ output += chunk.toString();
186
+ });
187
+ server.stderr?.on("data", (chunk) => {
188
+ output += chunk.toString();
189
+ });
190
+
191
+ try {
192
+ const baseUrl = `http://127.0.0.1:${port}`;
193
+ await waitForHttp(`${baseUrl}/health`, server, () => output);
194
+
195
+ const health = await fetchJson(`${baseUrl}/health`, HEALTH_RESPONSE_SCHEMA);
196
+ if (health.status !== "ok" || health.provider !== "dx-smoke") {
197
+ throw new Error(`Unexpected /health payload: ${JSON.stringify(health)}`);
198
+ }
199
+
200
+ const response = await fetch(`${baseUrl}/v1/ping`, {
201
+ method: "POST",
202
+ headers: { "content-type": "application/json" },
203
+ body: JSON.stringify({
204
+ requestId: "req_pack_smoke_ping",
205
+ input: { value: "hello" },
206
+ headers: {},
207
+ }),
208
+ });
209
+ const payload = PING_RESPONSE_SCHEMA.parse(await response.json());
210
+
211
+ if (!response.ok || payload.data?.ok !== true) {
212
+ throw new Error(
213
+ `Unexpected /v1/ping response (${response.status}): ${JSON.stringify(payload)}`,
214
+ );
215
+ }
216
+ } finally {
217
+ await stopServer(server);
218
+ }
219
+ }
220
+
221
+ async function getAvailablePort(): Promise<number> {
222
+ return await new Promise((resolvePromise, rejectPromise) => {
223
+ const server = createServer();
224
+ server.once("error", rejectPromise);
225
+ server.listen(0, "127.0.0.1", () => {
226
+ const address = server.address();
227
+ server.close((error) => {
228
+ if (error) {
229
+ rejectPromise(error);
230
+ return;
231
+ }
232
+ if (!address || typeof address === "string") {
233
+ rejectPromise(new Error("Could not allocate a local TCP port."));
234
+ return;
235
+ }
236
+ resolvePromise(address.port);
237
+ });
238
+ });
239
+ });
240
+ }
241
+
242
+ async function fetchJson<T>(url: string, schema: z.ZodType<T>): Promise<T> {
243
+ const response = await fetch(url);
244
+ if (!response.ok) {
245
+ throw new Error(`${url} returned ${response.status}`);
246
+ }
247
+ return schema.parse(await response.json());
248
+ }
249
+
250
+ async function waitForHttp(
251
+ url: string,
252
+ server: ChildProcess,
253
+ getOutput: () => string,
254
+ ): Promise<void> {
255
+ const deadline = Date.now() + 10_000;
256
+ let lastError: unknown;
257
+
258
+ while (Date.now() < deadline) {
259
+ if (server.exitCode !== null) {
260
+ throw new Error(
261
+ `Dev server exited early with code ${server.exitCode}\n${getOutput()}`,
262
+ );
263
+ }
264
+
265
+ try {
266
+ await fetchJson(url, HEALTH_RESPONSE_SCHEMA);
267
+ return;
268
+ } catch (error) {
269
+ lastError = error;
270
+ await new Promise((resolve) => setTimeout(resolve, 200));
271
+ }
272
+ }
273
+
274
+ throw new Error(
275
+ `Timed out waiting for ${url}: ${lastError instanceof Error ? lastError.message : String(lastError)}\n${getOutput()}`,
276
+ );
277
+ }
278
+
279
+ async function stopServer(server: ChildProcess): Promise<void> {
280
+ if (server.exitCode !== null) {
281
+ return;
282
+ }
283
+ server.kill("SIGTERM");
284
+ await new Promise<void>((resolvePromise) => {
285
+ const timeout = setTimeout(() => {
286
+ if (server.exitCode === null) {
287
+ server.kill("SIGKILL");
288
+ }
289
+ resolvePromise();
290
+ }, 2_000);
291
+ server.once("exit", () => {
292
+ clearTimeout(timeout);
293
+ resolvePromise();
294
+ });
295
+ });
296
+ }
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.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "ApiFuse Provider SDK — Build providers with zero architectural constraints",
@@ -65,20 +65,20 @@
65
65
  "pack:smoke": "bun bin/apifuse-pack-smoke.ts"
66
66
  },
67
67
  "devDependencies": {
68
- "@biomejs/biome": "^2.4.12",
68
+ "@biomejs/biome": "^2.4.15",
69
69
  "@types/bun": "latest",
70
- "@types/node": "^25.1.0",
70
+ "@types/node": "^25.8.0",
71
71
  "typescript": "^6.0.3"
72
72
  },
73
73
  "dependencies": {
74
- "@clack/prompts": "^1.2.0",
74
+ "@clack/prompts": "^1.4.0",
75
75
  "ajv": "^8.17",
76
- "hono": "^4.12.14",
76
+ "hono": "^4.12.19",
77
77
  "playwright": "^1.55.1",
78
78
  "playwright-stealth": "^0.0.1",
79
79
  "re2-wasm": "^1.0",
80
80
  "safe-regex": "^2.1",
81
81
  "tlsclientwrapper": "^4.2.0",
82
- "zod": "^4.3.6"
82
+ "zod": "^4.4.3"
83
83
  }
84
84
  }
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),
@@ -568,9 +568,8 @@ function renderPackageJson(input: {
568
568
  scripts: {
569
569
  dev: "apifuse dev .",
570
570
  check: "apifuse check .",
571
- "record:sample":
572
- 'apifuse record . --operation ping --params \'{"value":"hello"}\'',
573
571
  test: "apifuse test .",
572
+ record: "apifuse record .",
574
573
  "perf:sample": "apifuse perf . --operation ping --runs 3",
575
574
  start: "bun start.ts",
576
575
  },
@@ -21,13 +21,55 @@ bun run test
21
21
  - `POST /auth/poll`
22
22
  - `POST /auth/disconnect`
23
23
 
24
+ ## Local smoke
25
+
26
+ ```bash
27
+ curl -s http://localhost:3900/health
28
+ curl -s -X POST http://localhost:3900/v1/ping \
29
+ -H 'Content-Type: application/json' \
30
+ -d '{"requestId":"req_local_ping","input":{"value":"hello"},"headers":{}}'
31
+ ```
32
+
33
+ The `POST /v1/{operation}` body is a request envelope:
34
+
35
+ - `requestId` is required and can be any unique local debugging string.
36
+ - `input` contains the operation input shape.
37
+ - `headers` is optional.
38
+ - `connection` is optional; omit it for no-auth/public operations. For
39
+ credential debugging, pass `{ "id", "mode", "secrets", "metadata",
40
+ "externalRef" }` with local-only secret values.
41
+
42
+ Structured errors return an `error` object with `code`, `message`,
43
+ `requestId`, and optional `details`; validation failures include field paths in
44
+ `details`, and the `apifuse dev` terminal prints a structured provider log.
45
+
46
+ ## Debugging checklist
47
+
48
+ - `invalid_request`: include `requestId` and `input`; omit `connection` for
49
+ public/no-auth operations and never send `connection: null`.
50
+ - Credentials: declare `credential.keys`, pass local-only values through
51
+ `connection.secrets`, and read them with `ctx.credential`.
52
+ - Auth flow: call `/auth/start`, then `/auth/continue` with the same `flowId`;
53
+ carry returned `contextPatch` values into the next request's `context`.
54
+ - TLS/browser runtime: if Bun blocks native dependency lifecycle scripts, run
55
+ `bun pm untrusted` and trust SDK dependencies such as `koffi` before debugging
56
+ `ctx.tls`; for TypeScript browser Providers use
57
+ `browser.engine: "playwright-stealth"` (`nodriver` is Python-runtime only),
58
+ then install local Chromium with `bunx playwright install chromium` or set
59
+ `CDP_POOL_URL`.
60
+
24
61
  ## Next steps
25
62
 
26
63
  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.
64
+ 2. Once the real operation declares `upstream.baseUrl` and uses `ctx.http` or
65
+ `ctx.tls`, record a fixture with:
66
+ `bun run record -- --operation <operation> --params '<json-input>'`.
28
67
  3. Replace the starter `healthCheckUnsupported` with a real `healthCheck` for read-only upstream operations when safe.
29
68
  4. Extend tests and operation metadata until the provider is bounty-ready.
30
69
 
70
+ `apifuse record` is not expected to work with the generated local-only `ping`
71
+ operation because it intentionally has no upstream response to capture.
72
+
31
73
  ## Health-check authorship
32
74
 
33
75
  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
@@ -629,7 +629,7 @@ export function defineProvider<
629
629
  throw new ProviderError(
630
630
  `Provider "${config.id}" must define browser.engine when runtime is "browser"`,
631
631
  {
632
- fix: 'Add browser: { engine: "nodriver" } or another supported engine',
632
+ fix: 'Add browser: { engine: "playwright-stealth" } for TypeScript providers, or another supported engine for your runtime',
633
633
  },
634
634
  );
635
635
  if (config.browser && config.runtime !== "browser")