@apifuse/provider-sdk 2.1.0-beta.0 → 2.1.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/AUTHORING.md +218 -21
  2. package/CHANGELOG.md +54 -0
  3. package/README.md +147 -10
  4. package/SUBMISSION.md +87 -0
  5. package/bin/apifuse-check.ts +86 -4
  6. package/bin/apifuse-dev.ts +87 -13
  7. package/bin/apifuse-pack-check.ts +120 -0
  8. package/bin/apifuse-pack-smoke.ts +423 -0
  9. package/bin/apifuse-perf.ts +142 -49
  10. package/bin/apifuse-record.ts +182 -104
  11. package/bin/apifuse-submit-check.ts +2538 -0
  12. package/bin/apifuse.ts +1 -1
  13. package/dist/ceremonies/index.d.ts +41 -0
  14. package/dist/ceremonies/index.js +490 -0
  15. package/dist/choice-token.d.ts +24 -0
  16. package/dist/choice-token.js +74 -0
  17. package/dist/cli/commands.d.ts +10 -0
  18. package/dist/cli/commands.js +80 -0
  19. package/dist/cli/create.d.ts +47 -0
  20. package/dist/cli/create.js +762 -0
  21. package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
  22. package/dist/cli/templates/provider/.gitignore.tpl +22 -0
  23. package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
  24. package/dist/cli/templates/provider/README.md.tpl +160 -0
  25. package/dist/cli/templates/provider/dev.ts.tpl +5 -0
  26. package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
  27. package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
  28. package/dist/cli/templates/provider/index.ts.tpl +15 -0
  29. package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
  30. package/dist/cli/templates/provider/meta.ts.tpl +7 -0
  31. package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
  32. package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
  33. package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  34. package/dist/cli/templates/provider/start.ts.tpl +5 -0
  35. package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
  36. package/dist/config/loader.d.ts +107 -0
  37. package/dist/config/loader.js +935 -0
  38. package/dist/contract-json.d.ts +9 -0
  39. package/dist/contract-json.js +51 -0
  40. package/dist/contract-serialization.d.ts +4 -0
  41. package/dist/contract-serialization.js +78 -0
  42. package/dist/contract-types.d.ts +49 -0
  43. package/dist/contract-types.js +1 -0
  44. package/dist/contract.d.ts +6 -0
  45. package/dist/contract.js +155 -0
  46. package/dist/define.d.ts +97 -0
  47. package/dist/define.js +1320 -0
  48. package/dist/dev.d.ts +9 -0
  49. package/dist/dev.js +15 -0
  50. package/dist/errors.d.ts +59 -0
  51. package/dist/errors.js +97 -0
  52. package/dist/i18n/catalog.d.ts +29 -0
  53. package/dist/i18n/catalog.js +159 -0
  54. package/dist/i18n/index.d.ts +2 -0
  55. package/dist/i18n/index.js +2 -0
  56. package/dist/i18n/keys.d.ts +10 -0
  57. package/dist/i18n/keys.js +34 -0
  58. package/dist/index.d.ts +41 -0
  59. package/dist/index.js +37 -0
  60. package/dist/lint.d.ts +73 -0
  61. package/dist/lint.js +702 -0
  62. package/dist/observability.d.ts +5 -0
  63. package/dist/observability.js +39 -0
  64. package/dist/provider.d.ts +9 -0
  65. package/dist/provider.js +8 -0
  66. package/dist/public-schema-field-lint.d.ts +2 -0
  67. package/dist/public-schema-field-lint.js +158 -0
  68. package/dist/recipes/gov-api.d.ts +19 -0
  69. package/dist/recipes/gov-api.js +72 -0
  70. package/dist/recipes/rest-api.d.ts +21 -0
  71. package/dist/recipes/rest-api.js +115 -0
  72. package/dist/runtime/auth-flow.d.ts +14 -0
  73. package/dist/runtime/auth-flow.js +44 -0
  74. package/dist/runtime/browser.d.ts +25 -0
  75. package/dist/runtime/browser.js +1034 -0
  76. package/dist/runtime/cache.d.ts +10 -0
  77. package/dist/runtime/cache.js +372 -0
  78. package/dist/runtime/choice.d.ts +15 -0
  79. package/dist/runtime/choice.js +435 -0
  80. package/dist/runtime/credential.d.ts +8 -0
  81. package/dist/runtime/credential.js +61 -0
  82. package/dist/runtime/env.d.ts +2 -0
  83. package/dist/runtime/env.js +10 -0
  84. package/dist/runtime/executor.d.ts +16 -0
  85. package/dist/runtime/executor.js +51 -0
  86. package/dist/runtime/http.d.ts +8 -0
  87. package/dist/runtime/http.js +706 -0
  88. package/dist/runtime/insights.d.ts +9 -0
  89. package/dist/runtime/insights.js +324 -0
  90. package/dist/runtime/instrumentation.d.ts +8 -0
  91. package/dist/runtime/instrumentation.js +269 -0
  92. package/dist/runtime/key-derivation.d.ts +24 -0
  93. package/dist/runtime/key-derivation.js +73 -0
  94. package/dist/runtime/keyring.d.ts +25 -0
  95. package/dist/runtime/keyring.js +93 -0
  96. package/dist/runtime/namespace.d.ts +9 -0
  97. package/dist/runtime/namespace.js +19 -0
  98. package/dist/runtime/otlp.d.ts +39 -0
  99. package/dist/runtime/otlp.js +103 -0
  100. package/dist/runtime/perf.d.ts +12 -0
  101. package/dist/runtime/perf.js +52 -0
  102. package/dist/runtime/prevalidate.d.ts +12 -0
  103. package/dist/runtime/prevalidate.js +173 -0
  104. package/dist/runtime/provider.d.ts +2 -0
  105. package/dist/runtime/provider.js +11 -0
  106. package/dist/runtime/proxy-errors.d.ts +21 -0
  107. package/dist/runtime/proxy-errors.js +83 -0
  108. package/dist/runtime/proxy-telemetry.d.ts +8 -0
  109. package/dist/runtime/proxy-telemetry.js +174 -0
  110. package/dist/runtime/redis.d.ts +17 -0
  111. package/dist/runtime/redis.js +82 -0
  112. package/dist/runtime/request-options.d.ts +3 -0
  113. package/dist/runtime/request-options.js +42 -0
  114. package/dist/runtime/state.d.ts +17 -0
  115. package/dist/runtime/state.js +344 -0
  116. package/dist/runtime/stealth.d.ts +18 -0
  117. package/dist/runtime/stealth.js +834 -0
  118. package/dist/runtime/stt.d.ts +22 -0
  119. package/dist/runtime/stt.js +480 -0
  120. package/dist/runtime/trace.d.ts +26 -0
  121. package/dist/runtime/trace.js +142 -0
  122. package/dist/runtime/waterfall.d.ts +12 -0
  123. package/dist/runtime/waterfall.js +147 -0
  124. package/dist/schema.d.ts +74 -0
  125. package/dist/schema.js +243 -0
  126. package/dist/serve.d.ts +1 -0
  127. package/dist/serve.js +1 -0
  128. package/dist/server/index.d.ts +3 -0
  129. package/dist/server/index.js +2 -0
  130. package/dist/server/serve.d.ts +64 -0
  131. package/dist/server/serve.js +1110 -0
  132. package/dist/server/types.d.ts +136 -0
  133. package/dist/server/types.js +86 -0
  134. package/dist/stealth/profiles.d.ts +4 -0
  135. package/dist/stealth/profiles.js +259 -0
  136. package/dist/stream.d.ts +44 -0
  137. package/dist/stream.js +151 -0
  138. package/dist/testing/helpers.d.ts +23 -0
  139. package/dist/testing/helpers.js +95 -0
  140. package/dist/testing/index.d.ts +2 -0
  141. package/dist/testing/index.js +2 -0
  142. package/dist/testing/run.d.ts +34 -0
  143. package/dist/testing/run.js +303 -0
  144. package/dist/types.d.ts +1326 -0
  145. package/dist/types.js +61 -0
  146. package/dist/utils/date.d.ts +6 -0
  147. package/dist/utils/date.js +101 -0
  148. package/dist/utils/parse.d.ts +16 -0
  149. package/dist/utils/parse.js +51 -0
  150. package/dist/utils/text.d.ts +4 -0
  151. package/dist/utils/text.js +14 -0
  152. package/dist/utils/transform.d.ts +8 -0
  153. package/dist/utils/transform.js +48 -0
  154. package/package.json +57 -29
  155. package/src/ceremonies/index.ts +30 -3
  156. package/src/choice-token.ts +165 -0
  157. package/src/cli/commands.ts +34 -11
  158. package/src/cli/create.ts +214 -52
  159. package/src/cli/templates/provider/.dockerignore.tpl +22 -0
  160. package/src/cli/templates/provider/.gitignore.tpl +22 -0
  161. package/src/cli/templates/provider/README.md.tpl +134 -2
  162. package/src/cli/templates/provider/dev.ts.tpl +1 -1
  163. package/src/cli/templates/provider/domain/README.md.tpl +3 -0
  164. package/src/cli/templates/provider/index.ts.tpl +5 -44
  165. package/src/cli/templates/provider/mappers/README.md.tpl +3 -0
  166. package/src/cli/templates/provider/meta.ts.tpl +7 -0
  167. package/src/cli/templates/provider/operations/index.ts.tpl +5 -0
  168. package/src/cli/templates/provider/operations/ping.ts.tpl +24 -0
  169. package/src/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  170. package/src/cli/templates/provider/start.ts.tpl +1 -1
  171. package/src/cli/templates/provider/upstream/README.md.tpl +3 -0
  172. package/src/config/loader.ts +1282 -7
  173. package/src/contract-json.ts +75 -0
  174. package/src/contract-serialization.ts +89 -0
  175. package/src/contract-types.ts +52 -0
  176. package/src/contract.ts +215 -0
  177. package/src/define.ts +1726 -48
  178. package/src/errors.ts +27 -0
  179. package/src/i18n/catalog.ts +277 -0
  180. package/src/i18n/index.ts +2 -0
  181. package/src/i18n/keys.ts +64 -0
  182. package/src/index.ts +174 -15
  183. package/src/lint.ts +547 -73
  184. package/src/observability.ts +41 -0
  185. package/src/provider.ts +104 -5
  186. package/src/public-schema-field-lint.ts +237 -0
  187. package/src/runtime/auth-flow.ts +7 -0
  188. package/src/runtime/browser.ts +762 -51
  189. package/src/runtime/cache.ts +528 -0
  190. package/src/runtime/choice.ts +760 -0
  191. package/src/runtime/executor.ts +32 -3
  192. package/src/runtime/http.ts +945 -185
  193. package/src/runtime/insights.ts +11 -11
  194. package/src/runtime/instrumentation.ts +12 -4
  195. package/src/runtime/key-derivation.ts +1 -1
  196. package/src/runtime/keyring.ts +4 -3
  197. package/src/runtime/proxy-errors.ts +132 -0
  198. package/src/runtime/proxy-telemetry.ts +253 -0
  199. package/src/runtime/redis.ts +116 -0
  200. package/src/runtime/request-options.ts +66 -0
  201. package/src/runtime/state.ts +563 -0
  202. package/src/runtime/stealth.ts +1159 -0
  203. package/src/runtime/stt.ts +629 -0
  204. package/src/runtime/trace.ts +1 -1
  205. package/src/schema.ts +363 -1
  206. package/src/server/serve.ts +1172 -76
  207. package/src/server/types.ts +37 -0
  208. package/src/stream.ts +210 -0
  209. package/src/testing/run.ts +31 -5
  210. package/src/types.ts +1118 -44
  211. package/src/composite.ts +0 -43
  212. package/src/runtime/tls.ts +0 -425
  213. package/src/types/playwright-stealth.d.ts +0 -9
package/bin/apifuse.ts CHANGED
@@ -28,7 +28,7 @@ await module.main();
28
28
 
29
29
  function printHelp() {
30
30
  console.log(`
31
- apifuse - ApiFuse Provider SDK CLI
31
+ apifuse - APIFuse Provider SDK CLI
32
32
 
33
33
  Commands:`);
34
34
  for (const name of COMMAND_ORDER) {
@@ -0,0 +1,41 @@
1
+ import type { AuthFlowDefinition, AuthTurn } from "../types";
2
+ type JsonObject = Record<string, unknown>;
3
+ export declare function validateCeremonyOutput(turn: unknown): AuthTurn;
4
+ export declare function createOAuth2Ceremony(options: {
5
+ authorizeUrl: string;
6
+ tokenUrl: string;
7
+ clientIdEnvKey: string;
8
+ clientSecretEnvKey: string;
9
+ scopes: string[];
10
+ usePKCE?: boolean;
11
+ }): AuthFlowDefinition;
12
+ export declare function createDeviceFlowCeremony(options: {
13
+ deviceCodeUrl: string;
14
+ tokenUrl: string;
15
+ clientIdEnvKey: string;
16
+ clientSecretEnvKey?: string;
17
+ scopes: string[];
18
+ }): AuthFlowDefinition;
19
+ export declare function createWebAuthnCeremony(options: {
20
+ rpId: string;
21
+ challengeUrl?: string;
22
+ verifyUrl?: string;
23
+ timeoutMs?: number;
24
+ }): AuthFlowDefinition;
25
+ export declare function createMagicLinkCeremony(options: {
26
+ sendUrl: string;
27
+ verifyUrl: string;
28
+ emailField?: string;
29
+ expiresInMs?: number;
30
+ }): AuthFlowDefinition;
31
+ export declare function createFormCeremony(options: {
32
+ schema: JsonObject;
33
+ hint?: string;
34
+ mapCredential?: (input: Record<string, unknown>) => JsonObject;
35
+ }): AuthFlowDefinition;
36
+ export declare function combineCeremonies(...ceremonies: AuthFlowDefinition[]): AuthFlowDefinition;
37
+ export declare function createSwitchCeremony(options: {
38
+ choices: Record<string, AuthFlowDefinition>;
39
+ prompt?: string;
40
+ }): AuthFlowDefinition;
41
+ export {};
@@ -0,0 +1,490 @@
1
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
2
+ import Ajv from "ajv";
3
+ import { FlowExpiredError, ProviderSecretError, TurnValidationError, ValidationError, } from "../errors";
4
+ const ajv = new Ajv({ allErrors: true, strict: true, strictSchema: true });
5
+ const authTurnSchema = {
6
+ type: "object",
7
+ additionalProperties: false,
8
+ required: ["kind", "turnId"],
9
+ properties: {
10
+ kind: { type: "string", minLength: 1 },
11
+ turnId: { type: "string", minLength: 1 },
12
+ expiresAt: { type: "string", minLength: 1 },
13
+ data: {
14
+ type: "object",
15
+ additionalProperties: true,
16
+ },
17
+ expectedInput: {
18
+ type: "object",
19
+ additionalProperties: true,
20
+ },
21
+ hint: { type: "string" },
22
+ hintKey: { type: "string" },
23
+ timing: {
24
+ type: "object",
25
+ additionalProperties: false,
26
+ properties: {
27
+ suggestedPollIntervalMs: { type: "number", minimum: 1 },
28
+ maxWaitMs: { type: "number", minimum: 1 },
29
+ },
30
+ },
31
+ },
32
+ };
33
+ const validateAuthTurn = ajv.compile(authTurnSchema);
34
+ const OAUTH2_STATE_KEY = "__oauth2_state";
35
+ const OAUTH2_PKCE_VERIFIER_KEY = "__oauth2_pkce_verifier";
36
+ const DEVICE_FLOW_KEY = "__device_flow";
37
+ const MAGIC_LINK_KEY = "__magic_link";
38
+ const COMBINED_STAGE_KEY = "__combined_stage";
39
+ const SWITCH_SELECTION_KEY = "__switch_selection";
40
+ const FORM_FIELD_ORDER_EXTENSION = "x-apifuse-field-order";
41
+ function isRecord(value) {
42
+ return !!value && typeof value === "object" && !Array.isArray(value);
43
+ }
44
+ function ensureRecord(value) {
45
+ return isRecord(value) ? value : {};
46
+ }
47
+ function createTurn(kind, options = {}) {
48
+ return {
49
+ kind,
50
+ turnId: randomUUID(),
51
+ ...options,
52
+ };
53
+ }
54
+ function createExpiresAt(ttlMs) {
55
+ return new Date(Date.now() + ttlMs).toISOString();
56
+ }
57
+ function getRequiredEnv(ctx, key) {
58
+ const value = ctx.env.get(key);
59
+ if (!value) {
60
+ throw new ProviderSecretError(`Missing required secret: ${key}`);
61
+ }
62
+ return value;
63
+ }
64
+ function toBase64Url(input) {
65
+ return input
66
+ .toString("base64")
67
+ .replace(/\+/g, "-")
68
+ .replace(/\//g, "_")
69
+ .replace(/=+$/g, "");
70
+ }
71
+ function createCodeVerifier() {
72
+ return toBase64Url(randomBytes(32));
73
+ }
74
+ function createCodeChallenge(verifier) {
75
+ return createHash("sha256").update(verifier).digest("base64url");
76
+ }
77
+ function normalizeError(error) {
78
+ if (error instanceof Error) {
79
+ return error;
80
+ }
81
+ return new Error("Unexpected ceremony error");
82
+ }
83
+ function toRetryTurn(error, hint) {
84
+ const normalizedError = normalizeError(error);
85
+ if (normalizedError instanceof FlowExpiredError) {
86
+ return validateCeremonyOutput(createTurn("abort", {
87
+ hint: normalizedError.message,
88
+ data: { code: normalizedError.code ?? "flow_expired" },
89
+ }));
90
+ }
91
+ const retryData = normalizedError instanceof ValidationError
92
+ ? { errors: normalizedError.zodError }
93
+ : { error: normalizedError.message };
94
+ return validateCeremonyOutput(createTurn("retry", {
95
+ hint: `${hint}: ${normalizedError.message}`,
96
+ data: retryData,
97
+ }));
98
+ }
99
+ async function runCeremonyHandler(handler, hint, ctx, input) {
100
+ try {
101
+ return validateCeremonyOutput(await handler(ctx, input));
102
+ }
103
+ catch (error) {
104
+ return toRetryTurn(error, hint);
105
+ }
106
+ }
107
+ function getString(input, key) {
108
+ const value = input[key];
109
+ return typeof value === "string" ? value : undefined;
110
+ }
111
+ function getNestedRecord(ctx, key) {
112
+ return ensureRecord(ctx.context.get(key));
113
+ }
114
+ function buildJsonSchemaForm(expectedInput, hint) {
115
+ return createTurn("form", {
116
+ hint,
117
+ expectedInput: withDeclaredFormFieldOrder(expectedInput),
118
+ data: {},
119
+ });
120
+ }
121
+ function withDeclaredFormFieldOrder(expectedInput) {
122
+ const properties = expectedInput.properties;
123
+ if (!isRecord(properties)) {
124
+ return expectedInput;
125
+ }
126
+ const existingOrder = expectedInput[FORM_FIELD_ORDER_EXTENSION];
127
+ if (Array.isArray(existingOrder) &&
128
+ existingOrder.every((value) => typeof value === "string")) {
129
+ return expectedInput;
130
+ }
131
+ return {
132
+ ...expectedInput,
133
+ [FORM_FIELD_ORDER_EXTENSION]: Object.keys(properties),
134
+ };
135
+ }
136
+ export function validateCeremonyOutput(turn) {
137
+ if (!validateAuthTurn(turn)) {
138
+ const detail = validateAuthTurn.errors
139
+ ?.map((error) => `${error.instancePath || "$"} ${error.message ?? "invalid"}`)
140
+ .join("; ");
141
+ throw new TurnValidationError(detail || "Invalid AuthTurn output");
142
+ }
143
+ return turn;
144
+ }
145
+ export function createOAuth2Ceremony(options) {
146
+ return {
147
+ start: (ctx) => runCeremonyHandler(async () => {
148
+ const clientId = getRequiredEnv(ctx, options.clientIdEnvKey);
149
+ getRequiredEnv(ctx, options.clientSecretEnvKey);
150
+ const state = toBase64Url(randomBytes(24));
151
+ ctx.context.set(OAUTH2_STATE_KEY, state);
152
+ const authorizeUrl = new URL(options.authorizeUrl);
153
+ authorizeUrl.searchParams.set("response_type", "code");
154
+ authorizeUrl.searchParams.set("client_id", clientId);
155
+ authorizeUrl.searchParams.set("state", state);
156
+ if (options.scopes.length > 0) {
157
+ authorizeUrl.searchParams.set("scope", options.scopes.join(" "));
158
+ }
159
+ if (options.usePKCE) {
160
+ const verifier = createCodeVerifier();
161
+ ctx.context.set(OAUTH2_PKCE_VERIFIER_KEY, verifier);
162
+ authorizeUrl.searchParams.set("code_challenge", createCodeChallenge(verifier));
163
+ authorizeUrl.searchParams.set("code_challenge_method", "S256");
164
+ }
165
+ return createTurn("redirect", {
166
+ data: { url: authorizeUrl.toString() },
167
+ hint: "Open the provider authorization page to continue.",
168
+ expectedInput: {
169
+ type: "object",
170
+ required: ["code", "state"],
171
+ properties: {
172
+ code: { type: "string" },
173
+ state: { type: "string" },
174
+ },
175
+ },
176
+ });
177
+ }, "OAuth start failed", ctx),
178
+ continue: (ctx, input = {}) => runCeremonyHandler(async () => {
179
+ const code = getString(input, "code");
180
+ const receivedState = getString(input, "state");
181
+ const storedState = ctx.context.get(OAUTH2_STATE_KEY);
182
+ const codeVerifier = ctx.context.get(OAUTH2_PKCE_VERIFIER_KEY);
183
+ if (!code || !receivedState || receivedState !== storedState) {
184
+ throw new ValidationError("OAuth callback payload is invalid.");
185
+ }
186
+ const tokenResponse = await ctx.http.post(options.tokenUrl, {
187
+ grant_type: "authorization_code",
188
+ code,
189
+ client_id: getRequiredEnv(ctx, options.clientIdEnvKey),
190
+ client_secret: getRequiredEnv(ctx, options.clientSecretEnvKey),
191
+ ...(options.usePKCE
192
+ ? typeof codeVerifier === "string"
193
+ ? { code_verifier: codeVerifier }
194
+ : {}
195
+ : {}),
196
+ });
197
+ return createTurn("complete", {
198
+ data: { credential: ensureRecord(tokenResponse.data) },
199
+ hint: "OAuth flow completed.",
200
+ });
201
+ }, "OAuth token exchange failed", ctx, input),
202
+ abort: async () => validateCeremonyOutput(createTurn("abort", { hint: "OAuth flow aborted." })),
203
+ };
204
+ }
205
+ export function createDeviceFlowCeremony(options) {
206
+ return {
207
+ start: (ctx) => runCeremonyHandler(async () => {
208
+ const response = await ctx.http.post(options.deviceCodeUrl, {
209
+ client_id: getRequiredEnv(ctx, options.clientIdEnvKey),
210
+ scope: options.scopes.join(" "),
211
+ });
212
+ const data = ensureRecord(response.data);
213
+ ctx.context.set(DEVICE_FLOW_KEY, data);
214
+ return createTurn("message", {
215
+ data: {
216
+ user_code: getString(data, "user_code") ?? "",
217
+ verification_uri: getString(data, "verification_uri") ?? "",
218
+ },
219
+ hint: "Enter the code on the verification page, then poll for completion.",
220
+ timing: { suggestedPollIntervalMs: 5_000, maxWaitMs: 120_000 },
221
+ });
222
+ }, "Device flow start failed", ctx),
223
+ continue: async () => validateCeremonyOutput(createTurn("poll", {
224
+ hint: "Continue polling until the device flow completes.",
225
+ timing: { suggestedPollIntervalMs: 5_000, maxWaitMs: 120_000 },
226
+ })),
227
+ poll: (ctx) => runCeremonyHandler(async () => {
228
+ const deviceData = getNestedRecord(ctx, DEVICE_FLOW_KEY);
229
+ const deviceCode = getString(deviceData, "device_code");
230
+ if (!deviceCode) {
231
+ throw new FlowExpiredError("Device flow state is missing.");
232
+ }
233
+ const response = await ctx.http.post(options.tokenUrl, {
234
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
235
+ device_code: deviceCode,
236
+ client_id: getRequiredEnv(ctx, options.clientIdEnvKey),
237
+ ...(options.clientSecretEnvKey
238
+ ? {
239
+ client_secret: getRequiredEnv(ctx, options.clientSecretEnvKey),
240
+ }
241
+ : {}),
242
+ });
243
+ const data = ensureRecord(response.data);
244
+ const errorCode = getString(data, "error");
245
+ if (errorCode === "authorization_pending" ||
246
+ errorCode === "slow_down") {
247
+ return createTurn("poll", {
248
+ data,
249
+ hint: "Authorization pending.",
250
+ timing: {
251
+ suggestedPollIntervalMs: errorCode === "slow_down" ? 10_000 : 5_000,
252
+ maxWaitMs: 120_000,
253
+ },
254
+ });
255
+ }
256
+ if (errorCode === "expired_token") {
257
+ throw new FlowExpiredError("Device code expired.");
258
+ }
259
+ return createTurn("complete", {
260
+ data: { credential: data },
261
+ hint: "Device flow completed.",
262
+ });
263
+ }, "Device flow polling failed", ctx),
264
+ abort: async () => validateCeremonyOutput(createTurn("abort", { hint: "Device flow aborted." })),
265
+ };
266
+ }
267
+ export function createWebAuthnCeremony(options) {
268
+ return {
269
+ start: (ctx) => runCeremonyHandler(async () => {
270
+ const challenge = toBase64Url(randomBytes(32));
271
+ ctx.context.set("__webauthn_challenge", challenge);
272
+ if (options.challengeUrl) {
273
+ await ctx.http.post(options.challengeUrl, {
274
+ challenge,
275
+ rpId: options.rpId,
276
+ });
277
+ }
278
+ return createTurn("challenge", {
279
+ data: { challenge, rpId: options.rpId },
280
+ hint: "Complete the WebAuthn prompt in your browser.",
281
+ expiresAt: createExpiresAt(options.timeoutMs ?? 60_000),
282
+ expectedInput: {
283
+ type: "object",
284
+ required: ["attestation"],
285
+ properties: { attestation: { type: "object" } },
286
+ },
287
+ });
288
+ }, "WebAuthn start failed", ctx),
289
+ continue: (ctx, input = {}) => runCeremonyHandler(async () => {
290
+ if (!isRecord(input.attestation)) {
291
+ throw new ValidationError("WebAuthn attestation is required.");
292
+ }
293
+ const challenge = ctx.context.get("__webauthn_challenge");
294
+ if (typeof challenge !== "string" || challenge.length === 0) {
295
+ throw new FlowExpiredError("WebAuthn challenge has expired.");
296
+ }
297
+ if (options.verifyUrl) {
298
+ await ctx.http.post(options.verifyUrl, {
299
+ challenge,
300
+ attestation: input.attestation,
301
+ rpId: options.rpId,
302
+ });
303
+ }
304
+ return createTurn("complete", {
305
+ data: { credential: { attestation: input.attestation } },
306
+ hint: "WebAuthn ceremony completed.",
307
+ });
308
+ }, "WebAuthn verification failed", ctx, input),
309
+ abort: async () => validateCeremonyOutput(createTurn("abort", { hint: "WebAuthn ceremony aborted." })),
310
+ };
311
+ }
312
+ export function createMagicLinkCeremony(options) {
313
+ const emailField = options.emailField ?? "email";
314
+ return {
315
+ start: (ctx, input = {}) => runCeremonyHandler(async () => {
316
+ const email = getString(input, emailField);
317
+ if (!email) {
318
+ return buildJsonSchemaForm({
319
+ type: "object",
320
+ required: [emailField],
321
+ properties: {
322
+ [emailField]: { type: "string", format: "email" },
323
+ },
324
+ }, "Provide the email address to receive a magic link.");
325
+ }
326
+ await ctx.http.post(options.sendUrl, { email });
327
+ ctx.context.set(MAGIC_LINK_KEY, {
328
+ email,
329
+ expiresAt: createExpiresAt(options.expiresInMs ?? 300_000),
330
+ });
331
+ return createTurn("message", {
332
+ data: { email },
333
+ hint: "Check your email for the magic link, then poll for completion.",
334
+ timing: { suggestedPollIntervalMs: 5_000, maxWaitMs: 300_000 },
335
+ });
336
+ }, "Magic link start failed", ctx, input),
337
+ continue: async () => validateCeremonyOutput(createTurn("poll", {
338
+ hint: "Continue polling for magic link completion.",
339
+ timing: { suggestedPollIntervalMs: 5_000, maxWaitMs: 300_000 },
340
+ })),
341
+ poll: (ctx) => runCeremonyHandler(async () => {
342
+ const state = getNestedRecord(ctx, MAGIC_LINK_KEY);
343
+ const email = getString(state, "email");
344
+ const expiresAt = getString(state, "expiresAt");
345
+ if (!email || !expiresAt) {
346
+ throw new FlowExpiredError("Magic link state is missing.");
347
+ }
348
+ if (new Date(expiresAt).getTime() < Date.now()) {
349
+ throw new FlowExpiredError("Magic link expired.");
350
+ }
351
+ const response = await ctx.http.post(options.verifyUrl, { email });
352
+ const data = ensureRecord(response.data);
353
+ if (data.completed !== true) {
354
+ return createTurn("poll", {
355
+ data,
356
+ hint: "Waiting for the magic link click.",
357
+ timing: { suggestedPollIntervalMs: 5_000, maxWaitMs: 300_000 },
358
+ });
359
+ }
360
+ return createTurn("complete", {
361
+ data: { credential: ensureRecord(data.credential) },
362
+ hint: "Magic link completed.",
363
+ });
364
+ }, "Magic link polling failed", ctx),
365
+ abort: async () => validateCeremonyOutput(createTurn("abort", { hint: "Magic link flow aborted." })),
366
+ };
367
+ }
368
+ export function createFormCeremony(options) {
369
+ return {
370
+ start: async () => validateCeremonyOutput(buildJsonSchemaForm(options.schema, options.hint ?? "Provide the required input to continue.")),
371
+ continue: (ctx, input = {}) => runCeremonyHandler(async () => {
372
+ const { prevalidate } = await import("../runtime/prevalidate");
373
+ const result = prevalidate(options.schema, input);
374
+ if (!result.valid) {
375
+ throw new ValidationError("Form input failed validation.", {
376
+ zodError: result.errors,
377
+ });
378
+ }
379
+ return createTurn("complete", {
380
+ data: {
381
+ credential: options.mapCredential
382
+ ? options.mapCredential(input)
383
+ : input,
384
+ },
385
+ hint: "Form completed.",
386
+ });
387
+ }, "Form submission failed", ctx, input),
388
+ abort: async () => validateCeremonyOutput(createTurn("abort", { hint: "Form ceremony aborted." })),
389
+ };
390
+ }
391
+ export function combineCeremonies(...ceremonies) {
392
+ function getStage(ctx) {
393
+ const rawStage = ctx.context.get(COMBINED_STAGE_KEY);
394
+ return typeof rawStage === "number" ? rawStage : 0;
395
+ }
396
+ return {
397
+ start: (ctx) => runCeremonyHandler(async () => {
398
+ ctx.context.set(COMBINED_STAGE_KEY, 0);
399
+ const first = ceremonies[0];
400
+ if (!first) {
401
+ throw new ValidationError("At least one ceremony is required.");
402
+ }
403
+ return await first.start(ctx);
404
+ }, "Combined ceremony start failed", ctx),
405
+ continue: (ctx, input = {}) => runCeremonyHandler(async () => {
406
+ const stage = getStage(ctx);
407
+ const current = ceremonies[stage];
408
+ if (!current) {
409
+ throw new FlowExpiredError("Combined ceremony stage is invalid.");
410
+ }
411
+ const result = await current.continue(ctx, input);
412
+ if (result.kind !== "complete") {
413
+ return result;
414
+ }
415
+ const nextStage = stage + 1;
416
+ const nextCeremony = ceremonies[nextStage];
417
+ if (!nextCeremony) {
418
+ return result;
419
+ }
420
+ ctx.context.set(COMBINED_STAGE_KEY, nextStage);
421
+ return await nextCeremony.start(ctx);
422
+ }, "Combined ceremony continue failed", ctx, input),
423
+ poll: (ctx) => runCeremonyHandler(async () => {
424
+ const current = ceremonies[getStage(ctx)];
425
+ if (!current?.poll) {
426
+ throw new ValidationError("Current ceremony does not support polling.");
427
+ }
428
+ return await current.poll(ctx);
429
+ }, "Combined ceremony poll failed", ctx),
430
+ abort: (ctx) => runCeremonyHandler(async () => {
431
+ const current = ceremonies[getStage(ctx)];
432
+ if (current?.abort) {
433
+ return await current.abort(ctx);
434
+ }
435
+ return createTurn("abort", { hint: "Combined ceremony aborted." });
436
+ }, "Combined ceremony abort failed", ctx),
437
+ };
438
+ }
439
+ export function createSwitchCeremony(options) {
440
+ const choiceKeys = Object.keys(options.choices);
441
+ return {
442
+ start: async () => validateCeremonyOutput(createTurn("multi_choice", {
443
+ data: { choices: choiceKeys },
444
+ hint: options.prompt ?? "Choose an authentication method.",
445
+ expectedInput: {
446
+ type: "object",
447
+ required: ["choice"],
448
+ properties: {
449
+ choice: { type: "string", enum: choiceKeys },
450
+ },
451
+ },
452
+ })),
453
+ continue: (ctx, input = {}) => runCeremonyHandler(async () => {
454
+ const storedChoice = ctx.context.get(SWITCH_SELECTION_KEY);
455
+ const choice = typeof storedChoice === "string"
456
+ ? storedChoice
457
+ : getString(input, "choice");
458
+ if (!choice || !options.choices[choice]) {
459
+ throw new ValidationError("A valid choice is required.");
460
+ }
461
+ ctx.context.set(SWITCH_SELECTION_KEY, choice);
462
+ const ceremony = options.choices[choice];
463
+ if (storedChoice === undefined) {
464
+ return await ceremony.start(ctx);
465
+ }
466
+ return await ceremony.continue(ctx, input);
467
+ }, "Switch ceremony failed", ctx, input),
468
+ poll: (ctx) => runCeremonyHandler(async () => {
469
+ const choice = ctx.context.get(SWITCH_SELECTION_KEY);
470
+ if (typeof choice !== "string") {
471
+ throw new FlowExpiredError("No selected ceremony is active.");
472
+ }
473
+ const ceremony = options.choices[choice];
474
+ if (!ceremony?.poll) {
475
+ throw new ValidationError("Selected ceremony does not support polling.");
476
+ }
477
+ return await ceremony.poll(ctx);
478
+ }, "Switch ceremony poll failed", ctx),
479
+ abort: (ctx) => runCeremonyHandler(async () => {
480
+ const choice = ctx.context.get(SWITCH_SELECTION_KEY);
481
+ if (typeof choice === "string") {
482
+ const ceremony = options.choices[choice];
483
+ if (ceremony?.abort) {
484
+ return await ceremony.abort(ctx);
485
+ }
486
+ }
487
+ return createTurn("abort", { hint: "Switch ceremony aborted." });
488
+ }, "Switch ceremony abort failed", ctx),
489
+ };
490
+ }
@@ -0,0 +1,24 @@
1
+ export type ProviderChoiceTokenPayload = Record<string, unknown>;
2
+ export type ProviderChoiceTokenErrorReason = "invalid_shape" | "invalid_signature" | "invalid_payload" | "invalid_binding" | "stale";
3
+ export declare class ProviderChoiceTokenError extends Error {
4
+ readonly reason: ProviderChoiceTokenErrorReason;
5
+ constructor(reason: ProviderChoiceTokenErrorReason, message: string);
6
+ }
7
+ export interface CreateProviderChoiceTokenOptions<TPayload extends ProviderChoiceTokenPayload> {
8
+ prefix: string;
9
+ payload: TPayload;
10
+ secret: string;
11
+ }
12
+ export interface ParseProviderChoiceTokenOptions {
13
+ token: string;
14
+ prefix: string;
15
+ secret: string;
16
+ }
17
+ export interface FreshProviderChoiceIssuedAtOptions {
18
+ ttlMs: number;
19
+ nowMs?: number;
20
+ futureToleranceMs?: number;
21
+ }
22
+ export declare function createProviderChoiceToken<TPayload extends ProviderChoiceTokenPayload>(options: CreateProviderChoiceTokenOptions<TPayload>): string;
23
+ export declare function parseProviderChoiceToken(options: ParseProviderChoiceTokenOptions): ProviderChoiceTokenPayload;
24
+ export declare function assertFreshProviderChoiceIssuedAt(issuedAtMs: unknown, options: FreshProviderChoiceIssuedAtOptions): number;
@@ -0,0 +1,74 @@
1
+ import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes, timingSafeEqual, } from "node:crypto";
2
+ export class ProviderChoiceTokenError extends Error {
3
+ reason;
4
+ constructor(reason, message) {
5
+ super(message);
6
+ this.name = "ProviderChoiceTokenError";
7
+ this.reason = reason;
8
+ }
9
+ }
10
+ export function createProviderChoiceToken(options) {
11
+ const iv = randomBytes(12);
12
+ const cipher = createCipheriv("aes-256-gcm", choiceEncryptionKey(options.secret), iv);
13
+ const encryptedPayload = Buffer.concat([
14
+ cipher.update(JSON.stringify(options.payload), "utf8"),
15
+ cipher.final(),
16
+ ]).toString("base64url");
17
+ const authTag = cipher.getAuthTag().toString("base64url");
18
+ const encodedIv = iv.toString("base64url");
19
+ const signature = signProviderChoiceTokenBody(`${options.prefix}.${encodedIv}.${encryptedPayload}.${authTag}`, options.secret);
20
+ return `${options.prefix}.${encodedIv}.${encryptedPayload}.${authTag}.${signature}`;
21
+ }
22
+ export function parseProviderChoiceToken(options) {
23
+ const [actualPrefix, encodedIv, encryptedPayload, authTag, signature, ...extra] = options.token.split(".");
24
+ if (actualPrefix !== options.prefix ||
25
+ !encodedIv ||
26
+ !encryptedPayload ||
27
+ !authTag ||
28
+ !signature ||
29
+ extra.length > 0) {
30
+ throw new ProviderChoiceTokenError("invalid_shape", "Provider choice token shape is invalid.");
31
+ }
32
+ const signedBody = `${options.prefix}.${encodedIv}.${encryptedPayload}.${authTag}`;
33
+ const expectedSignature = signProviderChoiceTokenBody(signedBody, options.secret);
34
+ const actualBuffer = Buffer.from(signature);
35
+ const expectedBuffer = Buffer.from(expectedSignature);
36
+ if (actualBuffer.length !== expectedBuffer.length ||
37
+ !timingSafeEqual(actualBuffer, expectedBuffer)) {
38
+ throw new ProviderChoiceTokenError("invalid_signature", "Provider choice token signature is invalid.");
39
+ }
40
+ try {
41
+ const decipher = createDecipheriv("aes-256-gcm", choiceEncryptionKey(options.secret), Buffer.from(encodedIv, "base64url"));
42
+ decipher.setAuthTag(Buffer.from(authTag, "base64url"));
43
+ const decrypted = Buffer.concat([
44
+ decipher.update(Buffer.from(encryptedPayload, "base64url")),
45
+ decipher.final(),
46
+ ]).toString("utf8");
47
+ const parsed = JSON.parse(decrypted);
48
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
49
+ throw new Error("payload is not an object");
50
+ }
51
+ return Object.fromEntries(Object.entries(parsed));
52
+ }
53
+ catch {
54
+ throw new ProviderChoiceTokenError("invalid_payload", "Provider choice token payload is invalid.");
55
+ }
56
+ }
57
+ export function assertFreshProviderChoiceIssuedAt(issuedAtMs, options) {
58
+ const parsed = typeof issuedAtMs === "number" ? issuedAtMs : Number(issuedAtMs);
59
+ const nowMs = options.nowMs ?? Date.now();
60
+ const futureToleranceMs = options.futureToleranceMs ?? 30_000;
61
+ if (!Number.isFinite(parsed) ||
62
+ parsed <= 0 ||
63
+ nowMs - parsed > options.ttlMs ||
64
+ parsed - nowMs > futureToleranceMs) {
65
+ throw new ProviderChoiceTokenError("stale", "Provider choice token is stale.");
66
+ }
67
+ return parsed;
68
+ }
69
+ function choiceEncryptionKey(secret) {
70
+ return createHash("sha256").update(secret).digest();
71
+ }
72
+ function signProviderChoiceTokenBody(body, secret) {
73
+ return createHmac("sha256", secret).update(body).digest("base64url");
74
+ }
@@ -0,0 +1,10 @@
1
+ export type ApifuseCommandName = "create" | "dev" | "check" | "submit-check" | "bounty-check" | "record" | "test" | "perf";
2
+ export type ApifuseCommandManifest = {
3
+ name: ApifuseCommandName;
4
+ summary: string;
5
+ usage: string;
6
+ examples: string[];
7
+ modulePath: string;
8
+ };
9
+ export declare const COMMAND_MANIFEST: Record<ApifuseCommandName, ApifuseCommandManifest>;
10
+ export declare const COMMAND_ORDER: ApifuseCommandName[];