@apifuse/provider-sdk 2.1.0-beta.0 → 2.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUTHORING.md +35 -26
- package/CHANGELOG.md +14 -0
- package/README.md +104 -2
- package/bin/apifuse-check.ts +30 -2
- package/bin/apifuse-dev.ts +30 -6
- package/bin/apifuse-pack-check.ts +90 -0
- package/bin/apifuse-pack-smoke.ts +296 -0
- package/package.json +9 -8
- package/src/ceremonies/index.ts +22 -1
- package/src/cli/create.ts +2 -3
- package/src/cli/templates/provider/README.md.tpl +57 -2
- package/src/cli/templates/provider/index.ts.tpl +4 -1
- package/src/config/loader.ts +61 -1
- package/src/define.ts +44 -6
- 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/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
|
-
}
|