@apifuse/provider-sdk 2.1.0-beta.9 → 2.2.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 +134 -0
- package/CHANGELOG.md +61 -0
- package/README.md +21 -9
- package/SUBMISSION.md +10 -11
- package/bin/apifuse-pack-check.ts +22 -0
- package/bin/apifuse-submit-check.ts +1747 -388
- package/dist/auth-turn/auth-turn.v1.schema.json +89 -0
- package/dist/auth-turn/fixtures/invalid/empty-kind.json +4 -0
- package/dist/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
- package/dist/auth-turn/fixtures/invalid/missing-kind.json +3 -0
- package/dist/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
- package/dist/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
- package/dist/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
- package/dist/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
- package/dist/auth-turn/fixtures/valid/abort.json +8 -0
- package/dist/auth-turn/fixtures/valid/challenge.json +17 -0
- package/dist/auth-turn/fixtures/valid/complete.json +13 -0
- package/dist/auth-turn/fixtures/valid/form.json +14 -0
- package/dist/auth-turn/fixtures/valid/message.json +13 -0
- package/dist/auth-turn/fixtures/valid/multi_choice.json +15 -0
- package/dist/auth-turn/fixtures/valid/pending.json +5 -0
- package/dist/auth-turn/fixtures/valid/poll.json +9 -0
- package/dist/auth-turn/fixtures/valid/redirect.json +16 -0
- package/dist/auth-turn/fixtures/valid/retry.json +8 -0
- package/dist/auth-turn/fixtures/valid/unknown-kind.json +7 -0
- package/dist/auth-turn/index.d.ts +195 -0
- package/dist/auth-turn/index.js +133 -0
- package/dist/auth.d.ts +76 -0
- package/dist/auth.js +436 -0
- package/dist/ceremonies/index.js +7 -31
- package/dist/cli/create.js +45 -30
- package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
- package/dist/cli/templates/provider/.gitignore.tpl +22 -0
- package/dist/cli/templates/provider/AGENTS.md.tpl +87 -0
- package/dist/cli/templates/provider/CLAUDE.md.tpl +1 -0
- package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
- package/dist/cli/templates/provider/README.md.tpl +163 -0
- package/dist/cli/templates/provider/dev.ts.tpl +5 -0
- package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
- package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
- package/dist/cli/templates/provider/index.ts.tpl +15 -0
- package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
- package/dist/cli/templates/provider/meta.ts.tpl +7 -0
- package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
- package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
- package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
- package/dist/cli/templates/provider/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
- package/dist/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
- package/dist/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
- package/dist/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
- package/dist/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
- package/dist/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
- package/dist/cli/templates/provider/start.ts.tpl +5 -0
- package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
- package/dist/contract.js +1 -0
- package/dist/define.d.ts +6 -1
- package/dist/define.js +140 -70
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/lint.d.ts +1 -0
- package/dist/lint.js +27 -0
- package/dist/provider.d.ts +4 -2
- package/dist/provider.js +2 -1
- package/dist/runtime/auth-flow.js +2 -0
- package/dist/runtime/browser.js +203 -0
- package/dist/runtime/http.js +46 -336
- package/dist/runtime/proxy-retry-policy.d.ts +40 -0
- package/dist/runtime/proxy-retry-policy.js +326 -0
- package/dist/runtime/stealth.d.ts +5 -2
- package/dist/runtime/stealth.js +206 -206
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.js +4 -0
- package/dist/server/self-test-input-tokens.d.ts +1 -0
- package/dist/server/self-test-input-tokens.js +37 -0
- package/dist/server/self-test-redaction.d.ts +20 -0
- package/dist/server/self-test-redaction.js +70 -0
- package/dist/server/self-test-token.d.ts +30 -0
- package/dist/server/self-test-token.js +50 -0
- package/dist/server/self-test.d.ts +98 -0
- package/dist/server/self-test.js +555 -0
- package/dist/server/serve.d.ts +6 -0
- package/dist/server/serve.js +38 -10
- package/dist/server/types.d.ts +1 -0
- package/dist/server/types.js +1 -0
- package/dist/testing/run.js +5 -1
- package/dist/types.d.ts +150 -0
- package/package.json +9 -2
- package/src/auth-turn/auth-turn.v1.schema.json +89 -0
- package/src/auth-turn/fixtures/invalid/empty-kind.json +4 -0
- package/src/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
- package/src/auth-turn/fixtures/invalid/missing-kind.json +3 -0
- package/src/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
- package/src/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
- package/src/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
- package/src/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
- package/src/auth-turn/fixtures/valid/abort.json +8 -0
- package/src/auth-turn/fixtures/valid/challenge.json +17 -0
- package/src/auth-turn/fixtures/valid/complete.json +13 -0
- package/src/auth-turn/fixtures/valid/form.json +14 -0
- package/src/auth-turn/fixtures/valid/message.json +13 -0
- package/src/auth-turn/fixtures/valid/multi_choice.json +15 -0
- package/src/auth-turn/fixtures/valid/pending.json +5 -0
- package/src/auth-turn/fixtures/valid/poll.json +9 -0
- package/src/auth-turn/fixtures/valid/redirect.json +16 -0
- package/src/auth-turn/fixtures/valid/retry.json +8 -0
- package/src/auth-turn/fixtures/valid/unknown-kind.json +7 -0
- package/src/auth-turn/index.ts +177 -0
- package/src/auth.ts +786 -0
- package/src/ceremonies/index.ts +9 -43
- package/src/cli/create.ts +60 -97
- package/src/cli/templates/provider/AGENTS.md.tpl +87 -0
- package/src/cli/templates/provider/CLAUDE.md.tpl +1 -0
- package/src/cli/templates/provider/README.md.tpl +7 -4
- package/src/cli/templates/provider/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
- package/src/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
- package/src/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
- package/src/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
- package/src/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
- package/src/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
- package/src/contract.ts +1 -0
- package/src/define.ts +198 -71
- package/src/index.ts +16 -0
- package/src/lint.ts +33 -0
- package/src/provider.ts +27 -0
- package/src/runtime/auth-flow.ts +2 -0
- package/src/runtime/browser.ts +293 -1
- package/src/runtime/http.ts +84 -530
- package/src/runtime/proxy-retry-policy.ts +469 -0
- package/src/runtime/stealth.ts +269 -353
- package/src/server/index.ts +36 -0
- package/src/server/self-test-input-tokens.ts +46 -0
- package/src/server/self-test-redaction.ts +97 -0
- package/src/server/self-test-token.ts +70 -0
- package/src/server/self-test.ts +725 -0
- package/src/server/serve.ts +75 -6
- package/src/server/types.ts +1 -0
- package/src/testing/run.ts +9 -1
- package/src/types.ts +186 -0
package/dist/server/serve.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { Hono } from "hono";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { AuthAbortError, createAuthFlowHelpers } from "../auth";
|
|
5
6
|
import { AuthError, ProviderError, SessionExpiredError, TransportError, } from "../errors";
|
|
6
7
|
import { loadProviderLocaleCatalogs, localizeAuthTurn, } from "../i18n/catalog";
|
|
7
8
|
import { categoryForStatus, isRetryableCategory, PROVIDER_OBSERVABILITY_TAXONOMY_VERSION, } from "../observability";
|
|
@@ -24,6 +25,8 @@ import { createTraceContext } from "../runtime/trace";
|
|
|
24
25
|
import { parseSchema } from "../schema";
|
|
25
26
|
import { getStealthProfile } from "../stealth/profiles";
|
|
26
27
|
import { APIFUSE_STREAM_DONE_EVENT, APIFUSE_STREAM_ERROR_EVENT, encodeSseEvent, error as streamError, } from "../stream";
|
|
28
|
+
import { createSelfTestApp, createSelfTestInvoke, resolveSelfTestPort, } from "./self-test";
|
|
29
|
+
import { resolveSelfTestMasterSecrets } from "./self-test-token";
|
|
27
30
|
import { AuthFlowRequestSchema, OperationRequestSchema, } from "./types";
|
|
28
31
|
const DEFAULT_HOST = "0.0.0.0";
|
|
29
32
|
const DEFAULT_PORT = 3000;
|
|
@@ -104,7 +107,7 @@ function isProductionProviderBrowserMode(provider, env = process.env) {
|
|
|
104
107
|
return (env.NODE_ENV === "production" && env.APIFUSE__PROVIDER__ID === provider.id);
|
|
105
108
|
}
|
|
106
109
|
export function resolveProviderProxyAffinityKey(provider, request, operationId) {
|
|
107
|
-
const connectionKey = request
|
|
110
|
+
const connectionKey = resolveOperationConnectionId(request) ?? request.connection?.externalRef;
|
|
108
111
|
const affinity = typeof provider.proxy === "object"
|
|
109
112
|
? provider.proxy.session?.affinity
|
|
110
113
|
: undefined;
|
|
@@ -113,6 +116,9 @@ export function resolveProviderProxyAffinityKey(provider, request, operationId)
|
|
|
113
116
|
}
|
|
114
117
|
return connectionKey ?? provider.id;
|
|
115
118
|
}
|
|
119
|
+
function resolveOperationConnectionId(request) {
|
|
120
|
+
return request.connection?.id ?? request.connectionId;
|
|
121
|
+
}
|
|
116
122
|
function createProviderContext(provider, request, operationId, options = {}, state = createUnsupportedProviderRuntimeState(), proxyTelemetry) {
|
|
117
123
|
const baseUrl = getProviderBaseUrl(provider);
|
|
118
124
|
const stealthBaseUrl = getProviderStealthBaseUrl(provider);
|
|
@@ -139,7 +145,7 @@ function createProviderContext(provider, request, operationId, options = {}, sta
|
|
|
139
145
|
values: request.connection?.secrets,
|
|
140
146
|
});
|
|
141
147
|
const requestContext = {
|
|
142
|
-
connectionId: request
|
|
148
|
+
connectionId: resolveOperationConnectionId(request),
|
|
143
149
|
headers: request.headers ?? {},
|
|
144
150
|
};
|
|
145
151
|
const context = wrapWithInstrumentation({
|
|
@@ -209,7 +215,7 @@ function createFlowContextStore(allowedKeys, initialContext = {}) {
|
|
|
209
215
|
},
|
|
210
216
|
};
|
|
211
217
|
}
|
|
212
|
-
function createAuthFlowContext(provider, request, options = {}) {
|
|
218
|
+
function createAuthFlowContext(provider, request, options = {}, signal) {
|
|
213
219
|
const baseUrl = getProviderBaseUrl(provider);
|
|
214
220
|
const stealthBaseUrl = getProviderStealthBaseUrl(provider);
|
|
215
221
|
const stealthProfile = getProviderStealthProfile(provider);
|
|
@@ -251,6 +257,7 @@ function createAuthFlowContext(provider, request, options = {}) {
|
|
|
251
257
|
credential,
|
|
252
258
|
context: flowContextStore.context,
|
|
253
259
|
stt: options.stt ?? createSttClientFromEnv(provider.stt),
|
|
260
|
+
auth: createAuthFlowHelpers({ signal }),
|
|
254
261
|
},
|
|
255
262
|
getPatch: flowContextStore.getPatch,
|
|
256
263
|
};
|
|
@@ -879,14 +886,14 @@ function responseWithProviderTelemetry(response, proxyTelemetry) {
|
|
|
879
886
|
statusText: response.statusText,
|
|
880
887
|
});
|
|
881
888
|
}
|
|
882
|
-
async function handleAuthFlow(provider, request, route, options = {}) {
|
|
889
|
+
async function handleAuthFlow(provider, request, route, options = {}, signal) {
|
|
883
890
|
const flow = provider.auth?.flow;
|
|
884
891
|
if (!flow) {
|
|
885
892
|
throw new ProviderError("Auth flow is not configured", {
|
|
886
893
|
code: "AUTH_FLOW_NOT_CONFIGURED",
|
|
887
894
|
});
|
|
888
895
|
}
|
|
889
|
-
const { context, getPatch } = createAuthFlowContext(provider, request, options);
|
|
896
|
+
const { context, getPatch } = createAuthFlowContext(provider, request, options, signal);
|
|
890
897
|
try {
|
|
891
898
|
const result = route === "start"
|
|
892
899
|
? await flow.start(context)
|
|
@@ -916,6 +923,12 @@ async function handleAuthFlow(provider, request, route, options = {}) {
|
|
|
916
923
|
: result;
|
|
917
924
|
return toAuthFlowResponse(materializedResult, getPatch());
|
|
918
925
|
}
|
|
926
|
+
catch (error) {
|
|
927
|
+
if (error instanceof AuthAbortError) {
|
|
928
|
+
return toAuthFlowResponse(error.turn, getPatch());
|
|
929
|
+
}
|
|
930
|
+
throw error;
|
|
931
|
+
}
|
|
919
932
|
finally {
|
|
920
933
|
context.stealth.close?.();
|
|
921
934
|
}
|
|
@@ -982,7 +995,7 @@ export function createServerApp(provider, options = {}) {
|
|
|
982
995
|
.json()
|
|
983
996
|
.catch(() => undefined);
|
|
984
997
|
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
985
|
-
const response = await handleAuthFlow(provider, body, "start", options);
|
|
998
|
+
const response = await handleAuthFlow(provider, body, "start", options, c.req.raw.signal);
|
|
986
999
|
logProviderSuccess(logger, provider, "auth", "start", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
|
|
987
1000
|
return response instanceof Response ? response : c.json(response);
|
|
988
1001
|
}
|
|
@@ -1002,7 +1015,7 @@ export function createServerApp(provider, options = {}) {
|
|
|
1002
1015
|
.json()
|
|
1003
1016
|
.catch(() => undefined);
|
|
1004
1017
|
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
1005
|
-
const response = await handleAuthFlow(provider, body, "continue", options);
|
|
1018
|
+
const response = await handleAuthFlow(provider, body, "continue", options, c.req.raw.signal);
|
|
1006
1019
|
logProviderSuccess(logger, provider, "auth", "continue", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
|
|
1007
1020
|
return response instanceof Response ? response : c.json(response);
|
|
1008
1021
|
}
|
|
@@ -1022,7 +1035,7 @@ export function createServerApp(provider, options = {}) {
|
|
|
1022
1035
|
.json()
|
|
1023
1036
|
.catch(() => undefined);
|
|
1024
1037
|
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
1025
|
-
const response = await handleAuthFlow(provider, body, "poll", options);
|
|
1038
|
+
const response = await handleAuthFlow(provider, body, "poll", options, c.req.raw.signal);
|
|
1026
1039
|
logProviderSuccess(logger, provider, "auth", "poll", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
|
|
1027
1040
|
return response instanceof Response ? response : c.json(response);
|
|
1028
1041
|
}
|
|
@@ -1042,7 +1055,7 @@ export function createServerApp(provider, options = {}) {
|
|
|
1042
1055
|
.json()
|
|
1043
1056
|
.catch(() => undefined);
|
|
1044
1057
|
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
1045
|
-
const response = await handleAuthFlow(provider, body, "refresh", options);
|
|
1058
|
+
const response = await handleAuthFlow(provider, body, "refresh", options, c.req.raw.signal);
|
|
1046
1059
|
logProviderSuccess(logger, provider, "auth", "refresh", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
|
|
1047
1060
|
return response instanceof Response ? response : c.json(response);
|
|
1048
1061
|
}
|
|
@@ -1062,7 +1075,7 @@ export function createServerApp(provider, options = {}) {
|
|
|
1062
1075
|
.json()
|
|
1063
1076
|
.catch(() => undefined);
|
|
1064
1077
|
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
1065
|
-
const response = await handleAuthFlow(provider, body, "abort", options);
|
|
1078
|
+
const response = await handleAuthFlow(provider, body, "abort", options, c.req.raw.signal);
|
|
1066
1079
|
logProviderSuccess(logger, provider, "auth", "disconnect", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
|
|
1067
1080
|
return response instanceof Response ? response : c.json(response);
|
|
1068
1081
|
}
|
|
@@ -1106,5 +1119,20 @@ export async function serve(provider, options = {}) {
|
|
|
1106
1119
|
hostname: options.host ?? DEFAULT_HOST,
|
|
1107
1120
|
fetch: app.fetch,
|
|
1108
1121
|
});
|
|
1122
|
+
// Internal self-test listener (health dependency inversion): a SEPARATE
|
|
1123
|
+
// socket the tenant-facing gateway never dials. Off by default — it only
|
|
1124
|
+
// starts when the shared self-test master secret env is present.
|
|
1125
|
+
const selfTestSecrets = resolveSelfTestMasterSecrets();
|
|
1126
|
+
if (selfTestSecrets) {
|
|
1127
|
+
const selfTestApp = createSelfTestApp(provider, {
|
|
1128
|
+
secrets: selfTestSecrets,
|
|
1129
|
+
invoke: createSelfTestInvoke(app),
|
|
1130
|
+
});
|
|
1131
|
+
bunRuntime.serve({
|
|
1132
|
+
port: options.selfTestPort ?? resolveSelfTestPort(),
|
|
1133
|
+
hostname: options.host ?? DEFAULT_HOST,
|
|
1134
|
+
fetch: selfTestApp.fetch,
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1109
1137
|
await Promise.resolve();
|
|
1110
1138
|
}
|
package/dist/server/types.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export declare const OperationConnectionSchema: z.ZodObject<{
|
|
|
21
21
|
export declare const OperationRequestSchema: z.ZodObject<{
|
|
22
22
|
requestId: z.ZodString;
|
|
23
23
|
input: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
24
|
+
connectionId: z.ZodOptional<z.ZodString>;
|
|
24
25
|
connection: z.ZodOptional<z.ZodObject<{
|
|
25
26
|
id: z.ZodString;
|
|
26
27
|
mode: z.ZodEnum<{
|
package/dist/server/types.js
CHANGED
|
@@ -17,6 +17,7 @@ export const OperationConnectionSchema = z.object({
|
|
|
17
17
|
export const OperationRequestSchema = z.object({
|
|
18
18
|
requestId: z.string(),
|
|
19
19
|
input: z.record(z.string(), z.unknown()),
|
|
20
|
+
connectionId: z.string().optional(),
|
|
20
21
|
connection: OperationConnectionSchema.optional(),
|
|
21
22
|
headers: z.record(z.string(), z.string()).optional(),
|
|
22
23
|
trace: z.record(z.string(), z.string()).optional(),
|
package/dist/testing/run.js
CHANGED
|
@@ -68,13 +68,17 @@ function inferFixtureDir(providerId) {
|
|
|
68
68
|
return `providers/${providerId}/__fixtures__`;
|
|
69
69
|
}
|
|
70
70
|
function jsonResponse(data) {
|
|
71
|
+
const body = JSON.stringify(data);
|
|
72
|
+
const bodyBytes = new TextEncoder().encode(body);
|
|
71
73
|
return {
|
|
72
74
|
status: 200,
|
|
73
75
|
ok: true,
|
|
74
76
|
headers: {},
|
|
75
77
|
data,
|
|
76
78
|
json: async () => JSON.parse(JSON.stringify(data)),
|
|
77
|
-
text: async () =>
|
|
79
|
+
text: async () => body,
|
|
80
|
+
arrayBuffer: async () => bodyBytes.buffer.slice(bodyBytes.byteOffset, bodyBytes.byteOffset + bodyBytes.byteLength),
|
|
81
|
+
bytes: async () => bodyBytes.slice(0),
|
|
78
82
|
};
|
|
79
83
|
}
|
|
80
84
|
function unsupported(name) {
|
package/dist/types.d.ts
CHANGED
|
@@ -268,8 +268,16 @@ export interface HealthJourneySchedule {
|
|
|
268
268
|
kind: "interval";
|
|
269
269
|
/** ISO 8601 duration, for example PT8H. */
|
|
270
270
|
interval: Iso8601Duration;
|
|
271
|
+
randomize?: HealthScheduleRandomization;
|
|
271
272
|
jitter?: Iso8601Duration;
|
|
272
273
|
}
|
|
274
|
+
export type HealthScheduleRandomization = {
|
|
275
|
+
mode: "centered";
|
|
276
|
+
maxOffset: Iso8601Duration;
|
|
277
|
+
} | {
|
|
278
|
+
mode: "delayed";
|
|
279
|
+
maxDelay: Iso8601Duration;
|
|
280
|
+
};
|
|
273
281
|
export interface HealthJourneyStep {
|
|
274
282
|
id: string;
|
|
275
283
|
description?: string;
|
|
@@ -511,6 +519,9 @@ export interface HealthCheckCase<TInput = unknown, TOutput = unknown> {
|
|
|
511
519
|
export interface HealthCheckSuite<TInput = unknown, TOutput = unknown> {
|
|
512
520
|
/** Polling interval for the suite. All cases share this cadence. */
|
|
513
521
|
interval: ProbeInterval;
|
|
522
|
+
schedule?: {
|
|
523
|
+
randomize?: HealthScheduleRandomization;
|
|
524
|
+
};
|
|
514
525
|
/** Per-case timeout in milliseconds. Default: 30000. */
|
|
515
526
|
timeoutMs?: number;
|
|
516
527
|
/** Default degradation threshold for cases in this suite. Default: runtime threshold. */
|
|
@@ -583,6 +594,13 @@ export interface ProviderHealthMonitorConfig {
|
|
|
583
594
|
*/
|
|
584
595
|
serviceAccount?: string;
|
|
585
596
|
}
|
|
597
|
+
/**
|
|
598
|
+
* New name for `ProviderHealthMonitorConfig` as part of the health dependency
|
|
599
|
+
* inversion (`healthMonitor` → `healthProbe`). Both provider-level fields are
|
|
600
|
+
* accepted transitionally with identical validation; declaring both is a
|
|
601
|
+
* `ValidationError`.
|
|
602
|
+
*/
|
|
603
|
+
export type ProviderHealthProbeConfig = ProviderHealthMonitorConfig;
|
|
586
604
|
export interface HealthMonitorProbeOverride {
|
|
587
605
|
/** Optional runtime interval override as a positive `ms`-style duration string. */
|
|
588
606
|
interval?: ProbeInterval;
|
|
@@ -805,6 +823,7 @@ export type HttpMethod = "HEAD" | "head" | "GET" | "get" | "POST" | "post" | "PU
|
|
|
805
823
|
export interface StealthFetchOptions extends RequestOptions {
|
|
806
824
|
method?: HttpMethod;
|
|
807
825
|
body?: string | Buffer;
|
|
826
|
+
redirect?: "follow" | "manual" | "error";
|
|
808
827
|
/**
|
|
809
828
|
* Offsets policy-managed proxy pool selection for caller-managed retries.
|
|
810
829
|
* Use when a request receives an upstream challenge page rather than a
|
|
@@ -829,9 +848,19 @@ export interface CookieJar {
|
|
|
829
848
|
toString(): string;
|
|
830
849
|
find?(predicate: (cookie: string) => boolean): string | undefined;
|
|
831
850
|
}
|
|
851
|
+
export interface StealthSessionCookies extends CookieJar {
|
|
852
|
+
has(name: string): boolean;
|
|
853
|
+
setFromCookieStrings(cookieStrings: readonly string[]): void;
|
|
854
|
+
toHeader(): string;
|
|
855
|
+
snapshot(): Record<string, string>;
|
|
856
|
+
restore(cookies: Record<string, string>): void;
|
|
857
|
+
clear(): void;
|
|
858
|
+
}
|
|
832
859
|
export interface DeclarativeStealthResponse {
|
|
833
860
|
status: number;
|
|
834
861
|
ok: boolean;
|
|
862
|
+
url?: string;
|
|
863
|
+
redirected?: boolean;
|
|
835
864
|
headers: Record<string, string>;
|
|
836
865
|
rawHeaders: [string, string][];
|
|
837
866
|
body: string;
|
|
@@ -851,8 +880,30 @@ export type RequestWithMethodOptions = RequestOptions & {
|
|
|
851
880
|
method?: string;
|
|
852
881
|
body?: unknown;
|
|
853
882
|
};
|
|
883
|
+
export interface StealthRedirectHop {
|
|
884
|
+
url: string;
|
|
885
|
+
status: number;
|
|
886
|
+
method: string;
|
|
887
|
+
location?: string;
|
|
888
|
+
nextUrl?: string;
|
|
889
|
+
}
|
|
890
|
+
export interface StealthRedirectRunOptions extends Omit<StealthFetchOptions, "redirect"> {
|
|
891
|
+
url: string;
|
|
892
|
+
maxHops?: number;
|
|
893
|
+
stopWhen?: (hop: StealthRedirectHop) => boolean | Promise<boolean>;
|
|
894
|
+
}
|
|
895
|
+
export interface StealthRedirectRunResult {
|
|
896
|
+
final: StealthResponse;
|
|
897
|
+
hops: StealthRedirectHop[];
|
|
898
|
+
reason: "completed" | "stopped" | "max_hops" | "missing_location" | "loop";
|
|
899
|
+
cookies: Record<string, string>;
|
|
900
|
+
}
|
|
854
901
|
export interface StealthSession {
|
|
855
902
|
fetch(url: string, options?: StealthFetchOptions): Promise<StealthResponse>;
|
|
903
|
+
cookies: StealthSessionCookies;
|
|
904
|
+
redirects: {
|
|
905
|
+
run(options: StealthRedirectRunOptions): Promise<StealthRedirectRunResult>;
|
|
906
|
+
};
|
|
856
907
|
close(): void;
|
|
857
908
|
}
|
|
858
909
|
export interface ApiFuseResponse<T> {
|
|
@@ -873,6 +924,8 @@ export interface HttpResponse<T = unknown> {
|
|
|
873
924
|
data: T;
|
|
874
925
|
json<U = T>(): Promise<U>;
|
|
875
926
|
text(): Promise<string>;
|
|
927
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
928
|
+
bytes(): Promise<Uint8Array>;
|
|
876
929
|
}
|
|
877
930
|
export interface HttpStreamResponse {
|
|
878
931
|
status: number;
|
|
@@ -983,6 +1036,32 @@ export interface BrowserFrame {
|
|
|
983
1036
|
evaluate<T>(fn: string | (() => T)): Promise<T>;
|
|
984
1037
|
locator(selector: string): BrowserLocator;
|
|
985
1038
|
}
|
|
1039
|
+
export type BrowserResourceMethod = "GET" | "HEAD";
|
|
1040
|
+
export type BrowserResourceRequest = {
|
|
1041
|
+
readonly url: string;
|
|
1042
|
+
readonly method: BrowserResourceMethod;
|
|
1043
|
+
readonly resourceType?: string;
|
|
1044
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
1045
|
+
};
|
|
1046
|
+
export type BrowserResourceBody = Buffer | Uint8Array | ArrayBuffer | string;
|
|
1047
|
+
export type BrowserResourceDecision = {
|
|
1048
|
+
readonly action: "fulfill";
|
|
1049
|
+
readonly status?: number;
|
|
1050
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
1051
|
+
readonly body?: BrowserResourceBody;
|
|
1052
|
+
} | {
|
|
1053
|
+
readonly action: "block";
|
|
1054
|
+
readonly reason?: string;
|
|
1055
|
+
};
|
|
1056
|
+
export type BrowserResourceRoute = {
|
|
1057
|
+
readonly match: string | RegExp | ((request: BrowserResourceRequest) => boolean);
|
|
1058
|
+
readonly handle: (request: BrowserResourceRequest) => Promise<BrowserResourceDecision> | BrowserResourceDecision;
|
|
1059
|
+
};
|
|
1060
|
+
export type BrowserResourcePolicy = {
|
|
1061
|
+
readonly defaultAction?: "block";
|
|
1062
|
+
readonly allowedMethods?: readonly BrowserResourceMethod[];
|
|
1063
|
+
readonly routes: readonly BrowserResourceRoute[];
|
|
1064
|
+
};
|
|
986
1065
|
export interface BrowserPage extends BrowserFrame {
|
|
987
1066
|
close(): Promise<void>;
|
|
988
1067
|
fill(selector: string, text: string): Promise<void>;
|
|
@@ -997,6 +1076,7 @@ export interface BrowserPage extends BrowserFrame {
|
|
|
997
1076
|
timeout?: number;
|
|
998
1077
|
}): Promise<void>;
|
|
999
1078
|
frames(): Promise<BrowserFrame[]>;
|
|
1079
|
+
withResourcePolicy<T>(policy: BrowserResourcePolicy, run: () => Promise<T>): Promise<T>;
|
|
1000
1080
|
}
|
|
1001
1081
|
export type BrowserChallengeRequest = {
|
|
1002
1082
|
type: "recaptcha";
|
|
@@ -1137,6 +1217,73 @@ export interface ContextScratchpad {
|
|
|
1137
1217
|
toJSON(): Record<string, unknown>;
|
|
1138
1218
|
}
|
|
1139
1219
|
export type FlowContextStore = ContextScratchpad;
|
|
1220
|
+
export type AuthSafeJson = string | number | boolean | null | readonly AuthSafeJson[] | {
|
|
1221
|
+
readonly [key: string]: AuthSafeJson;
|
|
1222
|
+
};
|
|
1223
|
+
export type AuthSafeData = {
|
|
1224
|
+
readonly [key: string]: AuthSafeJson;
|
|
1225
|
+
};
|
|
1226
|
+
export type AuthAbortRetry = "never" | "retry" | "after_user_action";
|
|
1227
|
+
export type AuthAbortData = Record<string, unknown> & {
|
|
1228
|
+
readonly code: string;
|
|
1229
|
+
readonly message?: string;
|
|
1230
|
+
readonly retry?: AuthAbortRetry;
|
|
1231
|
+
readonly actionHint?: AuthSafeJson;
|
|
1232
|
+
readonly fieldErrors?: {
|
|
1233
|
+
readonly [field: string]: string;
|
|
1234
|
+
};
|
|
1235
|
+
readonly details?: AuthSafeData;
|
|
1236
|
+
};
|
|
1237
|
+
export interface AuthFlowTerminalContext {
|
|
1238
|
+
readonly signal?: AbortSignal;
|
|
1239
|
+
readonly deadline?: string;
|
|
1240
|
+
complete<TCredential extends Record<string, string>>(options: {
|
|
1241
|
+
readonly credential: TCredential;
|
|
1242
|
+
readonly metadata?: AuthSafeData;
|
|
1243
|
+
readonly data?: AuthSafeData;
|
|
1244
|
+
readonly turnId?: string;
|
|
1245
|
+
readonly expiresAt?: string;
|
|
1246
|
+
}): AuthTurn;
|
|
1247
|
+
abort(options: {
|
|
1248
|
+
readonly code: string;
|
|
1249
|
+
readonly message?: string;
|
|
1250
|
+
readonly retry?: AuthAbortRetry;
|
|
1251
|
+
readonly actionHint?: AuthSafeJson;
|
|
1252
|
+
readonly fieldErrors?: {
|
|
1253
|
+
readonly [field: string]: string;
|
|
1254
|
+
};
|
|
1255
|
+
readonly data?: AuthSafeData;
|
|
1256
|
+
readonly turnId?: string;
|
|
1257
|
+
readonly expiresAt?: string;
|
|
1258
|
+
}): AuthTurn;
|
|
1259
|
+
nextForm(options: {
|
|
1260
|
+
readonly hintKey?: ProviderLocaleKeyInput;
|
|
1261
|
+
readonly data?: AuthSafeData;
|
|
1262
|
+
readonly turnId?: string;
|
|
1263
|
+
readonly expiresAt?: string;
|
|
1264
|
+
readonly timing?: AuthTurn["timing"];
|
|
1265
|
+
} & ({
|
|
1266
|
+
readonly fields: Record<string, {
|
|
1267
|
+
readonly type?: "string" | "email" | "password" | "otp";
|
|
1268
|
+
readonly labelKey?: ProviderLocaleKeyInput;
|
|
1269
|
+
readonly descriptionKey?: ProviderLocaleKeyInput;
|
|
1270
|
+
readonly placeholderKey?: ProviderLocaleKeyInput;
|
|
1271
|
+
readonly required?: boolean;
|
|
1272
|
+
readonly sensitive?: boolean;
|
|
1273
|
+
}>;
|
|
1274
|
+
readonly expectedInput?: never;
|
|
1275
|
+
} | {
|
|
1276
|
+
readonly expectedInput: Record<string, unknown>;
|
|
1277
|
+
readonly fields?: never;
|
|
1278
|
+
})): AuthTurn;
|
|
1279
|
+
nextPoll(options?: {
|
|
1280
|
+
readonly hintKey?: ProviderLocaleKeyInput;
|
|
1281
|
+
readonly data?: AuthSafeData;
|
|
1282
|
+
readonly turnId?: string;
|
|
1283
|
+
readonly expiresAt?: string;
|
|
1284
|
+
readonly timing?: AuthTurn["timing"];
|
|
1285
|
+
}): AuthTurn;
|
|
1286
|
+
}
|
|
1140
1287
|
export interface FlowContext {
|
|
1141
1288
|
connectionId?: string;
|
|
1142
1289
|
externalRef?: string;
|
|
@@ -1148,6 +1295,7 @@ export interface FlowContext {
|
|
|
1148
1295
|
credential?: CredentialContext;
|
|
1149
1296
|
context: ContextScratchpad;
|
|
1150
1297
|
stt: SttContext;
|
|
1298
|
+
auth: AuthFlowTerminalContext;
|
|
1151
1299
|
}
|
|
1152
1300
|
export interface AuthTurn {
|
|
1153
1301
|
kind: string;
|
|
@@ -1322,5 +1470,7 @@ export interface ProviderDefinition {
|
|
|
1322
1470
|
meta: ProviderMeta;
|
|
1323
1471
|
operations: Record<string, OperationDefinition<SchemaLike, SchemaLike>>;
|
|
1324
1472
|
healthMonitor?: ProviderHealthMonitorConfig;
|
|
1473
|
+
/** Transitional alias for `healthMonitor`; `defineProvider` mirrors both. */
|
|
1474
|
+
healthProbe?: ProviderHealthProbeConfig;
|
|
1325
1475
|
healthJourneys?: readonly HealthJourneyDefinition[];
|
|
1326
1476
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
+
"version": "2.2.0-beta.2",
|
|
2
3
|
"name": "@apifuse/provider-sdk",
|
|
3
|
-
"version": "2.1.0-beta.9",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "APIFuse Provider SDK — Build providers with zero architectural constraints",
|
|
@@ -50,6 +50,12 @@
|
|
|
50
50
|
"import": "./dist/contract.js",
|
|
51
51
|
"default": "./dist/contract.js"
|
|
52
52
|
},
|
|
53
|
+
"./auth-turn": {
|
|
54
|
+
"types": "./dist/auth-turn/index.d.ts",
|
|
55
|
+
"import": "./dist/auth-turn/index.js",
|
|
56
|
+
"default": "./dist/auth-turn/index.js"
|
|
57
|
+
},
|
|
58
|
+
"./auth-turn/auth-turn.v1.schema.json": "./dist/auth-turn/auth-turn.v1.schema.json",
|
|
53
59
|
"./server": {
|
|
54
60
|
"types": "./dist/server/index.d.ts",
|
|
55
61
|
"import": "./dist/server/index.js",
|
|
@@ -77,7 +83,7 @@
|
|
|
77
83
|
"pack:smoke": "bun run build && bun bin/apifuse-pack-smoke.ts",
|
|
78
84
|
"release:guard": "bun scripts/guard-release-pr.ts",
|
|
79
85
|
"format:check": "biome format .",
|
|
80
|
-
"build": "tsgo -p tsconfig.build.json"
|
|
86
|
+
"build": "tsgo -p tsconfig.build.json && rm -rf dist/cli/templates && cp -R src/cli/templates dist/cli/templates && rm -rf dist/auth-turn/fixtures && cp -R src/auth-turn/fixtures dist/auth-turn/fixtures && cp src/auth-turn/auth-turn.v1.schema.json dist/auth-turn/auth-turn.v1.schema.json"
|
|
81
87
|
},
|
|
82
88
|
"devDependencies": {
|
|
83
89
|
"@biomejs/biome": "^2.5.0",
|
|
@@ -88,6 +94,7 @@
|
|
|
88
94
|
"dependencies": {
|
|
89
95
|
"@clack/prompts": "^1.5.1",
|
|
90
96
|
"@types/ms": "^2.1.0",
|
|
97
|
+
"acorn": "^8.17.0",
|
|
91
98
|
"ajv": "^8.17",
|
|
92
99
|
"hono": "^4.12.25",
|
|
93
100
|
"impit": "0.14.1",
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://apifuse.com/contracts/auth-turn/v1",
|
|
4
|
+
"title": "APIFuse AuthTurn envelope v1",
|
|
5
|
+
"description": "Protocol message exchanged during auth.flow ceremonies. kind is an open string; known kinds are tooling metadata, not a wire constraint.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["kind", "turnId"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"kind": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"minLength": 1,
|
|
13
|
+
"description": "Open turn kind. Known kinds are listed in the TURN_KINDS registry; unknown kinds remain valid on the wire."
|
|
14
|
+
},
|
|
15
|
+
"turnId": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"minLength": 1,
|
|
18
|
+
"description": "Provider-scoped identifier of this turn."
|
|
19
|
+
},
|
|
20
|
+
"expiresAt": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"minLength": 1,
|
|
23
|
+
"description": "Turn expiry timestamp as an ISO 8601 / RFC 3339 string."
|
|
24
|
+
},
|
|
25
|
+
"data": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"additionalProperties": true,
|
|
28
|
+
"description": "Kind-specific payload. Terminal kinds carry the payloads described in $defs."
|
|
29
|
+
},
|
|
30
|
+
"expectedInput": {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"additionalProperties": true,
|
|
33
|
+
"description": "JSON Schema describing the input expected next."
|
|
34
|
+
},
|
|
35
|
+
"hint": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"description": "Deprecated but load-bearing human-readable hint materialized from provider locale catalogs."
|
|
38
|
+
},
|
|
39
|
+
"hintKey": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"description": "Provider locale catalog key for the turn hint."
|
|
42
|
+
},
|
|
43
|
+
"timing": {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"additionalProperties": false,
|
|
46
|
+
"description": "Client pacing guidance for poll-style turns.",
|
|
47
|
+
"properties": {
|
|
48
|
+
"suggestedPollIntervalMs": {
|
|
49
|
+
"type": "number",
|
|
50
|
+
"minimum": 1
|
|
51
|
+
},
|
|
52
|
+
"maxWaitMs": {
|
|
53
|
+
"type": "number",
|
|
54
|
+
"minimum": 1
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"$defs": {
|
|
60
|
+
"completeTurnData": {
|
|
61
|
+
"title": "Terminal payload for kind \"complete\"",
|
|
62
|
+
"description": "data payload of a complete turn. The gateway extracts data.credential for persistence; complete turns are never echoed to browsers.",
|
|
63
|
+
"type": "object",
|
|
64
|
+
"additionalProperties": true,
|
|
65
|
+
"required": ["credential"],
|
|
66
|
+
"properties": {
|
|
67
|
+
"credential": {
|
|
68
|
+
"type": "object",
|
|
69
|
+
"additionalProperties": true
|
|
70
|
+
},
|
|
71
|
+
"metadata": {
|
|
72
|
+
"type": "object",
|
|
73
|
+
"additionalProperties": true
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"abortTurnData": {
|
|
78
|
+
"title": "Terminal payload for kind \"abort\"",
|
|
79
|
+
"description": "data payload of an abort turn. code, when present, is the machine-readable abort reason.",
|
|
80
|
+
"type": "object",
|
|
81
|
+
"additionalProperties": true,
|
|
82
|
+
"properties": {
|
|
83
|
+
"code": {
|
|
84
|
+
"type": "string"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"kind": "challenge",
|
|
3
|
+
"turnId": "fixture.challenge",
|
|
4
|
+
"hint": "Complete the WebAuthn prompt in your browser.",
|
|
5
|
+
"expiresAt": "2026-01-01T00:00:00.000Z",
|
|
6
|
+
"data": {
|
|
7
|
+
"challenge": "fixture-challenge-not-real",
|
|
8
|
+
"rpId": "example.com"
|
|
9
|
+
},
|
|
10
|
+
"expectedInput": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"required": ["attestation"],
|
|
13
|
+
"properties": {
|
|
14
|
+
"attestation": { "type": "object" }
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"kind": "form",
|
|
3
|
+
"turnId": "fixture.form",
|
|
4
|
+
"hint": "Provide the required input to continue.",
|
|
5
|
+
"data": {},
|
|
6
|
+
"expectedInput": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"required": ["apiKey"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"apiKey": { "type": "string" }
|
|
11
|
+
},
|
|
12
|
+
"x-apifuse-field-order": ["apiKey"]
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"kind": "message",
|
|
3
|
+
"turnId": "fixture.message",
|
|
4
|
+
"hint": "Enter the code on the verification page, then poll for completion.",
|
|
5
|
+
"data": {
|
|
6
|
+
"user_code": "FIXT-CODE",
|
|
7
|
+
"verification_uri": "https://example.com/device"
|
|
8
|
+
},
|
|
9
|
+
"timing": {
|
|
10
|
+
"suggestedPollIntervalMs": 5000,
|
|
11
|
+
"maxWaitMs": 120000
|
|
12
|
+
}
|
|
13
|
+
}
|