@apifuse/provider-sdk 2.1.0-beta.0 → 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 +14 -23
- package/CHANGELOG.md +7 -0
- package/README.md +35 -2
- package/bin/apifuse-pack-check.ts +40 -0
- package/bin/apifuse-pack-smoke.ts +122 -0
- package/package.json +5 -4
- package/src/ceremonies/index.ts +22 -1
- package/src/cli/templates/provider/README.md.tpl +14 -1
- package/src/cli/templates/provider/index.ts.tpl +4 -0
- package/src/config/loader.ts +61 -1
- package/src/define.ts +43 -5
- package/src/index.ts +0 -6
- package/src/provider.ts +0 -1
- package/src/runtime/http.ts +27 -11
- package/src/runtime/tls.ts +21 -12
- package/src/server/serve.ts +17 -3
- package/src/types.ts +28 -2
- package/src/composite.ts +0 -43
package/AUTHORING.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
- Canonical scaffolding command: `apifuse create`
|
|
4
4
|
- Monorepo contributors should use `apifuse create <name> --preset monorepo`
|
|
5
|
-
- Standalone
|
|
5
|
+
- Standalone bounty contributors should use `bunx @apifuse/provider-sdk@beta create <name> --yes` until this release is promoted to `latest`
|
|
6
6
|
- Provider server contract is:
|
|
7
7
|
- dev default `3900`
|
|
8
8
|
- start/Docker/container `3000`
|
|
@@ -47,6 +47,7 @@ description:
|
|
|
47
47
|
- `description` — 150+ chars English (error-level rule)
|
|
48
48
|
- Every Zod field in input AND output has `.describe()` including nested objects + array items (error-level rule)
|
|
49
49
|
- `fixtures.request` + `fixtures.response` both present (error-level rule)
|
|
50
|
+
- Exactly one of `healthCheck` or `healthCheckUnsupported` per operation. Prefer `healthCheck` for safe read-only upstream probes; use `healthCheckUnsupported` only with a specific reason for destructive, paid, credential-sensitive, flaky, or otherwise unsafe probes.
|
|
50
51
|
|
|
51
52
|
### Factored operations
|
|
52
53
|
|
|
@@ -62,31 +63,21 @@ Use `defineOperation()` when an operation is large enough to live beside helper
|
|
|
62
63
|
|
|
63
64
|
- `annotations`: `{ readOnly, destructive, idempotent, openWorld, rateLimit }` — agentic safety signals
|
|
64
65
|
- `tags`: operation-level semantic tags for retrieval (e.g., `["weather", "korea", "realtime"]`)
|
|
65
|
-
- `relatedOperations`: `{ alternatives?: string[]
|
|
66
|
+
- `relatedOperations`: `{ alternatives?: string[] }` — links to fallback/sibling operations
|
|
66
67
|
|
|
67
|
-
###
|
|
68
|
+
### External bounty submission evidence
|
|
68
69
|
|
|
69
|
-
|
|
70
|
+
External contributors are expected to submit standalone Provider source plus:
|
|
70
71
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
```
|
|
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.
|
|
90
81
|
|
|
91
82
|
### Running the lint locally
|
|
92
83
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# @apifuse/provider-sdk Changelog
|
|
2
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
|
+
|
|
3
10
|
## 2.1.0-beta.0
|
|
4
11
|
|
|
5
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`.
|
package/README.md
CHANGED
|
@@ -8,12 +8,19 @@ ApiFuse Provider SDK — build provider declarations and runtimes with one publi
|
|
|
8
8
|
bun add @apifuse/provider-sdk
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
For external bounty scaffolding, use the beta tag until this release is promoted
|
|
12
|
+
to `latest`:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
bunx @apifuse/provider-sdk@beta create my-provider --yes
|
|
16
|
+
```
|
|
17
|
+
|
|
11
18
|
## Create a provider
|
|
12
19
|
|
|
13
20
|
### Standalone (default)
|
|
14
21
|
|
|
15
22
|
```bash
|
|
16
|
-
bunx @apifuse/provider-sdk create my-provider
|
|
23
|
+
bunx @apifuse/provider-sdk@beta create my-provider --yes
|
|
17
24
|
```
|
|
18
25
|
|
|
19
26
|
The canonical `create` flow:
|
|
@@ -49,9 +56,18 @@ Removed legacy runtime paths are not supported:
|
|
|
49
56
|
|
|
50
57
|
```bash
|
|
51
58
|
cd my-provider
|
|
52
|
-
bun run dev
|
|
53
59
|
bun run check
|
|
54
60
|
bun run test
|
|
61
|
+
bun run dev
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Smoke the generated local server:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
curl -s http://localhost:3900/health
|
|
68
|
+
curl -s -X POST http://localhost:3900/v1/ping \
|
|
69
|
+
-H 'Content-Type: application/json' \
|
|
70
|
+
-d '{"input":{"value":"hello"},"headers":{},"connection":null}'
|
|
55
71
|
```
|
|
56
72
|
|
|
57
73
|
## Authoring ergonomics
|
|
@@ -80,6 +96,18 @@ export default defineProvider({
|
|
|
80
96
|
|
|
81
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`.
|
|
82
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.
|
|
110
|
+
|
|
83
111
|
### Operation annotations
|
|
84
112
|
|
|
85
113
|
Operations declare non-functional metadata via `annotations`:
|
|
@@ -114,3 +142,8 @@ Generator v1 scaffolds **TypeScript providers only** for this redesign. Python g
|
|
|
114
142
|
## Boundary
|
|
115
143
|
|
|
116
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.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
4
5
|
import { z } from "zod";
|
|
5
6
|
|
|
6
7
|
const PACK_RESULT_SCHEMA = z.array(
|
|
@@ -28,12 +29,23 @@ if (!first) {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
const filePaths = (first.files ?? []).map((file) => file.path);
|
|
32
|
+
const requiredPaths = [
|
|
33
|
+
"bin/apifuse.ts",
|
|
34
|
+
"bin/apifuse-create.ts",
|
|
35
|
+
"bin/apifuse-pack-smoke.ts",
|
|
36
|
+
"src/cli/create.ts",
|
|
37
|
+
"src/cli/templates/provider/index.ts.tpl",
|
|
38
|
+
"src/cli/templates/provider/README.md.tpl",
|
|
39
|
+
];
|
|
31
40
|
const forbiddenMatches = filePaths.filter(
|
|
32
41
|
(path) =>
|
|
33
42
|
path.startsWith("src/__tests__/") ||
|
|
34
43
|
path === "src/index.test.ts" ||
|
|
35
44
|
path === "bin/apifuse-init.ts",
|
|
36
45
|
);
|
|
46
|
+
const missingRequiredPaths = requiredPaths.filter(
|
|
47
|
+
(path) => !filePaths.includes(path),
|
|
48
|
+
);
|
|
37
49
|
|
|
38
50
|
if (forbiddenMatches.length > 0) {
|
|
39
51
|
throw new Error(
|
|
@@ -41,6 +53,34 @@ if (forbiddenMatches.length > 0) {
|
|
|
41
53
|
);
|
|
42
54
|
}
|
|
43
55
|
|
|
56
|
+
if (missingRequiredPaths.length > 0) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Packed artifact is missing required public SDK files:\n${missingRequiredPaths.join("\n")}`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const packageJsonInput: unknown = JSON.parse(
|
|
63
|
+
readFileSync("package.json", "utf8"),
|
|
64
|
+
);
|
|
65
|
+
const packageJson = z
|
|
66
|
+
.object({
|
|
67
|
+
dependencies: z.record(z.string(), z.string()).optional(),
|
|
68
|
+
devDependencies: z.record(z.string(), z.string()).optional(),
|
|
69
|
+
})
|
|
70
|
+
.parse(packageJsonInput);
|
|
71
|
+
|
|
72
|
+
if (!packageJson.dependencies?.["@clack/prompts"]) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
"@clack/prompts is imported by the public create CLI and must be listed in dependencies.",
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (packageJson.devDependencies?.["@clack/prompts"]) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
"@clack/prompts must not be devDependency-only because the published create CLI imports it at runtime.",
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
44
84
|
console.log(`Packed artifact OK: ${first.filename}`);
|
|
45
85
|
for (const filePath of filePaths) {
|
|
46
86
|
console.log(` - ${filePath}`);
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
4
|
+
import {
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
mkdtempSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join, resolve } from "node:path";
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
|
|
15
|
+
const PACK_RESULT_SCHEMA = z.array(
|
|
16
|
+
z.object({
|
|
17
|
+
filename: z.string(),
|
|
18
|
+
}),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const KEEP_TEMP = process.env.APIFUSE_PACK_SMOKE_KEEP_TEMP === "1";
|
|
22
|
+
|
|
23
|
+
const tempRoot = mkdtempSync(
|
|
24
|
+
join(tmpdir(), "apifuse-provider-sdk-pack-smoke-"),
|
|
25
|
+
);
|
|
26
|
+
const packDir = join(tempRoot, "pack");
|
|
27
|
+
const consumerDir = join(tempRoot, "consumer");
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
mkdirSync(packDir, { recursive: true });
|
|
31
|
+
mkdirSync(consumerDir, { recursive: true });
|
|
32
|
+
|
|
33
|
+
const packed = packSdk(packDir);
|
|
34
|
+
const tarballPath = resolve(packDir, packed.filename);
|
|
35
|
+
const tarballSpecifier = `file:${tarballPath}`;
|
|
36
|
+
|
|
37
|
+
writeFileSync(
|
|
38
|
+
join(consumerDir, "package.json"),
|
|
39
|
+
`${JSON.stringify(
|
|
40
|
+
{
|
|
41
|
+
private: true,
|
|
42
|
+
type: "module",
|
|
43
|
+
dependencies: {
|
|
44
|
+
"@apifuse/provider-sdk": tarballSpecifier,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
null,
|
|
48
|
+
2,
|
|
49
|
+
)}\n`,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
run("bun", ["install"], consumerDir);
|
|
53
|
+
|
|
54
|
+
const cliBin = join(consumerDir, "node_modules", ".bin", "apifuse");
|
|
55
|
+
if (!existsSync(cliBin)) {
|
|
56
|
+
throw new Error(`Expected CLI bin at ${cliBin}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
run(
|
|
60
|
+
"bun",
|
|
61
|
+
[
|
|
62
|
+
cliBin,
|
|
63
|
+
"create",
|
|
64
|
+
"dx-smoke",
|
|
65
|
+
"--yes",
|
|
66
|
+
"--json",
|
|
67
|
+
"--sdk-specifier",
|
|
68
|
+
tarballSpecifier,
|
|
69
|
+
],
|
|
70
|
+
consumerDir,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const generatedProviderDir = join(consumerDir, "dx-smoke");
|
|
74
|
+
run("bun", ["run", "check"], generatedProviderDir);
|
|
75
|
+
run("bun", ["run", "test"], generatedProviderDir);
|
|
76
|
+
|
|
77
|
+
console.log(
|
|
78
|
+
`Provider SDK packed-artifact smoke passed: ${tarballPath} -> ${generatedProviderDir}`,
|
|
79
|
+
);
|
|
80
|
+
} finally {
|
|
81
|
+
if (KEEP_TEMP) {
|
|
82
|
+
console.log(`Keeping smoke temp directory: ${tempRoot}`);
|
|
83
|
+
} else {
|
|
84
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function packSdk(destination: string): { filename: string } {
|
|
89
|
+
const raw = execFileSync(
|
|
90
|
+
"npm",
|
|
91
|
+
["pack", "--json", "--pack-destination", destination],
|
|
92
|
+
{
|
|
93
|
+
cwd: process.cwd(),
|
|
94
|
+
encoding: "utf8",
|
|
95
|
+
stdio: ["ignore", "pipe", "inherit"],
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
const parsed = PACK_RESULT_SCHEMA.parse(JSON.parse(raw));
|
|
99
|
+
const first = parsed[0];
|
|
100
|
+
if (!first) {
|
|
101
|
+
throw new Error("npm pack --json returned no package metadata.");
|
|
102
|
+
}
|
|
103
|
+
return first;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function run(command: string, args: string[], cwd: string): void {
|
|
107
|
+
const result = spawnSync(command, args, {
|
|
108
|
+
cwd,
|
|
109
|
+
env: process.env,
|
|
110
|
+
stdio: "inherit",
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (result.error) {
|
|
114
|
+
throw result.error;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (result.status !== 0) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`Command failed (${[command, ...args].join(" ")}) in ${cwd} with exit code ${result.status}`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apifuse/provider-sdk",
|
|
3
|
-
"version": "2.1.0-beta.
|
|
3
|
+
"version": "2.1.0-beta.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
|
-
"description": "ApiFuse Provider SDK
|
|
6
|
+
"description": "ApiFuse Provider SDK — Build providers with zero architectural constraints",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"main": "./src/index.ts",
|
|
9
9
|
"types": "./src/index.ts",
|
|
@@ -61,16 +61,17 @@
|
|
|
61
61
|
"type-check": "tsgo --noEmit",
|
|
62
62
|
"test": "bun test",
|
|
63
63
|
"check": "bun run lint && bun run type-check",
|
|
64
|
-
"pack:check": "bun bin/apifuse-pack-check.ts"
|
|
64
|
+
"pack:check": "bun bin/apifuse-pack-check.ts",
|
|
65
|
+
"pack:smoke": "bun bin/apifuse-pack-smoke.ts"
|
|
65
66
|
},
|
|
66
67
|
"devDependencies": {
|
|
67
68
|
"@biomejs/biome": "^2.4.12",
|
|
68
|
-
"@clack/prompts": "^1.2.0",
|
|
69
69
|
"@types/bun": "latest",
|
|
70
70
|
"@types/node": "^25.1.0",
|
|
71
71
|
"typescript": "^6.0.3"
|
|
72
72
|
},
|
|
73
73
|
"dependencies": {
|
|
74
|
+
"@clack/prompts": "^1.2.0",
|
|
74
75
|
"ajv": "^8.17",
|
|
75
76
|
"hono": "^4.12.14",
|
|
76
77
|
"playwright": "^1.55.1",
|
package/src/ceremonies/index.ts
CHANGED
|
@@ -63,6 +63,7 @@ const DEVICE_FLOW_KEY = "__device_flow";
|
|
|
63
63
|
const MAGIC_LINK_KEY = "__magic_link";
|
|
64
64
|
const COMBINED_STAGE_KEY = "__combined_stage";
|
|
65
65
|
const SWITCH_SELECTION_KEY = "__switch_selection";
|
|
66
|
+
const FORM_FIELD_ORDER_EXTENSION = "x-apifuse-field-order";
|
|
66
67
|
|
|
67
68
|
function isRecord(value: unknown): value is JsonObject {
|
|
68
69
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
@@ -175,11 +176,31 @@ function buildJsonSchemaForm(
|
|
|
175
176
|
): AuthTurn {
|
|
176
177
|
return createTurn("form", {
|
|
177
178
|
hint,
|
|
178
|
-
expectedInput,
|
|
179
|
+
expectedInput: withDeclaredFormFieldOrder(expectedInput),
|
|
179
180
|
data: {},
|
|
180
181
|
});
|
|
181
182
|
}
|
|
182
183
|
|
|
184
|
+
function withDeclaredFormFieldOrder(expectedInput: JsonObject): JsonObject {
|
|
185
|
+
const properties = expectedInput.properties;
|
|
186
|
+
if (!isRecord(properties)) {
|
|
187
|
+
return expectedInput;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const existingOrder = expectedInput[FORM_FIELD_ORDER_EXTENSION];
|
|
191
|
+
if (
|
|
192
|
+
Array.isArray(existingOrder) &&
|
|
193
|
+
existingOrder.every((value) => typeof value === "string")
|
|
194
|
+
) {
|
|
195
|
+
return expectedInput;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
...expectedInput,
|
|
200
|
+
[FORM_FIELD_ORDER_EXTENSION]: Object.keys(properties),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
183
204
|
export function validateCeremonyOutput(turn: unknown): AuthTurn {
|
|
184
205
|
if (!validateAuthTurn(turn)) {
|
|
185
206
|
const detail = validateAuthTurn.errors
|
|
@@ -25,4 +25,17 @@ bun run test
|
|
|
25
25
|
|
|
26
26
|
1. Replace the sample `ping` operation with real upstream logic.
|
|
27
27
|
2. Record a real fixture with `bun run record:sample` or a provider-specific equivalent.
|
|
28
|
-
3.
|
|
28
|
+
3. Replace the starter `healthCheckUnsupported` with a real `healthCheck` for read-only upstream operations when safe.
|
|
29
|
+
4. Extend tests and operation metadata until the provider is bounty-ready.
|
|
30
|
+
|
|
31
|
+
## Health-check authorship
|
|
32
|
+
|
|
33
|
+
Every operation must declare exactly one of:
|
|
34
|
+
|
|
35
|
+
- `healthCheck` — preferred for safe read-only upstream probes.
|
|
36
|
+
- `healthCheckUnsupported` — allowed only when a probe is destructive, paid,
|
|
37
|
+
credential-sensitive, flaky by design, or otherwise unsafe. Use a specific
|
|
38
|
+
reason; reviewers reject placeholder reasons such as "TODO" or "later".
|
|
39
|
+
|
|
40
|
+
The generated `ping` operation uses `healthCheckUnsupported` only because it is
|
|
41
|
+
a local scaffold check, not a real upstream API probe.
|
|
@@ -49,6 +49,10 @@ export default defineProvider({
|
|
|
49
49
|
request: { value: "hello" },
|
|
50
50
|
response: { ok: true, message: "{{DISPLAY_NAME}} received: hello" },
|
|
51
51
|
},
|
|
52
|
+
healthCheckUnsupported: {
|
|
53
|
+
reason:
|
|
54
|
+
"Generated local-only scaffold operation. Replace this with a real healthCheck for upstream-backed bounty operations when safe; keep healthCheckUnsupported only for destructive, paid, credential-sensitive, or otherwise unprobeable operations with a specific rationale.",
|
|
55
|
+
},
|
|
52
56
|
},
|
|
53
57
|
},
|
|
54
58
|
});
|
package/src/config/loader.ts
CHANGED
|
@@ -43,7 +43,67 @@ export type ResolvedProxyConfig = {
|
|
|
43
43
|
|
|
44
44
|
function normalizeProxyUrl(url?: string): string | undefined {
|
|
45
45
|
const normalized = url?.trim();
|
|
46
|
-
return normalized ? normalized : undefined;
|
|
46
|
+
return normalized ? applyStickyProxySession(normalized) : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function applyStickyProxySession(proxyUrl: string): string {
|
|
50
|
+
let parsed: URL;
|
|
51
|
+
try {
|
|
52
|
+
parsed = new URL(proxyUrl);
|
|
53
|
+
} catch {
|
|
54
|
+
return proxyUrl;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!parsed.hostname || !parsed.username || !parsed.password) {
|
|
58
|
+
return proxyUrl;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const host = parsed.hostname.toLowerCase();
|
|
62
|
+
if (!host.includes("smartproxy") && !host.includes("decodo")) {
|
|
63
|
+
return proxyUrl;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const username = decodeURIComponent(parsed.username);
|
|
67
|
+
const sessionId =
|
|
68
|
+
process.env.APIFUSE_PROXY_SESSION_ID?.trim() || "apifuse-shared";
|
|
69
|
+
const sessionDuration =
|
|
70
|
+
process.env.APIFUSE_PROXY_SESSION_DURATION?.trim() || undefined;
|
|
71
|
+
const stickyUsername = host.includes("smartproxy")
|
|
72
|
+
? buildSmartproxyUsername(username, sessionId, sessionDuration)
|
|
73
|
+
: buildDecodoUsername(username, sessionId, sessionDuration ?? "60");
|
|
74
|
+
|
|
75
|
+
parsed.username = stickyUsername;
|
|
76
|
+
return parsed.toString();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function buildSmartproxyUsername(
|
|
80
|
+
username: string,
|
|
81
|
+
sessionId: string,
|
|
82
|
+
sessionDuration?: string,
|
|
83
|
+
): string {
|
|
84
|
+
const parts = username.split("_");
|
|
85
|
+
const configuredLife = parts
|
|
86
|
+
.find((part) => part.startsWith("life-"))
|
|
87
|
+
?.slice("life-".length);
|
|
88
|
+
const baseUsername = parts
|
|
89
|
+
.filter((part) => !part.startsWith("session-") && !part.startsWith("life-"))
|
|
90
|
+
.join("_");
|
|
91
|
+
return `${baseUsername}_session-${sessionId}_life-${sessionDuration ?? configuredLife ?? "60"}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function buildDecodoUsername(
|
|
95
|
+
username: string,
|
|
96
|
+
sessionId: string,
|
|
97
|
+
sessionDuration: string,
|
|
98
|
+
): string {
|
|
99
|
+
const withoutSticky = username.replace(
|
|
100
|
+
/-session-.+-sessionduration-\d+$/,
|
|
101
|
+
"",
|
|
102
|
+
);
|
|
103
|
+
const baseUsername = withoutSticky.startsWith("user-")
|
|
104
|
+
? withoutSticky
|
|
105
|
+
: `user-${withoutSticky}`;
|
|
106
|
+
return `${baseUsername}-session-${sessionId}-sessionduration-${sessionDuration}`;
|
|
47
107
|
}
|
|
48
108
|
|
|
49
109
|
function syncProxyEnv(config: ApiFuseConfig): void {
|
package/src/define.ts
CHANGED
|
@@ -233,8 +233,10 @@ const HEALTH_CHECK_CASE_FIELDS = new Set([
|
|
|
233
233
|
const HEALTH_CHECK_UNSUPPORTED_FIELDS = new Set(["reason", "trackedIn"]);
|
|
234
234
|
const PROVIDER_HEALTH_MONITOR_FIELDS = new Set([
|
|
235
235
|
"requiredSecrets",
|
|
236
|
+
"probeOverrides",
|
|
236
237
|
"serviceAccount",
|
|
237
238
|
]);
|
|
239
|
+
const PROVIDER_HEALTH_MONITOR_PROBE_OVERRIDE_FIELDS = new Set(["interval"]);
|
|
238
240
|
|
|
239
241
|
function levenshtein(a: string, b: string): number {
|
|
240
242
|
const m = a.length;
|
|
@@ -307,13 +309,13 @@ function validateProviderHealthMonitor(
|
|
|
307
309
|
fix: `Set healthMonitor to { requiredSecrets?: string[]; serviceAccount?: string }`,
|
|
308
310
|
},
|
|
309
311
|
);
|
|
312
|
+
const healthMonitorRecord = Object.fromEntries(Object.entries(healthMonitor));
|
|
310
313
|
rejectUnknownFields(
|
|
311
|
-
|
|
314
|
+
healthMonitorRecord,
|
|
312
315
|
PROVIDER_HEALTH_MONITOR_FIELDS,
|
|
313
316
|
"healthMonitor",
|
|
314
317
|
);
|
|
315
|
-
const requiredSecrets =
|
|
316
|
-
.requiredSecrets;
|
|
318
|
+
const requiredSecrets = healthMonitorRecord.requiredSecrets;
|
|
317
319
|
if (requiredSecrets !== undefined) {
|
|
318
320
|
if (!Array.isArray(requiredSecrets))
|
|
319
321
|
throw new ValidationError(
|
|
@@ -326,8 +328,44 @@ function validateProviderHealthMonitor(
|
|
|
326
328
|
);
|
|
327
329
|
}
|
|
328
330
|
}
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
+
const probeOverrides = healthMonitorRecord.probeOverrides;
|
|
332
|
+
if (probeOverrides !== undefined) {
|
|
333
|
+
if (
|
|
334
|
+
!probeOverrides ||
|
|
335
|
+
typeof probeOverrides !== "object" ||
|
|
336
|
+
Array.isArray(probeOverrides)
|
|
337
|
+
)
|
|
338
|
+
throw new ValidationError(
|
|
339
|
+
`Provider "${providerId}" has invalid healthMonitor.probeOverrides: must be an object keyed by probe id.`,
|
|
340
|
+
);
|
|
341
|
+
for (const [probeId, override] of Object.entries(probeOverrides)) {
|
|
342
|
+
if (probeId.length === 0)
|
|
343
|
+
throw new ValidationError(
|
|
344
|
+
`Provider "${providerId}" has invalid healthMonitor.probeOverrides key: must be a non-empty probe id.`,
|
|
345
|
+
);
|
|
346
|
+
if (!override || typeof override !== "object" || Array.isArray(override))
|
|
347
|
+
throw new ValidationError(
|
|
348
|
+
`Provider "${providerId}" has invalid healthMonitor.probeOverrides["${probeId}"]: must be an object.`,
|
|
349
|
+
);
|
|
350
|
+
const overrideRecord = Object.fromEntries(Object.entries(override));
|
|
351
|
+
rejectUnknownFields(
|
|
352
|
+
overrideRecord,
|
|
353
|
+
PROVIDER_HEALTH_MONITOR_PROBE_OVERRIDE_FIELDS,
|
|
354
|
+
`healthMonitor.probeOverrides["${probeId}"]`,
|
|
355
|
+
);
|
|
356
|
+
const interval = overrideRecord.interval;
|
|
357
|
+
const validProbeIntervals: readonly string[] = PROBE_INTERVALS;
|
|
358
|
+
if (
|
|
359
|
+
interval !== undefined &&
|
|
360
|
+
(typeof interval !== "string" ||
|
|
361
|
+
!validProbeIntervals.includes(interval))
|
|
362
|
+
)
|
|
363
|
+
throw new ValidationError(
|
|
364
|
+
`Provider "${providerId}" has invalid healthMonitor.probeOverrides["${probeId}"].interval: must be one of ${PROBE_INTERVALS.join(", ")}.`,
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const serviceAccount = healthMonitorRecord.serviceAccount;
|
|
331
369
|
if (
|
|
332
370
|
serviceAccount !== undefined &&
|
|
333
371
|
(typeof serviceAccount !== "string" || serviceAccount.length === 0)
|
package/src/index.ts
CHANGED
|
@@ -2,12 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
export { z } from "zod";
|
|
4
4
|
export * from "./ceremonies";
|
|
5
|
-
export {
|
|
6
|
-
type ClarifyResponse,
|
|
7
|
-
type CompositeContext,
|
|
8
|
-
type CompositeOperationDefinition,
|
|
9
|
-
defineCompositeOperation,
|
|
10
|
-
} from "./composite";
|
|
11
5
|
export type {
|
|
12
6
|
ApiFuseConfig,
|
|
13
7
|
BrowserConfig,
|
package/src/provider.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export { z } from "zod";
|
|
2
2
|
|
|
3
3
|
export { createFormCeremony } from "./ceremonies";
|
|
4
|
-
export { defineCompositeOperation } from "./composite";
|
|
5
4
|
export { defineOperation, defineProvider } from "./define";
|
|
6
5
|
export { AuthError, ProviderError, ValidationError } from "./errors";
|
|
7
6
|
export type {
|
package/src/runtime/http.ts
CHANGED
|
@@ -83,13 +83,12 @@ async function doRequest(
|
|
|
83
83
|
});
|
|
84
84
|
|
|
85
85
|
if (!response.ok) {
|
|
86
|
-
|
|
86
|
+
await drainFetchResponse(response);
|
|
87
87
|
throw new TransportError(
|
|
88
|
-
`
|
|
88
|
+
`Upstream request failed with status ${response.status}`,
|
|
89
89
|
{
|
|
90
90
|
code: "upstream_http_error",
|
|
91
91
|
status: response.status,
|
|
92
|
-
fix: `Check the endpoint URL and request parameters. Response: ${text.slice(0, 200)}`,
|
|
93
92
|
},
|
|
94
93
|
);
|
|
95
94
|
}
|
|
@@ -121,16 +120,12 @@ async function doRequest(
|
|
|
121
120
|
}
|
|
122
121
|
|
|
123
122
|
if (error instanceof Error && error.name === "AbortError") {
|
|
124
|
-
throw new TransportError(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
code: "transport_timeout",
|
|
128
|
-
fix: `Increase timeout option (current: ${timeout}ms)`,
|
|
129
|
-
},
|
|
130
|
-
);
|
|
123
|
+
throw new TransportError("Request timed out", {
|
|
124
|
+
code: "transport_timeout",
|
|
125
|
+
});
|
|
131
126
|
}
|
|
132
127
|
|
|
133
|
-
throw new TransportError(
|
|
128
|
+
throw new TransportError("Network error", {
|
|
134
129
|
code: "transport_network_error",
|
|
135
130
|
cause: error instanceof Error ? error : undefined,
|
|
136
131
|
});
|
|
@@ -141,6 +136,27 @@ async function doRequest(
|
|
|
141
136
|
}
|
|
142
137
|
}
|
|
143
138
|
|
|
139
|
+
async function drainFetchResponse(response: Response): Promise<void> {
|
|
140
|
+
const reader = response.body?.getReader();
|
|
141
|
+
if (!reader) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
while (true) {
|
|
147
|
+
const { done } = await reader.read();
|
|
148
|
+
if (done) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
// Best-effort drain for transport reuse only; callers still receive the
|
|
154
|
+
// sanitized upstream error below.
|
|
155
|
+
} finally {
|
|
156
|
+
reader.releaseLock();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
144
160
|
function createProxyInit(
|
|
145
161
|
proxy?: string,
|
|
146
162
|
): Pick<FetchProxyInit, "dispatcher" | "proxy"> {
|
package/src/runtime/tls.ts
CHANGED
|
@@ -19,6 +19,12 @@ const MISSING_PROXY_WARNING =
|
|
|
19
19
|
|
|
20
20
|
export type TlsClientOptions = ProxyResolutionOptions & {
|
|
21
21
|
warn?: (message: string) => void;
|
|
22
|
+
/**
|
|
23
|
+
* Proxy-only TLS transport overrides. Use only for upstream proxy products
|
|
24
|
+
* that terminate CONNECT with a private CA instead of tunneling the origin
|
|
25
|
+
* certificate chain.
|
|
26
|
+
*/
|
|
27
|
+
proxyTls?: { insecureSkipVerify?: boolean };
|
|
22
28
|
};
|
|
23
29
|
|
|
24
30
|
const REMOVED_CHROME_PROFILE_NAMES = new Set([
|
|
@@ -187,14 +193,6 @@ export function normalizeResponse(
|
|
|
187
193
|
};
|
|
188
194
|
}
|
|
189
195
|
|
|
190
|
-
function getErrorMessage(error: unknown): string {
|
|
191
|
-
if (error instanceof Error) {
|
|
192
|
-
return error.toString();
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return String(error);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
196
|
function normalizeBody(body: TlsFetchOptions["body"]): string | null {
|
|
199
197
|
if (body === undefined) {
|
|
200
198
|
return null;
|
|
@@ -274,6 +272,7 @@ function createSessionFetcher(
|
|
|
274
272
|
let sessionClient: SessionClient | null = null;
|
|
275
273
|
let activeProxy: string | undefined;
|
|
276
274
|
let activeTlsIdentifier: string | undefined;
|
|
275
|
+
let activeInsecureSkipVerify = false;
|
|
277
276
|
let hasWarnedMissingProxy = false;
|
|
278
277
|
const warn = clientOptions.warn ?? console.warn;
|
|
279
278
|
|
|
@@ -305,19 +304,22 @@ function createSessionFetcher(
|
|
|
305
304
|
sessionClient = null;
|
|
306
305
|
activeProxy = undefined;
|
|
307
306
|
activeTlsIdentifier = undefined;
|
|
307
|
+
activeInsecureSkipVerify = false;
|
|
308
308
|
}
|
|
309
309
|
|
|
310
310
|
function getSessionClient(
|
|
311
311
|
profile?: string,
|
|
312
312
|
proxy?: string,
|
|
313
313
|
ja3?: string,
|
|
314
|
+
insecureSkipVerify = false,
|
|
314
315
|
): SessionClient {
|
|
315
316
|
const tlsIdentifier = ja3 ?? resolveIdentifier(profile ?? defaultProfile);
|
|
316
317
|
|
|
317
318
|
if (
|
|
318
319
|
!sessionClient ||
|
|
319
320
|
activeProxy !== proxy ||
|
|
320
|
-
activeTlsIdentifier !== tlsIdentifier
|
|
321
|
+
activeTlsIdentifier !== tlsIdentifier ||
|
|
322
|
+
activeInsecureSkipVerify !== insecureSkipVerify
|
|
321
323
|
) {
|
|
322
324
|
if (sessionClient) {
|
|
323
325
|
closeCurrentSession();
|
|
@@ -326,10 +328,12 @@ function createSessionFetcher(
|
|
|
326
328
|
sessionClient = new SessionClient(moduleClient, {
|
|
327
329
|
tlsClientIdentifier: tlsIdentifier,
|
|
328
330
|
...(proxy ? { proxyUrl: proxy } : {}),
|
|
331
|
+
...(insecureSkipVerify ? { insecureSkipVerify: true } : {}),
|
|
329
332
|
timeoutSeconds: 30,
|
|
330
333
|
} as ConstructorParameters<typeof SessionClient>[1]);
|
|
331
334
|
activeProxy = proxy;
|
|
332
335
|
activeTlsIdentifier = tlsIdentifier;
|
|
336
|
+
activeInsecureSkipVerify = insecureSkipVerify;
|
|
333
337
|
}
|
|
334
338
|
|
|
335
339
|
return sessionClient;
|
|
@@ -338,10 +342,15 @@ function createSessionFetcher(
|
|
|
338
342
|
return {
|
|
339
343
|
async fetch(url, options = {}) {
|
|
340
344
|
const proxy = resolveRequestProxy(options);
|
|
345
|
+
const insecureSkipVerify = Boolean(
|
|
346
|
+
options.tls?.insecureSkipVerify ??
|
|
347
|
+
(proxy && clientOptions.proxyTls?.insecureSkipVerify),
|
|
348
|
+
);
|
|
341
349
|
const session = getSessionClient(
|
|
342
350
|
options.profile,
|
|
343
351
|
proxy,
|
|
344
352
|
options.tls?.ja3,
|
|
353
|
+
insecureSkipVerify,
|
|
345
354
|
);
|
|
346
355
|
const requestUrl = resolveUrl(baseUrl, url);
|
|
347
356
|
|
|
@@ -352,9 +361,9 @@ function createSessionFetcher(
|
|
|
352
361
|
proxy,
|
|
353
362
|
});
|
|
354
363
|
|
|
355
|
-
if (response.status >= 400) {
|
|
364
|
+
if (response.status >= 400 && options.throwOnHttpError !== false) {
|
|
356
365
|
throw new TransportError(
|
|
357
|
-
`
|
|
366
|
+
`Upstream request failed with status ${response.status}`,
|
|
358
367
|
{
|
|
359
368
|
status: response.status,
|
|
360
369
|
},
|
|
@@ -367,7 +376,7 @@ function createSessionFetcher(
|
|
|
367
376
|
throw error;
|
|
368
377
|
}
|
|
369
378
|
|
|
370
|
-
throw new TransportError(
|
|
379
|
+
throw new TransportError("Network error", {
|
|
371
380
|
status: 0,
|
|
372
381
|
cause: error instanceof Error ? error : undefined,
|
|
373
382
|
});
|
package/src/server/serve.ts
CHANGED
|
@@ -215,9 +215,8 @@ function toErrorResponse(
|
|
|
215
215
|
return {
|
|
216
216
|
error: {
|
|
217
217
|
code: error.code ?? "provider_error",
|
|
218
|
-
message: error
|
|
218
|
+
message: publicProviderErrorMessage(error),
|
|
219
219
|
...(requestId ? { requestId } : {}),
|
|
220
|
-
...(error.fix ? { fix: error.fix } : {}),
|
|
221
220
|
...(error instanceof TransportError && error.status
|
|
222
221
|
? { details: { upstreamStatus: error.status } }
|
|
223
222
|
: {}),
|
|
@@ -245,6 +244,21 @@ function toErrorResponse(
|
|
|
245
244
|
};
|
|
246
245
|
}
|
|
247
246
|
|
|
247
|
+
function publicProviderErrorMessage(error: ProviderError): string {
|
|
248
|
+
if (error instanceof TransportError) {
|
|
249
|
+
if (error.code === "transport_timeout") return "Request timed out";
|
|
250
|
+
if (error.code === "transport_network_error") return "Network error";
|
|
251
|
+
if (error.code === "upstream_http_error" && error.status) {
|
|
252
|
+
return `Upstream request failed with status ${error.status}`;
|
|
253
|
+
}
|
|
254
|
+
if (error.status) {
|
|
255
|
+
return `Upstream request failed with status ${error.status}`;
|
|
256
|
+
}
|
|
257
|
+
return "Upstream request failed";
|
|
258
|
+
}
|
|
259
|
+
return error.message;
|
|
260
|
+
}
|
|
261
|
+
|
|
248
262
|
function toStatusCode(error: unknown): 400 | 404 | 500 | 502 | 504 {
|
|
249
263
|
if (error instanceof z.ZodError) {
|
|
250
264
|
return 400;
|
|
@@ -255,7 +269,7 @@ function toStatusCode(error: unknown): 400 | 404 | 500 | 502 | 504 {
|
|
|
255
269
|
}
|
|
256
270
|
|
|
257
271
|
if (error instanceof ProviderError) {
|
|
258
|
-
if (error.code === "NOT_FOUND") {
|
|
272
|
+
if (error.code === "NOT_FOUND" || error.code === "NO_DATA") {
|
|
259
273
|
return 404;
|
|
260
274
|
}
|
|
261
275
|
|
package/src/types.ts
CHANGED
|
@@ -82,7 +82,6 @@ export const OPERATION_TIMEOUT_MS_MAX = 60_000;
|
|
|
82
82
|
|
|
83
83
|
export interface OperationRelationships {
|
|
84
84
|
alternatives?: string[];
|
|
85
|
-
chainsWith?: string[];
|
|
86
85
|
}
|
|
87
86
|
|
|
88
87
|
/**
|
|
@@ -97,7 +96,15 @@ export interface OperationRelationships {
|
|
|
97
96
|
*/
|
|
98
97
|
|
|
99
98
|
/** Polling intervals supported by the health-monitor runtime. */
|
|
100
|
-
export type ProbeInterval =
|
|
99
|
+
export type ProbeInterval =
|
|
100
|
+
| "30s"
|
|
101
|
+
| "1m"
|
|
102
|
+
| "3m"
|
|
103
|
+
| "5m"
|
|
104
|
+
| "15m"
|
|
105
|
+
| "30m"
|
|
106
|
+
| "1h"
|
|
107
|
+
| "24h";
|
|
101
108
|
|
|
102
109
|
export const PROBE_INTERVALS: readonly ProbeInterval[] = [
|
|
103
110
|
"30s",
|
|
@@ -107,6 +114,7 @@ export const PROBE_INTERVALS: readonly ProbeInterval[] = [
|
|
|
107
114
|
"15m",
|
|
108
115
|
"30m",
|
|
109
116
|
"1h",
|
|
117
|
+
"24h",
|
|
110
118
|
] as const;
|
|
111
119
|
|
|
112
120
|
/**
|
|
@@ -222,6 +230,13 @@ export interface ProviderHealthMonitorConfig {
|
|
|
222
230
|
* `requiresConnection: true`.
|
|
223
231
|
*/
|
|
224
232
|
requiredSecrets?: string[];
|
|
233
|
+
/**
|
|
234
|
+
* Runtime probe overrides keyed by probe id (for example
|
|
235
|
+
* "catchtable/auth-flow" or "catchtable/waiting-lifecycle"). Use this for
|
|
236
|
+
* health-monitor probes that are provider-scoped or cross-operation and
|
|
237
|
+
* therefore cannot declare an `OperationDefinition.healthCheck.interval`.
|
|
238
|
+
*/
|
|
239
|
+
probeOverrides?: Record<string, HealthMonitorProbeOverride>;
|
|
225
240
|
/**
|
|
226
241
|
* Override the default service account ID for this provider's probes.
|
|
227
242
|
* Defaults to the runtime's `APIFUSE_SERVICE_ACCOUNT_ID` env var.
|
|
@@ -229,6 +244,11 @@ export interface ProviderHealthMonitorConfig {
|
|
|
229
244
|
serviceAccount?: string;
|
|
230
245
|
}
|
|
231
246
|
|
|
247
|
+
export interface HealthMonitorProbeOverride {
|
|
248
|
+
/** Optional runtime interval override. Must be one of PROBE_INTERVALS. */
|
|
249
|
+
interval?: ProbeInterval;
|
|
250
|
+
}
|
|
251
|
+
|
|
232
252
|
export interface OperationErrorCode {
|
|
233
253
|
code: string;
|
|
234
254
|
status?: number;
|
|
@@ -300,9 +320,15 @@ export interface TlsFetchOptions extends RequestOptions {
|
|
|
300
320
|
method?: string;
|
|
301
321
|
body?: string | Buffer;
|
|
302
322
|
profile?: string;
|
|
323
|
+
/**
|
|
324
|
+
* Defaults to true. Set to false when callers need to inspect upstream
|
|
325
|
+
* non-2xx bodies themselves instead of converting them to TransportError.
|
|
326
|
+
*/
|
|
327
|
+
throwOnHttpError?: boolean;
|
|
303
328
|
tls?: {
|
|
304
329
|
ja3?: string;
|
|
305
330
|
h2?: Record<string, unknown>;
|
|
331
|
+
insecureSkipVerify?: boolean;
|
|
306
332
|
};
|
|
307
333
|
headerOrder?: string[];
|
|
308
334
|
}
|
package/src/composite.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { infer as ZodInfer, ZodType } from "zod";
|
|
2
|
-
|
|
3
|
-
export interface ClarifyResponse {
|
|
4
|
-
_type: "clarify";
|
|
5
|
-
question: string;
|
|
6
|
-
missing: Array<{ name: string; description: string }>;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface CompositeContext {
|
|
10
|
-
chain: {
|
|
11
|
-
call: <T>(operationKey: string, params: unknown) => Promise<T>;
|
|
12
|
-
};
|
|
13
|
-
clarify: (opts: {
|
|
14
|
-
question: string;
|
|
15
|
-
missing: Array<{ name: string; description: string }>;
|
|
16
|
-
}) => ClarifyResponse;
|
|
17
|
-
setSlot: (name: string, value: unknown) => void;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface CompositeOperationDefinition<
|
|
21
|
-
TInput extends ZodType = ZodType,
|
|
22
|
-
TOutput extends ZodType = ZodType,
|
|
23
|
-
> {
|
|
24
|
-
id: string;
|
|
25
|
-
description: string;
|
|
26
|
-
input: TInput;
|
|
27
|
-
output: TOutput;
|
|
28
|
-
tags?: string[];
|
|
29
|
-
chainsWith?: string[];
|
|
30
|
-
steps: (
|
|
31
|
-
ctx: CompositeContext,
|
|
32
|
-
input: ZodInfer<TInput>,
|
|
33
|
-
) => Promise<ZodInfer<TOutput> | ClarifyResponse>;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function defineCompositeOperation<
|
|
37
|
-
TInput extends ZodType,
|
|
38
|
-
TOutput extends ZodType,
|
|
39
|
-
>(
|
|
40
|
-
config: CompositeOperationDefinition<TInput, TOutput>,
|
|
41
|
-
): CompositeOperationDefinition<TInput, TOutput> {
|
|
42
|
-
return config;
|
|
43
|
-
}
|