@apifuse/provider-sdk 2.2.0-beta.36 → 2.2.0-beta.38

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 (47) 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/cli/templates/provider/index.ts.tpl +6 -3
  9. package/dist/cli/templates/provider/operations/ping.ts.tpl +2 -1
  10. package/dist/declaration-validation.d.ts +2 -0
  11. package/dist/declaration-validation.js +29 -3
  12. package/dist/define.d.ts +39 -21
  13. package/dist/define.js +33 -9
  14. package/dist/health-scenario.d.ts +1842 -0
  15. package/dist/health-scenario.js +624 -0
  16. package/dist/index.d.ts +4 -2
  17. package/dist/index.js +1 -0
  18. package/dist/provider.d.ts +5 -1
  19. package/dist/provider.js +1 -0
  20. package/dist/runtime/browser.js +19 -11
  21. package/dist/runtime/resolver-public.d.ts +1 -1
  22. package/dist/runtime/resolver-public.js +1 -1
  23. package/dist/runtime/resolver-vendors/browser.js +57 -14
  24. package/dist/runtime/resolver-vendors/types.d.ts +9 -1
  25. package/dist/runtime/resolver-vendors/types.js +15 -0
  26. package/dist/runtime/resolver.d.ts +1 -0
  27. package/dist/runtime/resolver.js +13 -7
  28. package/dist/server/serve-implementation.d.ts +1 -1
  29. package/dist/server/serve-implementation.js +25 -0
  30. package/dist/server/types.d.ts +4 -4
  31. package/dist/types.d.ts +35 -25
  32. package/package.json +1 -1
  33. package/src/cli/templates/provider/index.ts.tpl +6 -3
  34. package/src/cli/templates/provider/operations/ping.ts.tpl +2 -1
  35. package/src/declaration-validation.ts +30 -3
  36. package/src/define.ts +144 -51
  37. package/src/health-scenario.ts +875 -0
  38. package/src/index.ts +78 -2
  39. package/src/provider.ts +81 -1
  40. package/src/runtime/browser.ts +34 -11
  41. package/src/runtime/resolver-public.ts +2 -0
  42. package/src/runtime/resolver-vendors/browser.ts +69 -11
  43. package/src/runtime/resolver-vendors/types.ts +21 -0
  44. package/src/runtime/resolver.ts +17 -5
  45. package/src/server/serve-implementation.ts +39 -1
  46. package/src/testing/run.ts +3 -3
  47. package/src/types.ts +52 -25
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.38
4
+
5
+ - Release candidate for main commit 448775077dbc9ab907209f1feee4e6e4e5ef4283.
6
+
7
+ ## 2.2.0-beta.37
8
+
9
+ - Release candidate for main commit aa14b268dffe1196f25c2b6b314598fe0896edec.
10
+
3
11
  ## 2.2.0-beta.36
4
12
 
5
13
  - Release candidate for main commit c6858f8b87d78b3b755adeb28982e875434be406.
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,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,
@@ -4,6 +4,8 @@ export declare const DECLARATION_INVALID_CODE = "DECLARATION_INVALID";
4
4
  export declare const DECLARATION_RULE_IDS: {
5
5
  readonly challengeShape: "credentials-challenge-shape";
6
6
  readonly journeyExecutable: "health-journey-executable";
7
+ readonly journeyRunScenarioExclusive: "health-journey-run-scenario-exclusive";
8
+ readonly journeyScenarioValid: "health-journey-scenario-valid";
7
9
  readonly schemaSerializable: "operation-schema-serializable";
8
10
  readonly proxyExplicitPolicy: "proxy-explicit-policy";
9
11
  readonly proxyVendorExclusive: "proxy-vendor-fields-exclusive";
@@ -1,9 +1,12 @@
1
1
  import { describeSchema } from "./contract-serialization.js";
2
2
  import { ProviderError } from "./errors.js";
3
+ import { HealthScenarioSchema } from "./health-scenario.js";
3
4
  export const DECLARATION_INVALID_CODE = "DECLARATION_INVALID";
4
5
  export const DECLARATION_RULE_IDS = {
5
6
  challengeShape: "credentials-challenge-shape",
6
7
  journeyExecutable: "health-journey-executable",
8
+ journeyRunScenarioExclusive: "health-journey-run-scenario-exclusive",
9
+ journeyScenarioValid: "health-journey-scenario-valid",
7
10
  schemaSerializable: "operation-schema-serializable",
8
11
  proxyExplicitPolicy: "proxy-explicit-policy",
9
12
  proxyVendorExclusive: "proxy-vendor-fields-exclusive",
@@ -35,15 +38,38 @@ function validateHealthDeclaration(provider, violations) {
35
38
  for (const [index, journey] of (provider.healthJourneys ?? []).entries()) {
36
39
  if (!journey || typeof journey !== "object")
37
40
  continue;
38
- if (typeof journey.run !== "function") {
41
+ const hasRun = journey.run !== undefined;
42
+ const hasScenario = journey.scenario !== undefined;
43
+ if (hasRun && hasScenario) {
44
+ const journeyPath = healthJourneyPath(journey, index);
45
+ violations.push({
46
+ ruleId: DECLARATION_RULE_IDS.journeyRunScenarioExclusive,
47
+ path: journeyPath,
48
+ message: "A health journey must declare exactly one of run or scenario, not both.",
49
+ fix: `Remove either ${journeyPath}.run or ${journeyPath}.scenario.`,
50
+ });
51
+ }
52
+ if (!hasRun && !hasScenario) {
39
53
  const journeyPath = healthJourneyPath(journey, index);
40
54
  violations.push({
41
55
  ruleId: DECLARATION_RULE_IDS.journeyExecutable,
42
56
  path: `${journeyPath}.run`,
43
- message: "coversOperations cannot provide health coverage without executable run logic.",
44
- fix: `Add an async run(ctx) implementation to ${journeyPath}.`,
57
+ message: "coversOperations cannot provide health coverage without run or a declarative scenario.",
58
+ fix: `Add an async run(ctx) implementation or a valid scenario to ${journeyPath}.`,
45
59
  });
46
60
  }
61
+ if (hasScenario) {
62
+ const parsed = HealthScenarioSchema.safeParse(journey.scenario);
63
+ if (!parsed.success) {
64
+ const journeyPath = healthJourneyPath(journey, index);
65
+ violations.push({
66
+ ruleId: DECLARATION_RULE_IDS.journeyScenarioValid,
67
+ path: `${journeyPath}.scenario`,
68
+ message: "scenario must conform to HealthScenario.",
69
+ fix: "Build the scenario with defineHealthScenario().",
70
+ });
71
+ }
72
+ }
47
73
  }
48
74
  // NOTE: healthCheck.cases[].enabled is intentionally NOT validated here.
49
75
  // self-test.ts reports a gated case as status "skipped" with skipReason
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,25 +10,25 @@ 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
33
  type AuthStartHandlerNoInputGuard<TStart> = TStart extends (...args: infer TArgs) => unknown ? TArgs["length"] extends 0 | 1 ? unknown : {
34
34
  "auth start handlers must not declare input parameters; return a form turn from start and receive user input in continue": never;
@@ -42,7 +42,7 @@ export type AuthStartNoInputGuard<TConfig> = TConfig extends {
42
42
  } ? AuthStartHandlerNoInputGuard<TStart> : TConfig extends {
43
43
  start: infer TStart;
44
44
  } ? AuthStartHandlerNoInputGuard<TStart> : unknown;
45
- export interface ProviderConfig<TOperations extends Record<string, ProviderOperation>> {
45
+ export interface ProviderDeclaration {
46
46
  id: string;
47
47
  version: string;
48
48
  runtime: "standard" | "shared" | "browser";
@@ -53,6 +53,8 @@ export interface ProviderConfig<TOperations extends Record<string, ProviderOpera
53
53
  * resolves omitted fields against the runtime deployment profiles.
54
54
  */
55
55
  deployment?: ProviderDeploymentOverrides;
56
+ /** Declares that provider operations use the SDK HTTP client. */
57
+ http?: true;
56
58
  allowedHosts?: string[];
57
59
  native?: NativeProviderConfig;
58
60
  stealth?: {
@@ -67,11 +69,21 @@ export interface ProviderConfig<TOperations extends Record<string, ProviderOpera
67
69
  engine: BrowserEngine;
68
70
  };
69
71
  auth?: AuthConfig;
72
+ /** Declares that provider operations issue and consume SDK choice tokens. */
73
+ choice?: true;
70
74
  reviewed?: ProviderReviewed;
71
75
  access?: ProviderAccessConfig;
72
76
  secrets?: ProviderSecretDeclaration[];
77
+ /** Declares that provider operations read SDK-managed environment values. */
78
+ env?: true;
73
79
  credential?: CredentialDeclaration;
74
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;
75
87
  meta: {
76
88
  displayName: string;
77
89
  displayNameKey?: string;
@@ -93,16 +105,15 @@ export interface ProviderConfig<TOperations extends Record<string, ProviderOpera
93
105
  publicSchemaFieldNames?: "normalized";
94
106
  };
95
107
  };
96
- operations: OperationMapConfig<TOperations>;
97
108
  healthMonitor?: ProviderHealthMonitorConfig;
98
109
  /** New name for `healthMonitor` (transitional alias); declaring both is a ValidationError. */
99
110
  healthProbe?: ProviderHealthMonitorConfig;
100
111
  healthJourneys?: readonly HealthJourneyDefinition[];
101
112
  }
102
- /** Define one provider operation with schema-driven handler inference. */
103
- export declare function defineOperation<TInput extends SchemaLike, TOutput extends SchemaLike>(operation: OperationConfig<TInput, TOutput>): OperationDefinition<TInput, TOutput>;
104
- /** Define a non-JSON provider operation with explicit transport metadata. */
105
- 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>;
106
117
  export declare function every(interval: string, options?: {
107
118
  jitter?: string;
108
119
  randomize?: HealthScheduleRandomization;
@@ -111,7 +122,14 @@ export declare function centered(maxOffset: string): HealthScheduleRandomization
111
122
  export declare function delayed(maxDelay: string): HealthScheduleRandomization;
112
123
  export declare function defineSmsOtpMatcher(config: Omit<SmsOtpMatcherDefinition, "extractOtp">): SmsOtpMatcherDefinition;
113
124
  export declare function defineHealthJourney(config: HealthJourneyDefinition): HealthJourneyDefinition;
114
- export declare function defineProvider<TOperations extends Record<string, ProviderOperation>, TConfig extends ProviderConfig<TOperations>>(config: TConfig & AuthStartNoInputGuard<TConfig>): ProviderDefinition & {
115
- 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>>;
116
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>;
117
135
  export {};
package/dist/define.js CHANGED
@@ -449,13 +449,17 @@ function authStartHasHiddenInput(start) {
449
449
  return false;
450
450
  return /\s=\s/.test(second);
451
451
  }
452
- /** Define one provider operation with schema-driven handler inference. */
453
- export function defineOperation(operation) {
454
- return operation;
452
+ /** Define one factored provider operation with schema-driven handler inference. */
453
+ export function defineOperation() {
454
+ return function operation(config) {
455
+ return config;
456
+ };
455
457
  }
456
- /** Define a non-JSON provider operation with explicit transport metadata. */
457
- export function defineStreamOperation(operation) {
458
- return operation;
458
+ /** Define a factored non-JSON operation with explicit transport metadata. */
459
+ export function defineStreamOperation() {
460
+ return function streamOperation(config) {
461
+ return config;
462
+ };
459
463
  }
460
464
  function assertObjectConfig(value) {
461
465
  if (!value || typeof value !== "object") {
@@ -558,6 +562,11 @@ function validateProviderShape(config) {
558
562
  assertRequiredField(config, "operations", String(config.id));
559
563
  if (typeof config.runtime === "string")
560
564
  assertLiteralField(config.runtime, "runtime", VALID_RUNTIMES, String(config.id));
565
+ if (config.native !== undefined && config.runtime === "browser") {
566
+ throw new ValidationError(`Provider "${String(config.id)}" cannot declare capability "native" with runtime "browser"`, {
567
+ fix: 'Use runtime: "standard" or runtime: "shared", or remove the native declaration.',
568
+ });
569
+ }
561
570
  const auth = config.auth;
562
571
  if (auth && typeof auth === "object" && "mode" in auth && typeof auth.mode === "string")
563
572
  assertLiteralField(auth.mode, "auth.mode", VALID_AUTH_MODES, String(config.id));
@@ -780,7 +789,9 @@ function validateProviderResolver(config) {
780
789
  });
781
790
  }
782
791
  rejectUnknownFields(resolver, new Set(["vendors", "kinds", "clientProfile"]), "resolver", config.id);
783
- validateResolverLiteralArray(resolver.vendors, "resolver.vendors", VALID_PROVIDER_RESOLVER_VENDORS, config.id);
792
+ if (resolver.vendors !== undefined) {
793
+ validateResolverLiteralArray(resolver.vendors, "resolver.vendors", VALID_PROVIDER_RESOLVER_VENDORS, config.id);
794
+ }
784
795
  validateResolverLiteralArray(resolver.kinds, "resolver.kinds", VALID_PROVIDER_CHALLENGE_KINDS, config.id);
785
796
  if (resolver.clientProfile !== undefined &&
786
797
  (typeof resolver.clientProfile !== "string" || !resolver.clientProfile.trim())) {
@@ -1402,6 +1413,7 @@ const HEALTH_JOURNEY_FIELDS = new Set([
1402
1413
  "manualTrigger",
1403
1414
  "steps",
1404
1415
  "run",
1416
+ "scenario",
1405
1417
  ]);
1406
1418
  const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set(["kind", "interval", "jitter", "randomize"]);
1407
1419
  const HEALTH_JOURNEY_STEP_FIELDS = new Set([
@@ -1824,6 +1836,10 @@ function validateHealthJourneys(providerId, operations, healthJourneys) {
1824
1836
  }
1825
1837
  if (journey.manualTrigger !== undefined)
1826
1838
  validateHealthJourneyManualTrigger(providerId, journey.id, journey.manualTrigger);
1839
+ if (journey.scenario !== undefined && journey.smsMatchers !== undefined)
1840
+ throw new ValidationError(`Provider "${providerId}" healthJourneys.${journey.id}.smsMatchers is not allowed on declarative scenarios.`);
1841
+ if (journey.scenario !== undefined && journey.requiredSecrets !== undefined)
1842
+ throw new ValidationError(`Provider "${providerId}" healthJourneys.${journey.id}.requiredSecrets is not allowed on declarative scenarios.`);
1827
1843
  if (journey.timeout !== undefined)
1828
1844
  assertIsoDuration(journey.timeout, `Provider "${providerId}" healthJourneys.${journey.id}.timeout`);
1829
1845
  if (journey.cooldown !== undefined)
@@ -1933,7 +1949,15 @@ function validateProviderDeployment(providerId, deployment) {
1933
1949
  fix: 'Pass deployment: { runtime: "shared" | "dedicated" | "browser", ... } or remove the field',
1934
1950
  });
1935
1951
  }
1936
- export function defineProvider(config) {
1952
+ /** Establish a provider declaration before its operations are contextually typed. */
1953
+ export function defineProvider(declaration) {
1954
+ const buildProvider = (implementation) => finalizeProvider({
1955
+ ...declaration,
1956
+ ...implementation,
1957
+ });
1958
+ return buildProvider;
1959
+ }
1960
+ function finalizeProvider(config) {
1937
1961
  validateProviderShape(config);
1938
1962
  const operations = resolveOperationFixtureRequests(config.operations);
1939
1963
  if (!CONNECTOR_ID_REGEX.test(config.id))
@@ -2000,7 +2024,7 @@ export function defineProvider(config) {
2000
2024
  credential: config.credential,
2001
2025
  context: config.context,
2002
2026
  meta: config.meta,
2003
- operations,
2027
+ operations: operations,
2004
2028
  // Transitional healthMonitor → healthProbe alias: mirror whichever field
2005
2029
  // was declared onto both so old and new consumers keep working.
2006
2030
  healthMonitor: config.healthMonitor ?? config.healthProbe,