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

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 (79) hide show
  1. package/AUTHORING.md +93 -0
  2. package/CHANGELOG.md +21 -0
  3. package/README.md +133 -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 +87 -0
  8. package/bin/apifuse-pack-smoke.ts +122 -0
  9. package/bin/apifuse-perf.ts +33 -32
  10. package/bin/apifuse-record.ts +17 -7
  11. package/bin/apifuse-test.ts +6 -4
  12. package/bin/apifuse.ts +36 -35
  13. package/package.json +29 -9
  14. package/src/ceremonies/index.ts +768 -0
  15. package/src/cli/commands.ts +87 -0
  16. package/src/cli/create.ts +845 -0
  17. package/src/cli/templates/provider/Dockerfile.tpl +7 -0
  18. package/src/cli/templates/provider/README.md.tpl +41 -0
  19. package/src/cli/templates/provider/dev.ts.tpl +5 -0
  20. package/src/cli/templates/provider/index.test.ts.tpl +13 -0
  21. package/src/cli/templates/provider/index.ts.tpl +58 -0
  22. package/src/cli/templates/provider/start.ts.tpl +5 -0
  23. package/src/config/loader.ts +61 -1
  24. package/src/define.ts +565 -41
  25. package/src/dev.ts +2 -6
  26. package/src/errors.ts +42 -0
  27. package/src/index.ts +44 -38
  28. package/src/lint.ts +574 -0
  29. package/src/provider.ts +13 -0
  30. package/src/runtime/auth-flow.ts +67 -0
  31. package/src/runtime/credential.ts +95 -0
  32. package/src/runtime/env.ts +13 -0
  33. package/src/runtime/executor.ts +13 -14
  34. package/src/runtime/http.ts +36 -12
  35. package/src/runtime/insights.ts +3 -3
  36. package/src/runtime/key-derivation.ts +122 -0
  37. package/src/runtime/keyring.ts +148 -0
  38. package/src/runtime/namespace.ts +33 -0
  39. package/src/runtime/prevalidate.ts +252 -0
  40. package/src/runtime/tls.ts +41 -17
  41. package/src/runtime/waterfall.ts +0 -1
  42. package/src/schema.ts +77 -0
  43. package/src/serve.ts +1 -664
  44. package/src/server/index.ts +22 -0
  45. package/src/server/serve.ts +624 -0
  46. package/src/server/types.ts +78 -0
  47. package/src/stealth/profiles.ts +10 -93
  48. package/src/testing/run.ts +391 -32
  49. package/src/types.ts +390 -41
  50. package/bin/apifuse-init.ts +0 -387
  51. package/src/__tests__/auth.test.ts +0 -396
  52. package/src/__tests__/browser-auth.test.ts +0 -180
  53. package/src/__tests__/browser.test.ts +0 -632
  54. package/src/__tests__/define.test.ts +0 -225
  55. package/src/__tests__/errors.test.ts +0 -69
  56. package/src/__tests__/executor.test.ts +0 -214
  57. package/src/__tests__/http.test.ts +0 -238
  58. package/src/__tests__/insights.test.ts +0 -210
  59. package/src/__tests__/instrumentation.test.ts +0 -290
  60. package/src/__tests__/otlp.test.ts +0 -141
  61. package/src/__tests__/perf.test.ts +0 -60
  62. package/src/__tests__/providers-yaml.test.ts +0 -135
  63. package/src/__tests__/proxy.test.ts +0 -359
  64. package/src/__tests__/recipes.test.ts +0 -36
  65. package/src/__tests__/serve.test.ts +0 -233
  66. package/src/__tests__/session.test.ts +0 -231
  67. package/src/__tests__/state.test.ts +0 -100
  68. package/src/__tests__/stealth.test.ts +0 -57
  69. package/src/__tests__/testing.test.ts +0 -97
  70. package/src/__tests__/tls.test.ts +0 -345
  71. package/src/__tests__/types.test.ts +0 -142
  72. package/src/__tests__/utils.test.ts +0 -62
  73. package/src/__tests__/waterfall.test.ts +0 -270
  74. package/src/config/providers-yaml.ts +0 -370
  75. package/src/index.test.ts +0 -1
  76. package/src/protocol.ts +0 -183
  77. package/src/runtime/auth.ts +0 -245
  78. package/src/runtime/session.ts +0 -573
  79. package/src/runtime/state.ts +0 -124
package/AUTHORING.md ADDED
@@ -0,0 +1,93 @@
1
+ ## Generator and runtime alignment
2
+
3
+ - Canonical scaffolding command: `apifuse create`
4
+ - Monorepo contributors should use `apifuse create <name> --preset monorepo`
5
+ - Standalone bounty contributors should use `bunx @apifuse/provider-sdk@beta create <name> --yes` until this release is promoted to `latest`
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
+ - 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.
51
+
52
+ ### Factored operations
53
+
54
+ 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`.
55
+
56
+ ### Strongly recommended (warn-level rules)
57
+
58
+ - `description` includes "use" AND "when" phrasing
59
+ - `inputExamples` with 2+ scenarios for complex input (nested objects, enums, format-sensitive strings)
60
+ - `derivations` for parameters not directly visible in the user query (e.g., `gridX` derived from geocoding)
61
+
62
+ ### Optional but valuable
63
+
64
+ - `annotations`: `{ readOnly, destructive, idempotent, openWorld, rateLimit }` — agentic safety signals
65
+ - `tags`: operation-level semantic tags for retrieval (e.g., `["weather", "korea", "realtime"]`)
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
+ ### Running the lint locally
83
+
84
+ ```bash
85
+ bun run lint:providers
86
+ ```
87
+
88
+ - Exit 0: all providers pass error-level rules (warnings may still appear)
89
+ - Exit 1: at least one error-level violation; CI will block merge
90
+
91
+ ### CI enforcement
92
+
93
+ `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,21 @@
1
+ # @apifuse/provider-sdk Changelog
2
+
3
+ ## 2.1.0-beta.1
4
+
5
+ - Fix public `apifuse create` runtime packaging by publishing `@clack/prompts` as a production dependency.
6
+ - Update generated Provider starter templates so the sample operation declares a local-only `healthCheckUnsupported` and passes the current health coverage contract.
7
+ - Add packed-artifact smoke coverage for the public create/check/test flow before npm release publishing.
8
+ - Document the public SDK-only bounty contributor path and maintainer-owned monorepo import boundary.
9
+
10
+ ## 2.1.0-beta.0
11
+
12
+ - 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`.
13
+ - Make removed Chrome/Edge stealth profile names fail loudly with `SDKError("Unknown stealth profile: <name>")` instead of falling through to a raw TLS identifier.
14
+
15
+ ## 2.0.0-beta.2
16
+
17
+ - Improve `defineProvider()` operation handler inference from Zod and Standard Schema inputs.
18
+ - Add `defineOperation()` for factored, composable operation declarations.
19
+ - Add descriptive `defineProvider()` validation errors for missing fields, invalid runtime/auth modes, and path-conflicting operation ids.
20
+ - Improve `runStandardTests()` fixture failures with current/expected JSON diffs.
21
+ - Document provider authoring ergonomics and public schema-related types.
package/README.md CHANGED
@@ -1,44 +1,149 @@
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
+ For external bounty scaffolding, use the beta tag until this release is promoted
12
+ to `latest`:
12
13
 
13
- ```typescript
14
- import { defineProvider } from '@apifuse/provider-sdk'
15
- import { z } from 'zod'
14
+ ```bash
15
+ bunx @apifuse/provider-sdk@beta create my-provider --yes
16
+ ```
16
17
 
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
- },
18
+ ## Create a provider
19
+
20
+ ### Standalone (default)
21
+
22
+ ```bash
23
+ bunx @apifuse/provider-sdk@beta create my-provider --yes
24
+ ```
25
+
26
+ The canonical `create` flow:
27
+ 1. scaffolds the provider,
28
+ 2. installs dependencies,
29
+ 3. runs baseline validation,
30
+ 4. prints the exact next local-dev command.
31
+
32
+ ### Monorepo preset
33
+
34
+ Inside the ApiFuse repository:
35
+
36
+ ```bash
37
+ apifuse create my-provider --preset monorepo
38
+ ```
39
+
40
+ ## Provider server contract
41
+
42
+ - Dev default: `3900`
43
+ - Start/Docker/container contract: `3000`
44
+ - `GET /health`
45
+ - `POST /v1/{operation}`
46
+ - `POST /auth/start`
47
+ - `POST /auth/continue`
48
+ - `POST /auth/poll`
49
+ - `POST /auth/disconnect`
50
+
51
+ Removed legacy runtime paths are not supported:
52
+ - `/execute/*`
53
+ - `/auth/abort`
54
+
55
+ ## Local workflow
56
+
57
+ ```bash
58
+ cd my-provider
59
+ bun run check
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 '{"input":{"value":"hello"},"headers":{},"connection":null}'
71
+ ```
72
+
73
+ ## Authoring ergonomics
74
+
75
+ `defineProvider()` infers each operation handler input from the operation `input` schema. For larger providers, factor operations with `defineOperation()` and compose them later:
76
+
77
+ ```ts
78
+ import { defineOperation, defineProvider, z } from "@apifuse/provider-sdk/provider"
79
+
80
+ const search = defineOperation({
81
+ input: z.object({ q: z.string().describe("Search query") }),
82
+ output: z.object({ count: z.number().describe("Result count") }),
83
+ async handler(ctx, input) {
84
+ return { count: input.q.length }
35
85
  },
36
86
  })
87
+
88
+ export default defineProvider({
89
+ id: "factored-provider",
90
+ version: "1.0.0",
91
+ runtime: "standard",
92
+ meta: { displayName: "Factored", category: "demo" },
93
+ operations: { search },
94
+ })
37
95
  ```
38
96
 
39
- ## Testing
97
+ 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`.
98
+
99
+ ### Operation health coverage
100
+
101
+ Every operation must declare exactly one of:
102
+
103
+ - `healthCheck` — preferred for safe read-only upstream probes.
104
+ - `healthCheckUnsupported` — allowed only when a probe is destructive, paid,
105
+ credential-sensitive, flaky by design, or otherwise unsafe. The `reason` must
106
+ be specific.
107
+
108
+ The generated `ping` operation uses `healthCheckUnsupported` only because it is
109
+ a local scaffold check, not a real upstream API probe.
40
110
 
41
- ```typescript
42
- import { runStandardTests } from '@apifuse/provider-sdk/testing'
43
- runStandardTests(myProvider)
111
+ ### Operation annotations
112
+
113
+ Operations declare non-functional metadata via `annotations`:
114
+
115
+ | Field | Type | Notes |
116
+ |---|---|---|
117
+ | `readOnly` | `boolean` | Operation has no side effects (safe to test in production). |
118
+ | `destructive` | `boolean` | Operation modifies/deletes state. |
119
+ | `idempotent` | `boolean` | Safe to retry without duplicate side effects. |
120
+ | `openWorld` | `boolean` | Callable without authentication. |
121
+ | `rateLimit` | `{ calls, window }` | Per-operation rate hint. `window` is `"minute"\|"hour"\|"day"`. |
122
+ | `timeoutMs` | `number` | Per-operation upstream timeout (1–60000 ms). Omit to inherit the gateway global default. |
123
+
124
+ `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.
125
+
126
+ ## Canonical CLI surface
127
+
128
+ ```bash
129
+ apifuse create <name>
130
+ apifuse dev [path]
131
+ apifuse check [path]
132
+ apifuse record [path] --operation <operation> --params '{"value":"hello"}'
133
+ apifuse test [path]
134
+ apifuse perf <path> --operation <operation>
44
135
  ```
136
+
137
+ ## Scope boundary
138
+
139
+ Generator v1 scaffolds **TypeScript providers only** for this redesign. Python generation remains future work.
140
+
141
+
142
+ ## Boundary
143
+
144
+ 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.
145
+
146
+ External bounty contributors should submit standalone Provider source plus
147
+ `bun run check` / `bun run test` evidence. ApiFuse maintainers own monorepo
148
+ import, registry generation, deployment projection checks, and release
149
+ publishing.
@@ -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(