@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.
- package/AUTHORING.md +93 -0
- package/CHANGELOG.md +21 -0
- package/README.md +133 -28
- package/bin/apifuse-check.ts +78 -71
- package/bin/apifuse-create.ts +12 -0
- package/bin/apifuse-dev.ts +24 -61
- package/bin/apifuse-pack-check.ts +87 -0
- package/bin/apifuse-pack-smoke.ts +122 -0
- package/bin/apifuse-perf.ts +33 -32
- package/bin/apifuse-record.ts +17 -7
- package/bin/apifuse-test.ts +6 -4
- package/bin/apifuse.ts +36 -35
- package/package.json +29 -9
- package/src/ceremonies/index.ts +768 -0
- package/src/cli/commands.ts +87 -0
- package/src/cli/create.ts +845 -0
- package/src/cli/templates/provider/Dockerfile.tpl +7 -0
- package/src/cli/templates/provider/README.md.tpl +41 -0
- package/src/cli/templates/provider/dev.ts.tpl +5 -0
- package/src/cli/templates/provider/index.test.ts.tpl +13 -0
- package/src/cli/templates/provider/index.ts.tpl +58 -0
- package/src/cli/templates/provider/start.ts.tpl +5 -0
- package/src/config/loader.ts +61 -1
- package/src/define.ts +565 -41
- package/src/dev.ts +2 -6
- package/src/errors.ts +42 -0
- package/src/index.ts +44 -38
- package/src/lint.ts +574 -0
- package/src/provider.ts +13 -0
- package/src/runtime/auth-flow.ts +67 -0
- package/src/runtime/credential.ts +95 -0
- package/src/runtime/env.ts +13 -0
- package/src/runtime/executor.ts +13 -14
- package/src/runtime/http.ts +36 -12
- package/src/runtime/insights.ts +3 -3
- package/src/runtime/key-derivation.ts +122 -0
- package/src/runtime/keyring.ts +148 -0
- package/src/runtime/namespace.ts +33 -0
- package/src/runtime/prevalidate.ts +252 -0
- package/src/runtime/tls.ts +41 -17
- package/src/runtime/waterfall.ts +0 -1
- package/src/schema.ts +77 -0
- package/src/serve.ts +1 -664
- package/src/server/index.ts +22 -0
- package/src/server/serve.ts +624 -0
- package/src/server/types.ts +78 -0
- package/src/stealth/profiles.ts +10 -93
- package/src/testing/run.ts +391 -32
- package/src/types.ts +390 -41
- package/bin/apifuse-init.ts +0 -387
- package/src/__tests__/auth.test.ts +0 -396
- package/src/__tests__/browser-auth.test.ts +0 -180
- package/src/__tests__/browser.test.ts +0 -632
- package/src/__tests__/define.test.ts +0 -225
- package/src/__tests__/errors.test.ts +0 -69
- package/src/__tests__/executor.test.ts +0 -214
- package/src/__tests__/http.test.ts +0 -238
- package/src/__tests__/insights.test.ts +0 -210
- package/src/__tests__/instrumentation.test.ts +0 -290
- package/src/__tests__/otlp.test.ts +0 -141
- package/src/__tests__/perf.test.ts +0 -60
- package/src/__tests__/providers-yaml.test.ts +0 -135
- package/src/__tests__/proxy.test.ts +0 -359
- package/src/__tests__/recipes.test.ts +0 -36
- package/src/__tests__/serve.test.ts +0 -233
- package/src/__tests__/session.test.ts +0 -231
- package/src/__tests__/state.test.ts +0 -100
- package/src/__tests__/stealth.test.ts +0 -57
- package/src/__tests__/testing.test.ts +0 -97
- package/src/__tests__/tls.test.ts +0 -345
- package/src/__tests__/types.test.ts +0 -142
- package/src/__tests__/utils.test.ts +0 -62
- package/src/__tests__/waterfall.test.ts +0 -270
- package/src/config/providers-yaml.ts +0 -370
- package/src/index.test.ts +0 -1
- package/src/protocol.ts +0 -183
- package/src/runtime/auth.ts +0 -245
- package/src/runtime/session.ts +0 -573
- 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 —
|
|
3
|
+
ApiFuse Provider SDK — build provider declarations and runtimes with one public SDK surface and one canonical CLI.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
bun add @apifuse/provider-sdk
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
For external bounty scaffolding, use the beta tag until this release is promoted
|
|
12
|
+
to `latest`:
|
|
12
13
|
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
```bash
|
|
15
|
+
bunx @apifuse/provider-sdk@beta create my-provider --yes
|
|
16
|
+
```
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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.
|
package/bin/apifuse-check.ts
CHANGED
|
@@ -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/
|
|
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:
|
|
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
|
-
|
|
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 (!
|
|
169
|
+
if (!hasSchemaParser(operation.input)) {
|
|
181
170
|
failures.push(`${operationId}: missing input schema`);
|
|
182
171
|
}
|
|
183
172
|
|
|
184
|
-
if (!
|
|
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
|
|
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
|
|
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
|
|
269
|
-
manifestPath: string,
|
|
257
|
+
function checkProviderMetadata(
|
|
270
258
|
provider: ProviderDefinition | undefined,
|
|
271
259
|
): CheckResult {
|
|
272
|
-
if (!
|
|
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: "
|
|
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
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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(
|
|
398
|
-
|
|
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
|
+
}
|
package/bin/apifuse-dev.ts
CHANGED
|
@@ -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 {
|
|
16
|
+
import type { BrowserClient, ProviderContext } from "../src/types";
|
|
18
17
|
|
|
19
18
|
const HELP_TEXT = `Usage: apifuse dev [path]
|
|
20
|
-
Example: apifuse dev providers/
|
|
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}/
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
authManager: AuthManager,
|
|
61
|
-
): { ctx: ProviderContext } {
|
|
60
|
+
export function createProviderContext(provider: ProviderDefinition): {
|
|
61
|
+
ctx: ProviderContext;
|
|
62
|
+
} {
|
|
62
63
|
const ctx: ProviderContext = {
|
|
63
|
-
|
|
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():
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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(
|