@apifuse/provider-sdk 2.0.0-beta.1 → 2.1.0-beta.0

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.
Files changed (78) hide show
  1. package/AUTHORING.md +102 -0
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +100 -28
  4. package/bin/apifuse-check.ts +78 -71
  5. package/bin/apifuse-create.ts +12 -0
  6. package/bin/apifuse-dev.ts +24 -61
  7. package/bin/apifuse-pack-check.ts +47 -0
  8. package/bin/apifuse-perf.ts +33 -32
  9. package/bin/apifuse-record.ts +17 -7
  10. package/bin/apifuse-test.ts +6 -4
  11. package/bin/apifuse.ts +36 -35
  12. package/package.json +28 -9
  13. package/src/ceremonies/index.ts +747 -0
  14. package/src/cli/commands.ts +87 -0
  15. package/src/cli/create.ts +845 -0
  16. package/src/cli/templates/provider/Dockerfile.tpl +7 -0
  17. package/src/cli/templates/provider/README.md.tpl +28 -0
  18. package/src/cli/templates/provider/dev.ts.tpl +5 -0
  19. package/src/cli/templates/provider/index.test.ts.tpl +13 -0
  20. package/src/cli/templates/provider/index.ts.tpl +54 -0
  21. package/src/cli/templates/provider/start.ts.tpl +5 -0
  22. package/src/composite.ts +43 -0
  23. package/src/define.ts +527 -41
  24. package/src/dev.ts +2 -6
  25. package/src/errors.ts +42 -0
  26. package/src/index.ts +50 -38
  27. package/src/lint.ts +574 -0
  28. package/src/provider.ts +14 -0
  29. package/src/runtime/auth-flow.ts +67 -0
  30. package/src/runtime/credential.ts +95 -0
  31. package/src/runtime/env.ts +13 -0
  32. package/src/runtime/executor.ts +13 -14
  33. package/src/runtime/http.ts +10 -2
  34. package/src/runtime/insights.ts +3 -3
  35. package/src/runtime/key-derivation.ts +122 -0
  36. package/src/runtime/keyring.ts +148 -0
  37. package/src/runtime/namespace.ts +33 -0
  38. package/src/runtime/prevalidate.ts +252 -0
  39. package/src/runtime/tls.ts +20 -5
  40. package/src/runtime/waterfall.ts +0 -1
  41. package/src/schema.ts +77 -0
  42. package/src/serve.ts +1 -664
  43. package/src/server/index.ts +22 -0
  44. package/src/server/serve.ts +610 -0
  45. package/src/server/types.ts +78 -0
  46. package/src/stealth/profiles.ts +10 -93
  47. package/src/testing/run.ts +391 -32
  48. package/src/types.ts +364 -41
  49. package/bin/apifuse-init.ts +0 -387
  50. package/src/__tests__/auth.test.ts +0 -396
  51. package/src/__tests__/browser-auth.test.ts +0 -180
  52. package/src/__tests__/browser.test.ts +0 -632
  53. package/src/__tests__/define.test.ts +0 -225
  54. package/src/__tests__/errors.test.ts +0 -69
  55. package/src/__tests__/executor.test.ts +0 -214
  56. package/src/__tests__/http.test.ts +0 -238
  57. package/src/__tests__/insights.test.ts +0 -210
  58. package/src/__tests__/instrumentation.test.ts +0 -290
  59. package/src/__tests__/otlp.test.ts +0 -141
  60. package/src/__tests__/perf.test.ts +0 -60
  61. package/src/__tests__/providers-yaml.test.ts +0 -135
  62. package/src/__tests__/proxy.test.ts +0 -359
  63. package/src/__tests__/recipes.test.ts +0 -36
  64. package/src/__tests__/serve.test.ts +0 -233
  65. package/src/__tests__/session.test.ts +0 -231
  66. package/src/__tests__/state.test.ts +0 -100
  67. package/src/__tests__/stealth.test.ts +0 -57
  68. package/src/__tests__/testing.test.ts +0 -97
  69. package/src/__tests__/tls.test.ts +0 -345
  70. package/src/__tests__/types.test.ts +0 -142
  71. package/src/__tests__/utils.test.ts +0 -62
  72. package/src/__tests__/waterfall.test.ts +0 -270
  73. package/src/config/providers-yaml.ts +0 -370
  74. package/src/index.test.ts +0 -1
  75. package/src/protocol.ts +0 -183
  76. package/src/runtime/auth.ts +0 -245
  77. package/src/runtime/session.ts +0 -573
  78. package/src/runtime/state.ts +0 -124
package/AUTHORING.md ADDED
@@ -0,0 +1,102 @@
1
+ ## Generator and runtime alignment
2
+
3
+ - Canonical scaffolding command: `apifuse create`
4
+ - Monorepo contributors should use `apifuse create <name> --preset monorepo`
5
+ - Standalone users should use `bunx @apifuse/provider-sdk create <name>`
6
+ - Provider server contract is:
7
+ - dev default `3900`
8
+ - start/Docker/container `3000`
9
+ - `GET /health`
10
+ - `POST /v1/{operation}`
11
+ - `POST /auth/start`
12
+ - `POST /auth/continue`
13
+ - `POST /auth/poll`
14
+ - `POST /auth/disconnect`
15
+ - Generator v1 for this redesign scaffolds TypeScript providers only. Python generation is future work.
16
+
17
+ ## Provider Authoring Guide
18
+
19
+ Provider code is the declaration input to the internal platform registry. The public SDK owns provider authoring/runtime ergonomics; internal docs, deploy, and discovery projections are built downstream from those declarations. `bun run lint:providers` enforces provider authoring standards.
20
+
21
+ ### Description template
22
+
23
+ Every operation `description` MUST be at least 150 characters and follow this structure:
24
+
25
+ ```
26
+ <What the tool does in one sentence>. Use when <specific scenarios>. Do NOT use for <counter-scenarios; point to alternatives>. Returns <key output fields>. <Important caveats: rate limits, auth, freshness>.
27
+ ```
28
+
29
+ Example:
30
+ ```ts
31
+ description:
32
+ "Retrieves KMA ultra-short-term weather observation for a given grid coordinate in South Korea, " +
33
+ "including temperature, humidity, wind speed, precipitation, and sky condition. " +
34
+ "Use when the user asks about current or hourly weather at a specific Korean location. " +
35
+ "Do NOT use for forecasts beyond 2 days — use kma_mid_forecast instead. " +
36
+ "Returns hourly data in KST timezone; null values indicate data unavailable. " +
37
+ "Rate-limited to 1000 calls/day on the free tier.",
38
+ ```
39
+
40
+ ### Language policy
41
+
42
+ - **Structural text**: English (operation `description`, Zod `.describe()`, `whenToUse`, `whenNotToUse`, `derivations`, `inputExamples.scenario/rationale`).
43
+ - **Values only**: native language (fixtures payloads, `inputExamples[].input` values like "대방동", "KRW-BTC", entity catalog entries).
44
+
45
+ ### Required per operation
46
+
47
+ - `description` — 150+ chars English (error-level rule)
48
+ - Every Zod field in input AND output has `.describe()` including nested objects + array items (error-level rule)
49
+ - `fixtures.request` + `fixtures.response` both present (error-level rule)
50
+
51
+ ### Factored operations
52
+
53
+ Use `defineOperation()` when an operation is large enough to live beside helper functions or in a separate module. It preserves the same type inference as inline `defineProvider()` operations and can be placed directly in the provider `operations` map. `defineProvider()` accepts Zod and Standard Schema v1-compatible schemas. If config validation fails, the SDK names the field to fix, for example `runtime`, `auth.mode`, `operations.<id>.handler`, or `operations.<id>.fixtures.response`.
54
+
55
+ ### Strongly recommended (warn-level rules)
56
+
57
+ - `description` includes "use" AND "when" phrasing
58
+ - `inputExamples` with 2+ scenarios for complex input (nested objects, enums, format-sensitive strings)
59
+ - `derivations` for parameters not directly visible in the user query (e.g., `gridX` derived from geocoding)
60
+
61
+ ### Optional but valuable
62
+
63
+ - `annotations`: `{ readOnly, destructive, idempotent, openWorld, rateLimit }` — agentic safety signals
64
+ - `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
+ ```
90
+
91
+ ### Running the lint locally
92
+
93
+ ```bash
94
+ bun run lint:providers
95
+ ```
96
+
97
+ - Exit 0: all providers pass error-level rules (warnings may still appear)
98
+ - Exit 1: at least one error-level violation; CI will block merge
99
+
100
+ ### CI enforcement
101
+
102
+ `bun run lint:providers` runs in the `lint-and-typecheck` job on every pull request. Error-level violations block merges.
package/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # @apifuse/provider-sdk Changelog
2
+
3
+ ## 2.1.0-beta.0
4
+
5
+ - 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`.
6
+ - Make removed Chrome/Edge stealth profile names fail loudly with `SDKError("Unknown stealth profile: <name>")` instead of falling through to a raw TLS identifier.
7
+
8
+ ## 2.0.0-beta.2
9
+
10
+ - Improve `defineProvider()` operation handler inference from Zod and Standard Schema inputs.
11
+ - Add `defineOperation()` for factored, composable operation declarations.
12
+ - Add descriptive `defineProvider()` validation errors for missing fields, invalid runtime/auth modes, and path-conflicting operation ids.
13
+ - Improve `runStandardTests()` fixture failures with current/expected JSON diffs.
14
+ - Document provider authoring ergonomics and public schema-related types.
package/README.md CHANGED
@@ -1,44 +1,116 @@
1
1
  # @apifuse/provider-sdk
2
2
 
3
- ApiFuse Provider SDK — Build private API automation providers.
3
+ ApiFuse Provider SDK — build provider declarations and runtimes with one public SDK surface and one canonical CLI.
4
4
 
5
- ## Installation
5
+ ## Install
6
6
 
7
7
  ```bash
8
8
  bun add @apifuse/provider-sdk
9
9
  ```
10
10
 
11
- ## Quick Start
11
+ ## Create a provider
12
12
 
13
- ```typescript
14
- import { defineProvider } from '@apifuse/provider-sdk'
15
- import { z } from 'zod'
13
+ ### Standalone (default)
16
14
 
17
- export default defineProvider({
18
- meta: {
19
- id: 'my-api-prices',
20
- displayName: 'My API Prices',
21
- category: 'finance',
22
- version: '1.0.0',
23
- runtime: 'standard',
24
- upstream: { baseUrl: 'https://api.example.com' },
25
- },
26
- input: z.object({ id: z.string() }),
27
- output: z.object({ price: z.number() }),
28
- operations: {
29
- prices: {
30
- execute: async (ctx, input) => {
31
- const res = await ctx.http.get('/prices', { params: { id: input.id } })
32
- return res.data as { price: number }
33
- },
34
- },
15
+ ```bash
16
+ bunx @apifuse/provider-sdk create my-provider
17
+ ```
18
+
19
+ The canonical `create` flow:
20
+ 1. scaffolds the provider,
21
+ 2. installs dependencies,
22
+ 3. runs baseline validation,
23
+ 4. prints the exact next local-dev command.
24
+
25
+ ### Monorepo preset
26
+
27
+ Inside the ApiFuse repository:
28
+
29
+ ```bash
30
+ apifuse create my-provider --preset monorepo
31
+ ```
32
+
33
+ ## Provider server contract
34
+
35
+ - Dev default: `3900`
36
+ - Start/Docker/container contract: `3000`
37
+ - `GET /health`
38
+ - `POST /v1/{operation}`
39
+ - `POST /auth/start`
40
+ - `POST /auth/continue`
41
+ - `POST /auth/poll`
42
+ - `POST /auth/disconnect`
43
+
44
+ Removed legacy runtime paths are not supported:
45
+ - `/execute/*`
46
+ - `/auth/abort`
47
+
48
+ ## Local workflow
49
+
50
+ ```bash
51
+ cd my-provider
52
+ bun run dev
53
+ bun run check
54
+ bun run test
55
+ ```
56
+
57
+ ## Authoring ergonomics
58
+
59
+ `defineProvider()` infers each operation handler input from the operation `input` schema. For larger providers, factor operations with `defineOperation()` and compose them later:
60
+
61
+ ```ts
62
+ import { defineOperation, defineProvider, z } from "@apifuse/provider-sdk/provider"
63
+
64
+ const search = defineOperation({
65
+ input: z.object({ q: z.string().describe("Search query") }),
66
+ output: z.object({ count: z.number().describe("Result count") }),
67
+ async handler(ctx, input) {
68
+ return { count: input.q.length }
35
69
  },
36
70
  })
71
+
72
+ export default defineProvider({
73
+ id: "factored-provider",
74
+ version: "1.0.0",
75
+ runtime: "standard",
76
+ meta: { displayName: "Factored", category: "demo" },
77
+ operations: { search },
78
+ })
37
79
  ```
38
80
 
39
- ## Testing
81
+ 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
+
83
+ ### Operation annotations
84
+
85
+ Operations declare non-functional metadata via `annotations`:
86
+
87
+ | Field | Type | Notes |
88
+ |---|---|---|
89
+ | `readOnly` | `boolean` | Operation has no side effects (safe to test in production). |
90
+ | `destructive` | `boolean` | Operation modifies/deletes state. |
91
+ | `idempotent` | `boolean` | Safe to retry without duplicate side effects. |
92
+ | `openWorld` | `boolean` | Callable without authentication. |
93
+ | `rateLimit` | `{ calls, window }` | Per-operation rate hint. `window` is `"minute"\|"hour"\|"day"`. |
94
+ | `timeoutMs` | `number` | Per-operation upstream timeout (1–60000 ms). Omit to inherit the gateway global default. |
40
95
 
41
- ```typescript
42
- import { runStandardTests } from '@apifuse/provider-sdk/testing'
43
- runStandardTests(myProvider)
96
+ `defineProvider()` validates `timeoutMs` is an integer in `[1, 60000]` and throws `ValidationError` otherwise. The gateway applies the value via `context.WithTimeout` on every proxied call and clamps defensively to the same bound.
97
+
98
+ ## Canonical CLI surface
99
+
100
+ ```bash
101
+ apifuse create <name>
102
+ apifuse dev [path]
103
+ apifuse check [path]
104
+ apifuse record [path] --operation <operation> --params '{"value":"hello"}'
105
+ apifuse test [path]
106
+ apifuse perf <path> --operation <operation>
44
107
  ```
108
+
109
+ ## Scope boundary
110
+
111
+ Generator v1 scaffolds **TypeScript providers only** for this redesign. Python generation remains future work.
112
+
113
+
114
+ ## Boundary
115
+
116
+ 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.
@@ -7,22 +7,12 @@ import { pathToFileURL } from "node:url";
7
7
  import { z } from "zod";
8
8
 
9
9
  import type { ProviderDefinition } from "../src";
10
+ import { safeParseSchemaSync } from "../src/schema";
10
11
 
11
12
  const HELP_TEXT = `Usage: apifuse check [path]
12
- Example: apifuse check providers/upbit-crypto
13
+ Example: apifuse check providers/airkorea
13
14
  Default: apifuse check .`;
14
15
 
15
- const manifestSchema = z.object({
16
- auth: z.enum(["none", "credentials", "api-key", "oauth2"]),
17
- category: z.string().min(1),
18
- displayName: z.string().min(1),
19
- id: z.string().min(1),
20
- language: z.literal("typescript"),
21
- runtime: z.enum(["standard", "browser"]),
22
- sdkVersion: z.number().int().positive(),
23
- version: z.string().min(1),
24
- });
25
-
26
16
  type CheckResult = {
27
17
  message: string;
28
18
  passed: boolean;
@@ -31,7 +21,7 @@ type CheckResult = {
31
21
 
32
22
  type SafeParseResult =
33
23
  | { success: true; data: unknown }
34
- | { success: false; error: z.ZodError };
24
+ | { success: false; error: unknown };
35
25
 
36
26
  export async function main() {
37
27
  const args = normalizeArgs(process.argv.slice(2));
@@ -115,7 +105,6 @@ function resolveFromParents(inputPath: string): string {
115
105
 
116
106
  async function runChecks(providerRoot: string): Promise<CheckResult[]> {
117
107
  const indexPath = resolve(providerRoot, "index.ts");
118
- const manifestPath = resolve(providerRoot, "manifest.json");
119
108
  const dockerfilePath = resolve(providerRoot, "Dockerfile");
120
109
  const packageJsonPath = resolve(providerRoot, "package.json");
121
110
 
@@ -129,7 +118,7 @@ async function runChecks(providerRoot: string): Promise<CheckResult[]> {
129
118
  checkOperations(provider),
130
119
  checkFixtures(provider),
131
120
  checkSchemas(provider),
132
- checkManifest(manifestPath, provider),
121
+ checkProviderMetadata(provider),
133
122
  checkDockerfile(dockerfilePath),
134
123
  checkPackageJson(packageJsonPath),
135
124
  ];
@@ -177,11 +166,11 @@ function checkOperations(
177
166
  failures.push(`${operationId}: missing handler`);
178
167
  }
179
168
 
180
- if (!hasSafeParse(operation.input)) {
169
+ if (!hasSchemaParser(operation.input)) {
181
170
  failures.push(`${operationId}: missing input schema`);
182
171
  }
183
172
 
184
- if (!hasSafeParse(operation.output)) {
173
+ if (!hasSchemaParser(operation.output)) {
185
174
  failures.push(`${operationId}: missing output schema`);
186
175
  }
187
176
  }
@@ -243,7 +232,7 @@ function checkSchemas(provider: ProviderDefinition | undefined): CheckResult {
243
232
  );
244
233
  if (!requestResult.success) {
245
234
  failures.push(
246
- `${operationId}: request fixture invalid (${requestResult.error.issues.map((issue: z.ZodIssue) => issue.message).join(", ")})`,
235
+ `${operationId}: request fixture invalid (${formatSchemaError(requestResult.error)})`,
247
236
  );
248
237
  }
249
238
 
@@ -253,7 +242,7 @@ function checkSchemas(provider: ProviderDefinition | undefined): CheckResult {
253
242
  );
254
243
  if (!responseResult.success) {
255
244
  failures.push(
256
- `${operationId}: response fixture invalid (${responseResult.error.issues.map((issue: z.ZodIssue) => issue.message).join(", ")})`,
245
+ `${operationId}: response fixture invalid (${formatSchemaError(responseResult.error)})`,
257
246
  );
258
247
  }
259
248
  }
@@ -265,58 +254,52 @@ function checkSchemas(provider: ProviderDefinition | undefined): CheckResult {
265
254
  };
266
255
  }
267
256
 
268
- function checkManifest(
269
- manifestPath: string,
257
+ function checkProviderMetadata(
270
258
  provider: ProviderDefinition | undefined,
271
259
  ): CheckResult {
272
- if (!existsSync(manifestPath)) {
273
- return { message: "manifest.json exists and is valid", passed: false };
274
- }
275
-
276
- try {
277
- const manifest = manifestSchema.parse(
278
- JSON.parse(readFileSync(manifestPath, "utf-8")) as unknown,
279
- );
280
- const details: string[] = [];
281
-
282
- if (provider) {
283
- if (manifest.id !== provider.id) {
284
- details.push(
285
- `id mismatch: manifest=${manifest.id} provider=${provider.id}`,
286
- );
287
- }
288
-
289
- if (manifest.displayName !== provider.meta.displayName) {
290
- details.push(
291
- `displayName mismatch: manifest=${manifest.displayName} provider=${provider.meta.displayName}`,
292
- );
293
- }
294
-
295
- if (manifest.category !== provider.meta.category) {
296
- details.push(
297
- `category mismatch: manifest=${manifest.category} provider=${provider.meta.category}`,
298
- );
299
- }
300
-
301
- if (manifest.runtime !== provider.runtime) {
302
- details.push(
303
- `runtime mismatch: manifest=${manifest.runtime} provider=${provider.runtime}`,
304
- );
305
- }
306
- }
307
-
308
- return {
309
- message: "manifest.json exists and is valid",
310
- passed: details.length === 0,
311
- details,
312
- };
313
- } catch (error) {
260
+ if (!provider) {
314
261
  return {
315
- message: "manifest.json exists and is valid",
262
+ message: "Provider metadata is declared in defineProvider",
316
263
  passed: false,
317
- details: [error instanceof Error ? error.message : String(error)],
318
264
  };
319
265
  }
266
+
267
+ const details: string[] = [];
268
+
269
+ if (!provider.id.trim()) {
270
+ details.push("provider.id is empty");
271
+ }
272
+
273
+ if (!provider.meta.displayName.trim()) {
274
+ details.push("provider.meta.displayName is empty");
275
+ }
276
+
277
+ if (!provider.meta.category.trim()) {
278
+ details.push("provider.meta.category is empty");
279
+ }
280
+
281
+ if (!provider.runtime) {
282
+ details.push("provider.runtime is missing");
283
+ }
284
+
285
+ if (!provider.auth?.mode) {
286
+ details.push("provider.auth.mode is missing");
287
+ }
288
+
289
+ return {
290
+ message: "Provider metadata is declared in defineProvider",
291
+ passed: details.length === 0,
292
+ details:
293
+ details.length > 0
294
+ ? details
295
+ : [
296
+ `id: ${provider.id}`,
297
+ `displayName: ${provider.meta.displayName}`,
298
+ `category: ${provider.meta.category}`,
299
+ `runtime: ${provider.runtime}`,
300
+ `auth: ${provider.auth?.mode ?? "none"}`,
301
+ ],
302
+ };
320
303
  }
321
304
 
322
305
  function checkDockerfile(dockerfilePath: string): CheckResult {
@@ -388,14 +371,38 @@ function isRecord(value: unknown): value is Record<string, unknown> {
388
371
  return typeof value === "object" && value !== null;
389
372
  }
390
373
 
391
- function hasSafeParse(value: unknown): value is {
392
- safeParse: (input: unknown) => SafeParseResult;
393
- } {
394
- return isRecord(value) && typeof value.safeParse === "function";
374
+ function hasSchemaParser(value: unknown): boolean {
375
+ return (
376
+ isRecord(value) &&
377
+ (typeof value.safeParse === "function" ||
378
+ (isRecord(value["~standard"]) &&
379
+ typeof value["~standard"].validate === "function"))
380
+ );
381
+ }
382
+
383
+ function formatSchemaError(error: unknown): string {
384
+ if (error instanceof z.ZodError) {
385
+ return error.issues.map((issue) => issue.message).join(", ");
386
+ }
387
+
388
+ if (Array.isArray(error)) {
389
+ return error
390
+ .map((issue) =>
391
+ isRecord(issue) && typeof issue.message === "string"
392
+ ? issue.message
393
+ : String(issue),
394
+ )
395
+ .join(", ");
396
+ }
397
+
398
+ return error instanceof Error ? error.message : String(error);
395
399
  }
396
400
 
397
- function parseFixture(schema: z.ZodType, fixture: unknown): SafeParseResult {
398
- return schema.safeParse(fixture);
401
+ function parseFixture(
402
+ schema: ProviderDefinition["operations"][string]["input"],
403
+ fixture: unknown,
404
+ ): SafeParseResult {
405
+ return safeParseSchemaSync(schema, fixture, "fixture");
399
406
  }
400
407
 
401
408
  if (import.meta.main) {
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { main as runMain } from "../src/cli/create";
4
+
5
+ export { runMain as main };
6
+
7
+ if (import.meta.main) {
8
+ await runMain().catch((error: unknown) => {
9
+ console.error(error instanceof Error ? error.message : String(error));
10
+ process.exit(1);
11
+ });
12
+ }
@@ -2,22 +2,21 @@
2
2
 
3
3
  import { existsSync } from "node:fs";
4
4
  import { dirname, resolve } from "node:path";
5
- import * as readline from "node:readline";
6
5
 
7
6
  import type { ProviderDefinition } from "../src";
8
7
  import {
9
8
  createBrowserClient,
9
+ createCredentialContext,
10
+ createEnvContext,
10
11
  createHttpClient,
11
- createStateContext,
12
12
  createTlsClient,
13
13
  ProviderError,
14
14
  } from "../src";
15
- import type { AuthManager } from "../src/runtime/auth";
16
15
  import { createTraceContext } from "../src/runtime/trace";
17
- import type { ProviderContext, SessionStore } from "../src/types";
16
+ import type { BrowserClient, ProviderContext } from "../src/types";
18
17
 
19
18
  const HELP_TEXT = `Usage: apifuse dev [path]
20
- Example: apifuse dev providers/upbit-crypto
19
+ Example: apifuse dev providers/airkorea
21
20
  Default: apifuse dev .`;
22
21
 
23
22
  export async function main() {
@@ -44,23 +43,27 @@ export async function main() {
44
43
  console.log(` GET http://localhost:${port}/health`);
45
44
 
46
45
  for (const operationId of Object.keys(provider.operations)) {
47
- console.log(` POST http://localhost:${port}/execute/${operationId}`);
48
- console.log(` GET http://localhost:${port}/schema/${operationId}`);
46
+ console.log(` POST http://localhost:${port}/v1/${operationId}`);
49
47
  }
50
48
 
49
+ console.log(` POST http://localhost:${port}/auth/start`);
50
+ console.log(` POST http://localhost:${port}/auth/continue`);
51
+ console.log(` POST http://localhost:${port}/auth/poll`);
52
+ console.log(` POST http://localhost:${port}/auth/disconnect`);
53
+
51
54
  console.log("\nHot reload:");
52
55
  console.log(
53
56
  ` bun --hot ${resolveImportPath("apifuse-dev.ts")} ${args[0] ?? "."}`,
54
57
  );
55
58
  }
56
59
 
57
- export function createProviderContext(
58
- provider: ProviderDefinition,
59
- session: SessionStore,
60
- authManager: AuthManager,
61
- ): { ctx: ProviderContext } {
60
+ export function createProviderContext(provider: ProviderDefinition): {
61
+ ctx: ProviderContext;
62
+ } {
62
63
  const ctx: ProviderContext = {
63
- auth: authManager.createAuthContext(),
64
+ env: createEnvContext(provider.secrets?.map((secret) => secret.name)),
65
+ credential: createCredentialContext(),
66
+ auth: createUnsupportedAuthStub(),
64
67
  browser:
65
68
  provider.runtime === "browser"
66
69
  ? createBrowserClient({
@@ -68,8 +71,6 @@ export function createProviderContext(
68
71
  })
69
72
  : createUnsupportedBrowserStub(),
70
73
  http: createHttpClient(),
71
- session,
72
- state: createStateContext("dev-secret"),
73
74
  trace: createTraceContext(),
74
75
  tls: createTlsClient("http://localhost"),
75
76
  };
@@ -77,39 +78,6 @@ export function createProviderContext(
77
78
  return { ctx };
78
79
  }
79
80
 
80
- export async function runExchangeWithDeferredFieldPrompting(
81
- authManager: AuthManager,
82
- ctx: ProviderContext,
83
- credentials: Record<string, string>,
84
- options: { pollIntervalMs?: number } = {},
85
- ): Promise<void> {
86
- const promptedFields = new Set<string>();
87
- const pollIntervalMs = options.pollIntervalMs ?? 100;
88
- let isSettled = false;
89
-
90
- const exchangePromise = authManager.exchange(ctx, credentials).finally(() => {
91
- isSettled = true;
92
- });
93
-
94
- while (!isSettled) {
95
- for (const fieldName of authManager.getPendingFields()) {
96
- if (promptedFields.has(fieldName)) {
97
- continue;
98
- }
99
-
100
- promptedFields.add(fieldName);
101
- const value = await promptForField(fieldName);
102
- authManager.resolveField(fieldName, value.trim());
103
- }
104
-
105
- if (!isSettled) {
106
- await Bun.sleep(pollIntervalMs);
107
- }
108
- }
109
-
110
- await exchangePromise;
111
- }
112
-
113
81
  function normalizeArgs(argv: string[]): string[] {
114
82
  return argv[0] === "dev" ? argv.slice(1) : argv;
115
83
  }
@@ -147,7 +115,7 @@ function resolveImportPath(fileName: string): string {
147
115
  return resolve(process.cwd(), "bin", fileName);
148
116
  }
149
117
 
150
- function createUnsupportedBrowserStub(): ProviderContext["browser"] {
118
+ function createUnsupportedBrowserStub(): BrowserClient {
151
119
  return {
152
120
  engine: "playwright-stealth",
153
121
  async newPage() {
@@ -163,20 +131,15 @@ function createUnsupportedBrowserStub(): ProviderContext["browser"] {
163
131
  }
164
132
 
165
133
  async function promptForField(fieldName: string): Promise<string> {
166
- const rl = readline.createInterface({
167
- input: process.stdin,
168
- output: process.stdout,
134
+ throw new ProviderError(`Auth prompt is unavailable for ${fieldName}`, {
135
+ code: "AUTH_PROMPT_UNAVAILABLE",
169
136
  });
137
+ }
170
138
 
171
- try {
172
- return await new Promise<string>((resolvePrompt) => {
173
- rl.question(`\n[apifuse dev] Enter ${fieldName}: `, (answer) => {
174
- resolvePrompt(answer);
175
- });
176
- });
177
- } finally {
178
- rl.close();
179
- }
139
+ function createUnsupportedAuthStub() {
140
+ return {
141
+ requestField: promptForField,
142
+ };
180
143
  }
181
144
 
182
145
  function assertProviderDefinition(