@apifuse/provider-sdk 2.2.0-beta.35 → 2.2.0-beta.37

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 (44) hide show
  1. package/AUTHORING.md +22 -8
  2. package/CHANGELOG.md +8 -0
  3. package/README.md +20 -8
  4. package/bin/apifuse-dev.ts +1 -1
  5. package/bin/apifuse-pack-types.ts +6 -5
  6. package/bin/apifuse-record.ts +1 -1
  7. package/bin/apifuse-submit-check.ts +23 -10
  8. package/dist/ceremonies/index.d.ts +8 -0
  9. package/dist/ceremonies/index.js +32 -25
  10. package/dist/cli/templates/provider/Dockerfile.tpl +1 -1
  11. package/dist/cli/templates/provider/index.ts.tpl +6 -3
  12. package/dist/cli/templates/provider/operations/ping.ts.tpl +2 -1
  13. package/dist/define.d.ts +46 -25
  14. package/dist/define.js +381 -9
  15. package/dist/index.d.ts +2 -2
  16. package/dist/provider.d.ts +2 -1
  17. package/dist/runtime/browser.js +19 -11
  18. package/dist/runtime/choice.js +24 -7
  19. package/dist/runtime/resolver-public.d.ts +1 -1
  20. package/dist/runtime/resolver-public.js +1 -1
  21. package/dist/runtime/resolver-vendors/browser.js +57 -14
  22. package/dist/runtime/resolver-vendors/types.d.ts +9 -1
  23. package/dist/runtime/resolver-vendors/types.js +15 -0
  24. package/dist/runtime/resolver.d.ts +1 -0
  25. package/dist/runtime/resolver.js +13 -7
  26. package/dist/server/serve-implementation.js +25 -0
  27. package/dist/types.d.ts +25 -17
  28. package/package.json +6 -1
  29. package/src/ceremonies/index.ts +45 -31
  30. package/src/cli/templates/provider/Dockerfile.tpl +1 -1
  31. package/src/cli/templates/provider/index.ts.tpl +6 -3
  32. package/src/cli/templates/provider/operations/ping.ts.tpl +2 -1
  33. package/src/define.ts +462 -59
  34. package/src/index.ts +5 -2
  35. package/src/provider.ts +6 -1
  36. package/src/runtime/browser.ts +34 -11
  37. package/src/runtime/choice.ts +25 -8
  38. package/src/runtime/resolver-public.ts +2 -0
  39. package/src/runtime/resolver-vendors/browser.ts +69 -11
  40. package/src/runtime/resolver-vendors/types.ts +21 -0
  41. package/src/runtime/resolver.ts +17 -5
  42. package/src/server/serve-implementation.ts +39 -1
  43. package/src/testing/run.ts +3 -3
  44. package/src/types.ts +41 -17
package/AUTHORING.md CHANGED
@@ -104,7 +104,14 @@ description:
104
104
 
105
105
  ### Factored operations
106
106
 
107
- Use `defineOperation()` when an operation is large enough to live beside helper functions or in a separate module. It preserves the same type inference as inline `defineProvider()` operations and can be placed directly in the provider `operations` map. `defineProvider()` accepts Zod and Standard Schema v1-compatible schemas. If config validation fails, the SDK names the field to fix, for example `runtime`, `auth.mode`, `operations.<id>.handler`, or `operations.<id>.fixtures.response`.
107
+ `defineProvider(declaration)` returns the builder that accepts `operations`, so
108
+ inline handlers are typed only after capability declarations are fixed. Export
109
+ `ProviderContextOf<typeof buildProvider>` once from the provider entry point.
110
+ Separate operation files import that provider context and call
111
+ `defineOperation<ProviderContext>()({...})`; helpers should accept the one SDK
112
+ client they use rather than the provider context. Zod and Standard Schema v1
113
+ schemas retain input/output inference. Invalid configs name the offending field,
114
+ such as `auth.mode` or `operations.<id>.fixtures.response`.
108
115
 
109
116
  ### Replay-safe fixtures
110
117
 
@@ -236,8 +243,9 @@ level, then call `ctx.stt` from operation handlers or auth-flow handlers.
236
243
  ```ts
237
244
  export default defineProvider({
238
245
  id: "example-provider",
239
- // ...metadata, auth, operations, allowedHosts
246
+ // ...metadata, auth, allowedHosts
240
247
  stt: { mode: "required" },
248
+ })({
241
249
  operations: {
242
250
  verifyAudioOtp: {
243
251
  input: z.object({
@@ -357,11 +365,13 @@ const paymentWebviewJourney = defineHealthJourney({
357
365
  ],
358
366
  });
359
367
 
360
- export default defineProvider({
368
+ const buildProvider = defineProvider({
361
369
  id: "example-provider",
362
- // ...metadata, auth, operations, allowedHosts
370
+ // ...metadata, auth, allowedHosts
363
371
  healthJourneys: [paymentWebviewJourney],
364
372
  });
373
+
374
+ export default buildProvider({ operations });
365
375
  ```
366
376
  <!-- @magic-end:sample -->
367
377
 
@@ -604,7 +614,7 @@ failures are sanitized before propagation.
604
614
  HTML, or upstream `Error` objects.
605
615
 
606
616
  ```ts
607
- export default defineProvider({
617
+ const buildProvider = defineProvider({
608
618
  id: "example-provider",
609
619
  version: "1.0.0",
610
620
  runtime: "standard",
@@ -638,8 +648,10 @@ export default defineProvider({
638
648
  },
639
649
  },
640
650
  credential: { keys: ["cookie"] },
641
- // ...metadata and operations
651
+ // ...metadata
642
652
  });
653
+
654
+ export default buildProvider({ operations });
643
655
  ```
644
656
 
645
657
  - Credentials auth providers should use `defineCredentialsAuth()` instead of
@@ -666,15 +678,17 @@ const credentialsAuth = defineCredentialsAuth({
666
678
  },
667
679
  });
668
680
 
669
- export default defineProvider({
681
+ const buildProvider = defineProvider({
670
682
  id: "example-provider",
671
683
  version: "1.0.0",
672
684
  runtime: "standard",
673
685
  auth: credentialsAuth.auth,
674
686
  credential: credentialsAuth.credential,
675
687
  context: credentialsAuth.context,
676
- // ...metadata and operations
688
+ // ...metadata
677
689
  });
690
+
691
+ export default buildProvider({ operations });
678
692
  ```
679
693
 
680
694
  For OTP, MFA, CAPTCHA handoff, or user-approved login, return a challenge from
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @apifuse/provider-sdk Changelog
2
2
 
3
+ ## 2.2.0-beta.37
4
+
5
+ - Release candidate for main commit aa14b268dffe1196f25c2b6b314598fe0896edec.
6
+
7
+ ## 2.2.0-beta.36
8
+
9
+ - Release candidate for main commit c6858f8b87d78b3b755adeb28982e875434be406.
10
+
3
11
  ## 2.2.0-beta.35
4
12
 
5
13
  - Release candidate for main commit 027fa0087b883a6281bc6d8bdaaf6d0be382000f.
package/README.md CHANGED
@@ -196,12 +196,28 @@ the bad request path; provider/runtime failures include `code`, `message`, and
196
196
 
197
197
  ## Authoring ergonomics
198
198
 
199
- `defineProvider()` infers each operation handler input from the operation `input` schema. For larger providers, factor operations with `defineOperation()` and compose them later:
199
+ `defineProvider()` establishes the capability declaration before its returned
200
+ builder contextually types operations. For larger providers, export the derived
201
+ context once and use it with `defineOperation()` in separate files:
200
202
 
201
203
  ```ts
202
- import { defineOperation, defineProvider, z } from "@apifuse/provider-sdk/provider"
204
+ import {
205
+ defineOperation,
206
+ defineProvider,
207
+ type ProviderContextOf,
208
+ z,
209
+ } from "@apifuse/provider-sdk/provider"
210
+
211
+ const buildProvider = defineProvider({
212
+ id: "factored-provider",
213
+ version: "1.0.0",
214
+ runtime: "standard",
215
+ meta: { displayName: "Factored", category: "demo" },
216
+ })
203
217
 
204
- const search = defineOperation({
218
+ export type ProviderContext = ProviderContextOf<typeof buildProvider>
219
+
220
+ const search = defineOperation<ProviderContext>()({
205
221
  input: z.object({ q: z.string().describe("Search query") }),
206
222
  output: z.object({ count: z.number().describe("Result count") }),
207
223
  async handler(ctx, input) {
@@ -212,11 +228,7 @@ const search = defineOperation({
212
228
  },
213
229
  })
214
230
 
215
- export default defineProvider({
216
- id: "factored-provider",
217
- version: "1.0.0",
218
- runtime: "standard",
219
- meta: { displayName: "Factored", category: "demo" },
231
+ export default buildProvider({
220
232
  operations: { search },
221
233
  })
222
234
  ```
@@ -129,7 +129,7 @@ export function createProviderContext(provider: ProviderDefinition): {
129
129
  credential,
130
130
  state,
131
131
  }),
132
- };
132
+ } satisfies Omit<ProviderContext, "native"> as unknown as ProviderContext;
133
133
 
134
134
  return { ctx };
135
135
  }
@@ -77,6 +77,7 @@ const NEGATIVE_CONTROLS = [
77
77
  '\truntime: "standard",',
78
78
  '\tresolver: { vendors: ["unknown-vendor"], kinds: ["turnstile"] },',
79
79
  '\tmeta: { displayName: "Invalid Resolver Vendor", descriptionKey: "meta.description", category: "test" },',
80
+ "})({",
80
81
  "\toperations: {",
81
82
  "\t\tprobe: {",
82
83
  "\t\t\tinput: z.object({}),",
@@ -381,7 +382,7 @@ function setUpFixtureConsumer(consumerDir: string, tarballPath: string): void {
381
382
  [
382
383
  'import { defineProvider, ProviderError, resolveProxy, SessionExpiredError, z } from "@apifuse/provider-sdk";',
383
384
  'import { invalidateResolverSolution } from "@apifuse/provider-sdk/runtime/resolver";',
384
- 'import type { BrowserCookie, ChallengeSolution, NativeNetworkClient, NativeNetworkConnection, NativeProviderConfig, NativeProviderContext, NativeTcpEgressGrant, ProviderChallenge, ProviderContext, ProviderFileRef, ProviderFilesContext, ProviderResolvedFile, ProviderResolverConfig, ResolverContext, ResolverRuntimeOptions } from "@apifuse/provider-sdk";',
385
+ 'import type { BrowserCookie, ChallengeSolution, NativeContext, NativeNetworkClient, NativeNetworkConnection, NativeProviderConfig, NativeTcpEgressGrant, ProviderChallenge, ProviderContext, ProviderFileRef, ProviderFilesContext, ProviderResolvedFile, ProviderResolverConfig, ResolverContext, ResolverRuntimeOptions } from "@apifuse/provider-sdk";',
385
386
  'import type { ProxyProtocol, ProxyResolutionOptions, ProxyResolutionSource, ProxyVendorName, RequestOptions, ResolvedProxyConfig } from "@apifuse/provider-sdk";',
386
387
  'import { defineCredentialsAuth } from "@apifuse/provider-sdk/provider";',
387
388
  'import type { NativeNetworkClient as ProviderEntryNativeNetworkClient, ProviderFilesContext as ProviderEntryFilesContext } from "@apifuse/provider-sdk/provider";',
@@ -415,12 +416,12 @@ function setUpFixtureConsumer(consumerDir: string, tarballPath: string): void {
415
416
  "const connection: NativeNetworkConnection = { read: async () => null, write: async () => {}, close: async () => {} };",
416
417
  "const network: NativeNetworkClient = { connectTcp: async () => connection, connectTls: async () => connection, grantTcpEgress: () => ({ revoke() {} }) };",
417
418
  "const providerEntryNetwork: ProviderEntryNativeNetworkClient = network;",
418
- "const nativeContext: NativeProviderContext = { network };",
419
+ "const nativeContext: NativeContext = { network };",
419
420
  'const grant: NativeTcpEgressGrant = network.grantTcpEgress({ sourceHost: "booking-loco.kakao.com", sourcePort: 443, host: "loco.kakao.com", port: 5228, tls: "disabled" });',
420
421
  'const nativeConfig: NativeProviderConfig = { network: { tcp: [{ host: "booking-loco.kakao.com", ports: [443], tls: "required" }] } };',
421
422
  "const providerContext = undefined as unknown as ProviderContext;",
422
423
  "const optionalFiles: ProviderFilesContext | undefined = providerContext.files;",
423
- "const optionalNative: NativeProviderContext | undefined = providerContext.native;",
424
+ "const native: NativeContext = providerContext.native;",
424
425
  "export const providerResolver: ResolverContext = providerContext.resolver;",
425
426
  'export const resolverRuntimeOptions: ResolverRuntimeOptions = { allowedHosts: ["example.com"], cache: providerContext.cache };',
426
427
  "",
@@ -435,7 +436,7 @@ function setUpFixtureConsumer(consumerDir: string, tarballPath: string): void {
435
436
  'export const cookieSolution: ChallengeSolution = { form: "cookies", cookies: { cf_clearance: "clearance" }, userAgent: "fixture-agent" };',
436
437
  "export const invalidation = invalidateResolverSolution(providerContext.resolver, awsWafChallenge, cookieSolution);",
437
438
  'export const resolverConfig: ProviderResolverConfig = { vendors: ["browser", "capsolver"], kinds: ["cloudflare_interstitial", "turnstile"] };',
438
- 'export const resolverProvider = defineProvider({ id: "pack-types-resolver", version: "1.0.0", runtime: "standard", resolver: resolverConfig, meta: { displayName: "Pack Types Resolver", descriptionKey: "meta.description", category: "test" }, operations: { probe: { input: z.object({}), output: z.object({ ok: z.boolean() }), handler: async () => ({ ok: true }), healthCheckUnsupported: { reason: "type fixture" } } } });',
439
+ 'export const resolverProvider = defineProvider({ id: "pack-types-resolver", version: "1.0.0", runtime: "standard", resolver: resolverConfig, meta: { displayName: "Pack Types Resolver", descriptionKey: "meta.description", category: "test" } })({ operations: { probe: { input: z.object({}), output: z.object({ ok: z.boolean() }), handler: async () => ({ ok: true }), healthCheckUnsupported: { reason: "type fixture" } } } });',
439
440
  "export const resolverContext: ResolverContext = { solve: async () => tokenSolution };",
440
441
  'export const browserCookie: BrowserCookie = { name: "persistent-id", value: "persistent-token", domain: "example.com", path: "/", expires: 1786698176, httpOnly: true, secure: true };',
441
442
  'const browserPage = undefined as unknown as Awaited<ReturnType<ProviderContext["browser"]["newPage"]>>;',
@@ -465,7 +466,7 @@ function setUpFixtureConsumer(consumerDir: string, tarballPath: string): void {
465
466
  " grant,",
466
467
  " nativeConfig,",
467
468
  " optionalFiles,",
468
- " optionalNative,",
469
+ " native,",
469
470
  " browserCookie,",
470
471
  " browserCookies,",
471
472
  " queryCredentialOptions,",
@@ -581,7 +581,7 @@ export function createCaptureContext(
581
581
  credential,
582
582
  state,
583
583
  }),
584
- };
584
+ } satisfies Omit<ProviderContext, "native"> as unknown as ProviderContext;
585
585
 
586
586
  return {
587
587
  ctx,
@@ -1196,18 +1196,31 @@ function scoreFlatOperationComposition(providerRoot: string): SubmitCheck {
1196
1196
  );
1197
1197
  }
1198
1198
 
1199
- // Scope the scan to the argument of the EXPORTED `defineProvider(...)` call.
1199
+ // Scope the scan to the exported provider builder's implementation argument,
1200
+ // or to the legacy-looking declaration call used by structural test fixtures.
1200
1201
  // A provider can contain helper/non-exported defineProvider calls before the
1201
1202
  // real default export (e.g. test scaffolds), so resolve the default export
1202
1203
  // rather than blindly taking the first regex match. Resolution order:
1203
- // 1. `export default defineProvider(` — inline default export
1204
- // 2. `export default <ident>` then `const <ident> = defineProvider(`
1205
- // 3. fallback: first `defineProvider(` in the file
1204
+ // 1. `export default <builder>(` — phase-separated provider
1205
+ // 2. `export default defineProvider(` structural fixture
1206
+ // 3. `export default <ident>` then `const <ident> = defineProvider(`
1207
+ // 4. fallback: first `defineProvider(` in the file
1206
1208
  let defineParenIndex = -1;
1209
+ const builderDefault = /\bexport\s+default\s+([A-Za-z_$][\w$]*)\s*\(/.exec(source);
1210
+ if (builderDefault?.[1] !== undefined && builderDefault[1] !== "defineProvider") {
1211
+ defineParenIndex = builderDefault.index + builderDefault[0].length - 1;
1212
+ }
1207
1213
  const inlineDefault = /\bexport\s+default\s+defineProvider\s*\(/.exec(source);
1208
- if (inlineDefault) {
1209
- defineParenIndex = inlineDefault.index + inlineDefault[0].length - 1; // points at `(`
1210
- } else {
1214
+ if (defineParenIndex === -1 && inlineDefault) {
1215
+ const declarationParen = inlineDefault.index + inlineDefault[0].length - 1;
1216
+ const declarationStart = declarationParen + 1;
1217
+ const declaration = balancedValueExpression(source, declarationStart);
1218
+ let cursor = declarationStart + declaration.length;
1219
+ while (/\s/.test(source[cursor] ?? "")) cursor++;
1220
+ if (source[cursor] === ")") cursor++;
1221
+ while (/\s/.test(source[cursor] ?? "")) cursor++;
1222
+ defineParenIndex = source[cursor] === "(" ? cursor : declarationParen;
1223
+ } else if (defineParenIndex === -1) {
1211
1224
  const namedDefault = /\bexport\s+default\s+([A-Za-z_$][\w$]*)\s*;?/.exec(source);
1212
1225
  const exportedName = namedDefault?.[1];
1213
1226
  if (exportedName !== undefined) {
@@ -1236,7 +1249,7 @@ function scoreFlatOperationComposition(providerRoot: string): SubmitCheck {
1236
1249
  const argStart = defineParenIndex + 1;
1237
1250
  const argText = balancedValueExpression(source, argStart);
1238
1251
 
1239
- // Resolve the value passed as `operations:` inside the defineProvider call,
1252
+ // Resolve the value passed as `operations:` inside the implementation call,
1240
1253
  // following one alias hop. The value is classified as a static object
1241
1254
  // literal (pass) or a factory/call expression (block). The regex index is
1242
1255
  // offset back into the full source so line numbers stay accurate.
@@ -1249,7 +1262,7 @@ function scoreFlatOperationComposition(providerRoot: string): SubmitCheck {
1249
1262
  opsLine = offsetToLine(source, valueStart);
1250
1263
  }
1251
1264
 
1252
- // Property shorthand: `defineProvider({ ..., operations })` — resolve the
1265
+ // Property shorthand: `buildProvider({ operations })` — resolve the
1253
1266
  // local `operations` const initializer.
1254
1267
  let aliasName: string | undefined;
1255
1268
  if (opsValue === undefined) {
@@ -1395,7 +1408,7 @@ function scoreFlatOperationComposition(providerRoot: string): SubmitCheck {
1395
1408
  blockerMessage:
1396
1409
  "defineProvider operations are composed by a factory call instead of a static object literal.",
1397
1410
  remediation:
1398
- "Declare operations as a static literal: defineProvider({ operations: { 'op-id': defineOperation({...}) } }). The provider-registry AST gate requires static runtime/operations; factory composition fails the registry build. If composition is unavoidable, add `// @apifuse-allow flat-operation-composition: <reason>`.",
1411
+ "Declare operations as a static literal in the provider builder call. The provider-registry AST gate requires static runtime/operations; factory composition fails the registry build. If composition is unavoidable, add `// @apifuse-allow flat-operation-composition: <reason>`.",
1399
1412
  passMessage: "defineProvider declares operations as a static object literal.",
1400
1413
  });
1401
1414
  }
@@ -1,8 +1,16 @@
1
+ import type { AuthStartNoInputGuard } from "../define.js";
1
2
  import type { AuthFlowDefinition, AuthTurn } from "../types.js";
2
3
  type JsonObject = Record<string, unknown>;
3
4
  export declare const OAUTH2_PROXIED_AUTH_PROXY_ORIGIN_ENV_KEY = "APIFUSE__AUTH_PROXY__URL";
4
5
  export declare const OAUTH2_PROXIED_PKCE_VERIFIER_KEY = "__oauth2_proxied_pkce_verifier";
5
6
  export declare function validateCeremonyOutput(turn: unknown): AuthTurn;
7
+ /**
8
+ * Defines an auth flow while preserving its concrete type for downstream checks.
9
+ * The compile-time guard validates the inferred literal; annotating or widening
10
+ * a flow to `AuthFlowDefinition` before passing it defeats the check. Full
11
+ * enforcement requires branding, which is deferred to a future major.
12
+ */
13
+ export declare function defineAuthFlow<const TFlow extends AuthFlowDefinition>(flow: TFlow & AuthStartNoInputGuard<TFlow>): TFlow;
6
14
  export declare function createOAuth2Ceremony(options: {
7
15
  authorizeUrl: string;
8
16
  tokenUrl: string;
@@ -163,8 +163,17 @@ export function validateCeremonyOutput(turn) {
163
163
  }
164
164
  return turn;
165
165
  }
166
+ /**
167
+ * Defines an auth flow while preserving its concrete type for downstream checks.
168
+ * The compile-time guard validates the inferred literal; annotating or widening
169
+ * a flow to `AuthFlowDefinition` before passing it defeats the check. Full
170
+ * enforcement requires branding, which is deferred to a future major.
171
+ */
172
+ export function defineAuthFlow(flow) {
173
+ return flow;
174
+ }
166
175
  export function createOAuth2Ceremony(options) {
167
- return {
176
+ return defineAuthFlow({
168
177
  start: (ctx) => runCeremonyHandler(async () => {
169
178
  const clientId = getRequiredEnv(ctx, options.clientIdEnvKey);
170
179
  getRequiredEnv(ctx, options.clientSecretEnvKey);
@@ -221,7 +230,7 @@ export function createOAuth2Ceremony(options) {
221
230
  });
222
231
  }, "OAuth token exchange failed", ctx, input),
223
232
  abort: async () => validateCeremonyOutput(createTurn("abort", { hint: "OAuth flow aborted." })),
224
- };
233
+ });
225
234
  }
226
235
  /**
227
236
  * Builds the start handler for a custom-scheme OAuth provider. The shared
@@ -272,7 +281,7 @@ export function createOAuth2ProxiedStart(options) {
272
281
  }, "OAuth2 proxied start failed", ctx);
273
282
  }
274
283
  export function createDeviceFlowCeremony(options) {
275
- return {
284
+ return defineAuthFlow({
276
285
  start: (ctx) => runCeremonyHandler(async () => {
277
286
  const response = await ctx.http.post(options.deviceCodeUrl, {
278
287
  client_id: getRequiredEnv(ctx, options.clientIdEnvKey),
@@ -330,10 +339,10 @@ export function createDeviceFlowCeremony(options) {
330
339
  });
331
340
  }, "Device flow polling failed", ctx),
332
341
  abort: async () => validateCeremonyOutput(createTurn("abort", { hint: "Device flow aborted." })),
333
- };
342
+ });
334
343
  }
335
344
  export function createWebAuthnCeremony(options) {
336
- return {
345
+ return defineAuthFlow({
337
346
  start: (ctx) => runCeremonyHandler(async () => {
338
347
  const challenge = toBase64Url(randomBytes(32));
339
348
  ctx.context.set("__webauthn_challenge", challenge);
@@ -375,21 +384,23 @@ export function createWebAuthnCeremony(options) {
375
384
  });
376
385
  }, "WebAuthn verification failed", ctx, input),
377
386
  abort: async () => validateCeremonyOutput(createTurn("abort", { hint: "WebAuthn ceremony aborted." })),
378
- };
387
+ });
379
388
  }
380
389
  export function createMagicLinkCeremony(options) {
381
390
  const emailField = options.emailField ?? "email";
382
- return {
383
- start: (ctx, input = {}) => runCeremonyHandler(async () => {
391
+ const buildEmailForm = () => buildJsonSchemaForm({
392
+ type: "object",
393
+ required: [emailField],
394
+ properties: {
395
+ [emailField]: { type: "string", format: "email" },
396
+ },
397
+ }, "Provide the email address to receive a magic link.");
398
+ return defineAuthFlow({
399
+ start: (ctx) => runCeremonyHandler(async () => buildEmailForm(), "Magic link start failed", ctx),
400
+ continue: (ctx, input = {}) => runCeremonyHandler(async () => {
384
401
  const email = getString(input, emailField);
385
402
  if (!email) {
386
- return buildJsonSchemaForm({
387
- type: "object",
388
- required: [emailField],
389
- properties: {
390
- [emailField]: { type: "string", format: "email" },
391
- },
392
- }, "Provide the email address to receive a magic link.");
403
+ return buildEmailForm();
393
404
  }
394
405
  await ctx.http.post(options.sendUrl, { email });
395
406
  ctx.context.set(MAGIC_LINK_KEY, {
@@ -401,11 +412,7 @@ export function createMagicLinkCeremony(options) {
401
412
  hint: "Check your email for the magic link, then poll for completion.",
402
413
  timing: { suggestedPollIntervalMs: 5_000, maxWaitMs: 300_000 },
403
414
  });
404
- }, "Magic link start failed", ctx, input),
405
- continue: async () => validateCeremonyOutput(createTurn("poll", {
406
- hint: "Continue polling for magic link completion.",
407
- timing: { suggestedPollIntervalMs: 5_000, maxWaitMs: 300_000 },
408
- })),
415
+ }, "Magic link continuation failed", ctx, input),
409
416
  poll: (ctx) => runCeremonyHandler(async () => {
410
417
  const state = getNestedRecord(ctx, MAGIC_LINK_KEY);
411
418
  const email = getString(state, "email");
@@ -431,10 +438,10 @@ export function createMagicLinkCeremony(options) {
431
438
  });
432
439
  }, "Magic link polling failed", ctx),
433
440
  abort: async () => validateCeremonyOutput(createTurn("abort", { hint: "Magic link flow aborted." })),
434
- };
441
+ });
435
442
  }
436
443
  export function createFormCeremony(options) {
437
- return {
444
+ return defineAuthFlow({
438
445
  start: async () => validateCeremonyOutput(buildJsonSchemaForm(options.schema, options.hint ?? "Provide the required input to continue.")),
439
446
  continue: (ctx, input = {}) => runCeremonyHandler(async () => {
440
447
  const { prevalidate } = await import("../runtime/prevalidate.js");
@@ -452,7 +459,7 @@ export function createFormCeremony(options) {
452
459
  });
453
460
  }, "Form submission failed", ctx, input),
454
461
  abort: async () => validateCeremonyOutput(createTurn("abort", { hint: "Form ceremony aborted." })),
455
- };
462
+ });
456
463
  }
457
464
  export function combineCeremonies(...ceremonies) {
458
465
  function getStage(ctx) {
@@ -504,7 +511,7 @@ export function combineCeremonies(...ceremonies) {
504
511
  }
505
512
  export function createSwitchCeremony(options) {
506
513
  const choiceKeys = Object.keys(options.choices);
507
- return {
514
+ return defineAuthFlow({
508
515
  start: async () => validateCeremonyOutput(createTurn("multi_choice", {
509
516
  data: { choices: choiceKeys },
510
517
  hint: options.prompt ?? "Choose an authentication method.",
@@ -550,5 +557,5 @@ export function createSwitchCeremony(options) {
550
557
  }
551
558
  return createTurn("abort", { hint: "Switch ceremony aborted." });
552
559
  }, "Switch ceremony abort failed", ctx),
553
- };
560
+ });
554
561
  }
@@ -1,6 +1,6 @@
1
1
  FROM oven/bun:1.2-alpine
2
2
  WORKDIR /provider
3
- COPY package.json bun.lockb* ./
3
+ COPY package.json bun.lock ./
4
4
  RUN bun install --frozen-lockfile
5
5
  COPY . .
6
6
  EXPOSE 3000
@@ -1,9 +1,9 @@
1
- import { defineProvider } from "@apifuse/provider-sdk/provider";
1
+ import { defineProvider, type ProviderContextOf } from "@apifuse/provider-sdk/provider";
2
2
 
3
3
  import { providerMeta } from "./meta";
4
4
  import { operations } from "./operations";
5
5
 
6
- export default defineProvider({
6
+ const buildProvider = defineProvider({
7
7
  id: "{{PROVIDER_ID}}",
8
8
  version: "1.0.0",
9
9
  runtime: "{{RUNTIME}}"{{BROWSER_BLOCK}},
@@ -11,5 +11,8 @@ export default defineProvider({
11
11
  reviewed: "community",
12
12
  {{SECRETS_BLOCK}}{{CREDENTIAL_BLOCK}}auth: {{AUTH_BLOCK}},
13
13
  meta: providerMeta,
14
- operations: operations,
15
14
  });
15
+
16
+ export type ProviderContext = ProviderContextOf<typeof buildProvider>;
17
+
18
+ export default buildProvider({ operations });
@@ -1,8 +1,9 @@
1
1
  import { defineOperation } from "@apifuse/provider-sdk/provider";
2
+ import type { ProviderContext } from "../index";
2
3
 
3
4
  import { pingInputSchema, pingOutputSchema } from "../schemas/ping";
4
5
 
5
- export const pingOperation = defineOperation({
6
+ export const pingOperation = defineOperation<ProviderContext>()({
6
7
  descriptionKey: "operations.ping.description",
7
8
  input: pingInputSchema,
8
9
  output: pingOutputSchema,
package/dist/define.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AuthConfig, BrowserEngine, ContextDeclaration, CredentialDeclaration, HealthJourneyDefinition, HealthJourneySchedule, HealthScheduleRandomization, InferSchemaOutput, NativeProviderConfig, OperationDefinition, OperationHandlerResult, OperationHttpStreamTransport, OperationSseTransport, OperationWebSocketTransport, ProviderAccessConfig, ProviderDefinition, ProviderOcrConfig, ProviderDeploymentOverrides, ProviderHealthMonitorConfig, ProviderProxyConfig, ProviderPublicProfile, ProviderResolverConfig, ProviderReviewed, ProviderSecretDeclaration, ProviderStreamEvent, ProviderSttConfig, SchemaLike, SmsOtpMatcherDefinition, StealthPlatform } from "./types.js";
1
+ import type { AuthConfig, BrowserEngine, ContextDeclaration, CredentialDeclaration, HealthJourneyDefinition, HealthJourneySchedule, HealthScheduleRandomization, InferSchemaOutput, NativeProviderConfig, OperationDefinition, OperationHandlerResult, OperationHttpStreamTransport, OperationSseTransport, OperationWebSocketTransport, ProviderAccessConfig, ProviderDefinition, ProviderContext, ProviderContextFor, ProviderOcrConfig, ProviderDeploymentOverrides, ProviderHealthMonitorConfig, ProviderProxyConfig, ProviderPublicProfile, ProviderResolverConfig, ProviderReviewed, ProviderSecretDeclaration, ProviderStreamEvent, ProviderSttConfig, SchemaLike, SmsOtpMatcherDefinition, StealthPlatform } from "./types.js";
2
2
  type ProviderImplementationSourceAccess = "official_api" | "private_api" | "browser_flow" | "hybrid";
3
3
  type ProviderImplementationCredentialStrategy = "apifuse_managed" | "workspace_secret" | "user_oauth" | "user_session" | "none";
4
4
  interface ProviderImplementationProfile {
@@ -10,36 +10,39 @@ interface ProviderImplementationProfile {
10
10
  }
11
11
  export declare const VALID_PROVIDER_RESOLVER_VENDORS: readonly ["browser", "capsolver", "capmonster", "2captcha", "custom"];
12
12
  export declare const VALID_PROVIDER_CHALLENGE_KINDS: readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha", "cloudflare_interstitial", "aws_waf", "akamai_sec_cpt", "akamai_sensor"];
13
- type ProviderOperation = OperationDefinition<SchemaLike, SchemaLike>;
14
- type OperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<OperationDefinition<TInput, TOutput>, "handler"> & {
15
- handler(ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0], input: InferSchemaOutput<TInput>): OperationHandlerResult<InferSchemaOutput<TOutput>> | Promise<OperationHandlerResult<InferSchemaOutput<TOutput>>>;
13
+ type ProviderOperation = OperationDefinition<any, any, any>;
14
+ type OperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike, TContext = ProviderContext> = Omit<OperationDefinition<TInput, TOutput, TContext>, "handler"> & {
15
+ handler(ctx: TContext, input: InferSchemaOutput<TInput>): OperationHandlerResult<InferSchemaOutput<TOutput>> | Promise<OperationHandlerResult<InferSchemaOutput<TOutput>>>;
16
16
  };
17
- type OperationMapConfig<TOperations extends Record<string, ProviderOperation>> = {
18
- [K in keyof TOperations]: TOperations[K] extends OperationDefinition<infer TInput, infer TOutput> ? OperationConfig<TInput, TOutput> | OperationDefinition<TInput, TOutput> : never;
17
+ type OperationMapConfig<TOperations extends Record<string, ProviderOperation>, TContext = ProviderContext> = {
18
+ [K in keyof TOperations]: TOperations[K] extends OperationDefinition<infer TInput, infer TOutput> ? OperationConfig<TInput, TOutput, TContext> | OperationDefinition<TInput, TOutput, TContext> : never;
19
19
  };
20
- type StreamOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = SseOperationConfig<TInput, TOutput> | HttpStreamOperationConfig<TInput, TOutput> | WebSocketOperationConfig<TInput, TOutput>;
21
- type SseOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<OperationConfig<TInput, TOutput>, "handler" | "transport"> & {
20
+ type StreamOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike, TContext = ProviderContext> = SseOperationConfig<TInput, TOutput, TContext> | HttpStreamOperationConfig<TInput, TOutput, TContext> | WebSocketOperationConfig<TInput, TOutput, TContext>;
21
+ type SseOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike, TContext = ProviderContext> = Omit<OperationConfig<TInput, TOutput, TContext>, "handler" | "transport"> & {
22
22
  transport: OperationSseTransport;
23
- handler(ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0], input: InferSchemaOutput<TInput>): AsyncIterable<ProviderStreamEvent> | Promise<AsyncIterable<ProviderStreamEvent>>;
23
+ handler(ctx: TContext, input: InferSchemaOutput<TInput>): AsyncIterable<ProviderStreamEvent> | Promise<AsyncIterable<ProviderStreamEvent>>;
24
24
  };
25
- type HttpStreamOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<OperationConfig<TInput, TOutput>, "handler" | "transport"> & {
25
+ type HttpStreamOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike, TContext = ProviderContext> = Omit<OperationConfig<TInput, TOutput, TContext>, "handler" | "transport"> & {
26
26
  transport: OperationHttpStreamTransport;
27
- handler(ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0], input: InferSchemaOutput<TInput>): Response | ReadableStream<Uint8Array> | Promise<Response | ReadableStream<Uint8Array>>;
27
+ handler(ctx: TContext, input: InferSchemaOutput<TInput>): Response | ReadableStream<Uint8Array> | Promise<Response | ReadableStream<Uint8Array>>;
28
28
  };
29
- type WebSocketOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<OperationConfig<TInput, TOutput>, "handler" | "transport"> & {
29
+ type WebSocketOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike, TContext = ProviderContext> = Omit<OperationConfig<TInput, TOutput, TContext>, "handler" | "transport"> & {
30
30
  transport: OperationWebSocketTransport;
31
- handler(ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0], input: InferSchemaOutput<TInput>): Response | ReadableStream<Uint8Array> | Promise<Response | ReadableStream<Uint8Array>>;
31
+ handler(ctx: TContext, input: InferSchemaOutput<TInput>): Response | ReadableStream<Uint8Array> | Promise<Response | ReadableStream<Uint8Array>>;
32
32
  };
33
- type AuthStartNoInputGuard<TConfig> = TConfig extends {
33
+ type AuthStartHandlerNoInputGuard<TStart> = TStart extends (...args: infer TArgs) => unknown ? TArgs["length"] extends 0 | 1 ? unknown : {
34
+ "auth start handlers must not declare input parameters; return a form turn from start and receive user input in continue": never;
35
+ } : unknown;
36
+ export type AuthStartNoInputGuard<TConfig> = TConfig extends {
34
37
  auth?: {
35
38
  flow?: {
36
39
  start: infer TStart;
37
40
  };
38
41
  };
39
- } ? TStart extends (...args: infer TArgs) => unknown ? TArgs extends [unknown] ? unknown : {
40
- "auth start handlers must not declare input parameters; return a form turn from start and receive user input in continue": never;
41
- } : unknown : unknown;
42
- export interface ProviderConfig<TOperations extends Record<string, ProviderOperation>> {
42
+ } ? AuthStartHandlerNoInputGuard<TStart> : TConfig extends {
43
+ start: infer TStart;
44
+ } ? AuthStartHandlerNoInputGuard<TStart> : unknown;
45
+ export interface ProviderDeclaration {
43
46
  id: string;
44
47
  version: string;
45
48
  runtime: "standard" | "shared" | "browser";
@@ -50,6 +53,8 @@ export interface ProviderConfig<TOperations extends Record<string, ProviderOpera
50
53
  * resolves omitted fields against the runtime deployment profiles.
51
54
  */
52
55
  deployment?: ProviderDeploymentOverrides;
56
+ /** Declares that provider operations use the SDK HTTP client. */
57
+ http?: true;
53
58
  allowedHosts?: string[];
54
59
  native?: NativeProviderConfig;
55
60
  stealth?: {
@@ -64,11 +69,21 @@ export interface ProviderConfig<TOperations extends Record<string, ProviderOpera
64
69
  engine: BrowserEngine;
65
70
  };
66
71
  auth?: AuthConfig;
72
+ /** Declares that provider operations issue and consume SDK choice tokens. */
73
+ choice?: true;
67
74
  reviewed?: ProviderReviewed;
68
75
  access?: ProviderAccessConfig;
69
76
  secrets?: ProviderSecretDeclaration[];
77
+ /** Declares that provider operations read SDK-managed environment values. */
78
+ env?: true;
70
79
  credential?: CredentialDeclaration;
71
80
  context?: ContextDeclaration;
81
+ /** Declares that provider operations use SDK-managed persistent state. */
82
+ state?: true;
83
+ /** Declares that provider operations use the SDK provider cache. */
84
+ cache?: true;
85
+ /** Declares that provider operations access runtime-resolvable files. */
86
+ files?: true;
72
87
  meta: {
73
88
  displayName: string;
74
89
  displayNameKey?: string;
@@ -90,16 +105,15 @@ export interface ProviderConfig<TOperations extends Record<string, ProviderOpera
90
105
  publicSchemaFieldNames?: "normalized";
91
106
  };
92
107
  };
93
- operations: OperationMapConfig<TOperations>;
94
108
  healthMonitor?: ProviderHealthMonitorConfig;
95
109
  /** New name for `healthMonitor` (transitional alias); declaring both is a ValidationError. */
96
110
  healthProbe?: ProviderHealthMonitorConfig;
97
111
  healthJourneys?: readonly HealthJourneyDefinition[];
98
112
  }
99
- /** Define one provider operation with schema-driven handler inference. */
100
- export declare function defineOperation<TInput extends SchemaLike, TOutput extends SchemaLike>(operation: OperationConfig<TInput, TOutput>): OperationDefinition<TInput, TOutput>;
101
- /** Define a non-JSON provider operation with explicit transport metadata. */
102
- export declare function defineStreamOperation<TInput extends SchemaLike, TOutput extends SchemaLike>(operation: StreamOperationConfig<TInput, TOutput>): OperationDefinition<TInput, TOutput>;
113
+ /** Define one factored provider operation with schema-driven handler inference. */
114
+ export declare function defineOperation<TContext>(): <TInput extends SchemaLike, TOutput extends SchemaLike>(config: OperationConfig<TInput, TOutput, TContext>) => OperationDefinition<TInput, TOutput, TContext>;
115
+ /** Define a factored non-JSON operation with explicit transport metadata. */
116
+ export declare function defineStreamOperation<TContext>(): <TInput extends SchemaLike, TOutput extends SchemaLike>(config: StreamOperationConfig<TInput, TOutput, TContext>) => OperationDefinition<TInput, TOutput, TContext>;
103
117
  export declare function every(interval: string, options?: {
104
118
  jitter?: string;
105
119
  randomize?: HealthScheduleRandomization;
@@ -108,7 +122,14 @@ export declare function centered(maxOffset: string): HealthScheduleRandomization
108
122
  export declare function delayed(maxDelay: string): HealthScheduleRandomization;
109
123
  export declare function defineSmsOtpMatcher(config: Omit<SmsOtpMatcherDefinition, "extractOtp">): SmsOtpMatcherDefinition;
110
124
  export declare function defineHealthJourney(config: HealthJourneyDefinition): HealthJourneyDefinition;
111
- export declare function defineProvider<TOperations extends Record<string, ProviderOperation>, TConfig extends ProviderConfig<TOperations>>(config: TConfig & AuthStartNoInputGuard<TConfig>): ProviderDefinition & {
112
- operations: OperationMapConfig<TOperations>;
125
+ /** The second authoring phase for a declaration established by defineProvider. */
126
+ export type ProviderBuilder<TDeclaration extends ProviderDeclaration> = <TOperations extends Record<string, ProviderOperation>>(implementation: {
127
+ operations: OperationMapConfig<TOperations, ProviderContextFor<TDeclaration>>;
128
+ }) => ProviderDefinition & {
129
+ operations: OperationMapConfig<TOperations, ProviderContextFor<TDeclaration>>;
113
130
  };
131
+ /** Extract the declaration-derived operation context from a provider builder. */
132
+ export type ProviderContextOf<TBuilder> = TBuilder extends ProviderBuilder<infer TDeclaration> ? ProviderContextFor<TDeclaration> : never;
133
+ /** Establish a provider declaration before its operations are contextually typed. */
134
+ export declare function defineProvider<const TDeclaration extends ProviderDeclaration>(declaration: TDeclaration & Record<Exclude<keyof TDeclaration, keyof ProviderDeclaration>, never> & AuthStartNoInputGuard<TDeclaration>): ProviderBuilder<TDeclaration>;
114
135
  export {};