@apifuse/provider-sdk 2.2.0-beta.57 → 2.2.0-beta.59

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 (107) hide show
  1. package/AUTHORING.md +9 -0
  2. package/CHANGELOG.md +8 -0
  3. package/README.md +22 -5
  4. package/SUBMISSION.md +2 -1
  5. package/bin/apifuse-pack-types.ts +49 -0
  6. package/bin/apifuse-record.ts +6 -0
  7. package/dist/cli/templates/provider/README.md.tpl +19 -5
  8. package/dist/config/loader.d.ts +7 -0
  9. package/dist/config/loader.js +30 -6
  10. package/dist/declaration-validation.d.ts +2 -1
  11. package/dist/declaration-validation.js +39 -0
  12. package/dist/define.d.ts +5 -4
  13. package/dist/define.js +25 -1
  14. package/dist/engine.d.ts +16 -0
  15. package/dist/engine.js +58 -1
  16. package/dist/error-resolution.js +13 -0
  17. package/dist/index.d.ts +6 -4
  18. package/dist/index.js +4 -2
  19. package/dist/lint.js +3 -3
  20. package/dist/provider.d.ts +3 -3
  21. package/dist/provider.js +2 -2
  22. package/dist/runtime/bounded-expiring-map.d.ts +21 -0
  23. package/dist/runtime/bounded-expiring-map.js +64 -0
  24. package/dist/runtime/cache.d.ts +2 -0
  25. package/dist/runtime/cache.js +8 -1
  26. package/dist/runtime/egress-lease.d.ts +63 -0
  27. package/dist/runtime/egress-lease.js +233 -0
  28. package/dist/runtime/http.js +1 -0
  29. package/dist/runtime/proxy-retry-policy.d.ts +4 -1
  30. package/dist/runtime/proxy-retry-policy.js +11 -2
  31. package/dist/runtime/request-telemetry.d.ts +7 -9
  32. package/dist/runtime/resolver-config.d.ts +7 -0
  33. package/dist/runtime/resolver-config.js +8 -0
  34. package/dist/runtime/resolver-public.d.ts +3 -1
  35. package/dist/runtime/resolver-public.js +1 -1
  36. package/dist/runtime/resolver-shared.d.ts +9 -0
  37. package/dist/runtime/resolver-shared.js +57 -0
  38. package/dist/runtime/resolver-telemetry.d.ts +134 -0
  39. package/dist/runtime/resolver-telemetry.js +204 -0
  40. package/dist/runtime/resolver-usage.d.ts +24 -0
  41. package/dist/runtime/resolver-usage.js +60 -0
  42. package/dist/runtime/resolver-vendors/bindings.d.ts +5 -0
  43. package/dist/runtime/resolver-vendors/bindings.js +5 -0
  44. package/dist/runtime/resolver-vendors/capsolver.d.ts +2 -2
  45. package/dist/runtime/resolver-vendors/capsolver.js +21 -9
  46. package/dist/runtime/resolver-vendors/hypersolutions.d.ts +27 -0
  47. package/dist/runtime/resolver-vendors/hypersolutions.js +369 -0
  48. package/dist/runtime/resolver-vendors/twocaptcha.d.ts +2 -2
  49. package/dist/runtime/resolver-vendors/twocaptcha.js +26 -10
  50. package/dist/runtime/resolver-vendors/types.d.ts +35 -5
  51. package/dist/runtime/resolver-vendors/types.js +20 -10
  52. package/dist/runtime/resolver.d.ts +5 -1
  53. package/dist/runtime/resolver.js +397 -109
  54. package/dist/runtime/stealth-akamai-sbsd.d.ts +54 -0
  55. package/dist/runtime/stealth-akamai-sbsd.js +142 -0
  56. package/dist/runtime/stealth.js +547 -53
  57. package/dist/schema.d.ts +9 -0
  58. package/dist/schema.js +16 -0
  59. package/dist/server/index.d.ts +2 -0
  60. package/dist/server/index.js +1 -0
  61. package/dist/server/self-test.d.ts +4 -0
  62. package/dist/server/self-test.js +10 -1
  63. package/dist/server/serve-implementation.d.ts +7 -1
  64. package/dist/server/serve-implementation.js +181 -53
  65. package/dist/server/types.d.ts +6 -0
  66. package/dist/server/types.js +14 -0
  67. package/dist/stealth/profiles.d.ts +7 -1
  68. package/dist/stealth/profiles.js +9 -0
  69. package/dist/testing/run.js +6 -0
  70. package/dist/types.d.ts +115 -12
  71. package/package.json +1 -1
  72. package/src/cli/templates/provider/README.md.tpl +19 -5
  73. package/src/config/loader.ts +34 -6
  74. package/src/declaration-validation.ts +47 -1
  75. package/src/define.ts +46 -3
  76. package/src/engine.ts +76 -1
  77. package/src/error-resolution.ts +13 -0
  78. package/src/index.ts +32 -1
  79. package/src/lint.ts +3 -3
  80. package/src/provider.ts +9 -0
  81. package/src/runtime/bounded-expiring-map.ts +67 -0
  82. package/src/runtime/cache.ts +10 -1
  83. package/src/runtime/egress-lease.ts +336 -0
  84. package/src/runtime/http.ts +1 -0
  85. package/src/runtime/proxy-retry-policy.ts +12 -1
  86. package/src/runtime/request-telemetry.ts +5 -7
  87. package/src/runtime/resolver-config.ts +12 -0
  88. package/src/runtime/resolver-public.ts +21 -0
  89. package/src/runtime/resolver-shared.ts +75 -0
  90. package/src/runtime/resolver-telemetry.ts +362 -0
  91. package/src/runtime/resolver-usage.ts +96 -0
  92. package/src/runtime/resolver-vendors/bindings.ts +5 -0
  93. package/src/runtime/resolver-vendors/capsolver.ts +22 -3
  94. package/src/runtime/resolver-vendors/hypersolutions.ts +480 -0
  95. package/src/runtime/resolver-vendors/twocaptcha.ts +26 -12
  96. package/src/runtime/resolver-vendors/types.ts +60 -12
  97. package/src/runtime/resolver.ts +465 -124
  98. package/src/runtime/stealth-akamai-sbsd.ts +207 -0
  99. package/src/runtime/stealth.ts +727 -95
  100. package/src/schema.ts +17 -0
  101. package/src/server/index.ts +21 -0
  102. package/src/server/self-test.ts +13 -0
  103. package/src/server/serve-implementation.ts +268 -48
  104. package/src/server/types.ts +14 -0
  105. package/src/stealth/profiles.ts +10 -0
  106. package/src/testing/run.ts +6 -0
  107. package/src/types.ts +130 -22
package/src/schema.ts CHANGED
@@ -83,6 +83,8 @@ export function safeParseSchemaSync(
83
83
  export const APIFUSE_SENSITIVE_META_KEY = "x-apifuse-sensitive";
84
84
  export const APIFUSE_SENSITIVE_KIND_META_KEY = "x-apifuse-sensitive-kind";
85
85
  export const APIFUSE_DESCRIPTION_KEY_META_KEY = "x-apifuse-description-key";
86
+ export const APIFUSE_CONTENT_TRUST_META_KEY = "x-apifuse-content-trust";
87
+ export const APIFUSE_CONTENT_PROVENANCE_META_KEY = "x-apifuse-content-provenance";
86
88
  export const APIFUSE_REDACTION_MARKER = "<redacted>";
87
89
 
88
90
  export type SensitivePathSegment = string | "*";
@@ -127,6 +129,21 @@ export function describeKey<TSchema extends ZodType>(
127
129
  });
128
130
  }
129
131
 
132
+ /**
133
+ * Mark an output field as externally authored text (listing bodies, reviews,
134
+ * posts) that the provider relays verbatim. Sets both content-trust JSON Schema
135
+ * meta keys on the field node so the platform can enforce the boundary when it
136
+ * assembles tool output; validation and the parsed value are unchanged.
137
+ */
138
+ export function untrustedContent<TSchema extends ZodType>(schema: TSchema): TSchema {
139
+ const metadata = schema.meta() ?? {};
140
+ return schema.meta({
141
+ ...metadata,
142
+ [APIFUSE_CONTENT_TRUST_META_KEY]: "untrusted",
143
+ [APIFUSE_CONTENT_PROVENANCE_META_KEY]: "external",
144
+ });
145
+ }
146
+
130
147
  declare module "zod" {
131
148
  interface ZodType {
132
149
  describeKey(key: ProviderLocaleKey | string): this;
@@ -23,6 +23,27 @@ export {
23
23
  type TelemetryKey,
24
24
  type TenantNeutral,
25
25
  } from "../runtime/request-telemetry.js";
26
+ export {
27
+ ResolverTelemetryCollector,
28
+ type ResolverAttemptSample,
29
+ type ResolverCacheReadTelemetryEvent,
30
+ type ResolverCacheWriteTelemetryEvent,
31
+ type ResolverFailoverTelemetryEvent,
32
+ type ResolverIdentityTelemetryEvent,
33
+ type ResolverOutcomeTelemetryEvent,
34
+ type ResolverTelemetryAttemptOutcome,
35
+ type ResolverTelemetryCacheStatus,
36
+ type ResolverTelemetryCacheWriteReason,
37
+ type ResolverTelemetryErrorClass,
38
+ type ResolverTelemetryHeaderPayload,
39
+ type ResolverTelemetryIdentitySource,
40
+ type ResolverTelemetryLogPayload,
41
+ type ResolverTelemetryOutcome,
42
+ type ResolverTelemetryPhase,
43
+ type ResolverTelemetrySink,
44
+ type ResolverVendorAttemptTelemetryEvent,
45
+ } from "../runtime/resolver-telemetry.js";
46
+ export type { ResolverVendorUnavailableReason } from "../runtime/resolver-vendors/types.js";
26
47
  export type { Span, TraceContext } from "../runtime/trace.js";
27
48
  export {
28
49
  createServerApp,
@@ -117,6 +117,8 @@ export type SelfTestAuthFlowInvoke = (args: {
117
117
  externalRef?: string;
118
118
  input?: Record<string, unknown>;
119
119
  context?: Record<string, unknown>;
120
+ /** Opaque engine state from the previous turn (`engine` in the auth response), sent verbatim. */
121
+ engine?: { egressLease: string };
120
122
  signal?: AbortSignal;
121
123
  }) => Promise<{
122
124
  status: number;
@@ -343,6 +345,7 @@ export function createSelfTestAuthFlowInvoke(app: {
343
345
  externalRef,
344
346
  input,
345
347
  context,
348
+ engine,
346
349
  signal,
347
350
  }) => {
348
351
  const response = await app.request(`/auth/${route}`, {
@@ -351,10 +354,14 @@ export function createSelfTestAuthFlowInvoke(app: {
351
354
  body: JSON.stringify({
352
355
  requestId,
353
356
  flowId,
357
+ // Engine ceremony leases are scoped to a tenant; the self-test has no
358
+ // customer tenant, so it presents a fixed one that never collides.
359
+ tenantId: "self-test",
354
360
  ...(connectionId ? { connectionId } : {}),
355
361
  ...(externalRef ? { externalRef } : {}),
356
362
  ...(input ? { input } : {}),
357
363
  ...(context ? { context } : {}),
364
+ ...(engine ? { engine } : {}),
358
365
  }),
359
366
  ...(signal ? { signal } : {}),
360
367
  });
@@ -550,6 +557,7 @@ type ParsedAuthFlowTurn =
550
557
  ok: true;
551
558
  turn: { kind: string; data?: unknown; expectedInput?: unknown };
552
559
  contextPatch?: Record<string, unknown>;
560
+ engine?: { egressLease: string };
553
561
  }
554
562
  | { ok: false; code: string; message: string; httpStatus: number };
555
563
 
@@ -578,6 +586,7 @@ function parseAuthFlowResponse(result: { status: number; body: unknown }): Parse
578
586
  };
579
587
  }
580
588
  const contextPatch = objectProperty(result.body, "contextPatch");
589
+ const egressLease = objectProperty(objectProperty(result.body, "engine"), "egressLease");
581
590
  return {
582
591
  ok: true,
583
592
  turn: {
@@ -588,6 +597,7 @@ function parseAuthFlowResponse(result: { status: number; body: unknown }): Parse
588
597
  ...(contextPatch && typeof contextPatch === "object" && !Array.isArray(contextPatch)
589
598
  ? { contextPatch: contextPatch as Record<string, unknown> }
590
599
  : {}),
600
+ ...(typeof egressLease === "string" && egressLease ? { engine: { egressLease } } : {}),
591
601
  };
592
602
  }
593
603
 
@@ -777,6 +787,9 @@ async function materializeFlowCredential(
777
787
  ...(options.externalRef ? { externalRef: options.externalRef } : {}),
778
788
  input: submitInputs,
779
789
  ...(Object.keys(flowContext).length > 0 ? { context: flowContext } : {}),
790
+ // The ceremony egress lease is engine state, not flow context: carry it the
791
+ // way the gateway does so the continue turn stays on the start turn's egress.
792
+ ...(started.engine ? { engine: started.engine } : {}),
780
793
  ...(options.signal ? { signal: options.signal } : {}),
781
794
  }),
782
795
  );
@@ -29,6 +29,7 @@ import {
29
29
  isTransportError,
30
30
  isValidationError,
31
31
  ProviderError,
32
+ SDKError,
32
33
  type ProviderErrorObservability,
33
34
  type ProviderErrorOptions,
34
35
  } from "../errors.js";
@@ -55,6 +56,10 @@ import {
55
56
  PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
56
57
  } from "../runtime/choice.js";
57
58
  import { createCredentialContext } from "../runtime/credential.js";
59
+ import {
60
+ createCeremonyEgressLeaseRuntime,
61
+ ENGINE_CEREMONY_EGRESS_LEASE,
62
+ } from "../runtime/egress-lease.js";
58
63
  import { createEnvContext } from "../runtime/env.js";
59
64
  import { executeOperation } from "../runtime/executor.js";
60
65
  import { createHttpClient } from "../runtime/http.js";
@@ -80,9 +85,15 @@ import {
80
85
  type TenantNeutral,
81
86
  type TenantOpaqueKeys,
82
87
  } from "../runtime/request-telemetry.js";
88
+ import {
89
+ ResolverTelemetryCollector,
90
+ type ResolverTelemetryLogPayload,
91
+ } from "../runtime/resolver-telemetry.js";
83
92
  import type * as ResolverRuntimeModule from "../runtime/resolver.js";
84
93
  import {
85
94
  createUnsupportedResolverClient,
95
+ bindResolverTelemetry,
96
+ inheritResolverTelemetryBinding,
86
97
  type ResolverSolveWithRecorder,
87
98
  } from "../runtime/resolver-shared.js";
88
99
  import {
@@ -96,9 +107,11 @@ import {
96
107
  } from "../runtime/state.js";
97
108
  import { StealthCookieJar } from "../runtime/stealth-cookies.js";
98
109
  import type * as StealthRuntimeModule from "../runtime/stealth.js";
110
+ import type { StealthChallengeRuntime } from "../runtime/stealth-akamai-sbsd.js";
99
111
  import { createSttClientFromEnv } from "../runtime/stt.js";
100
112
  import {
101
113
  createTraceContext,
114
+ getTraceRecorder,
102
115
  type TraceContext as RuntimeTraceContext,
103
116
  type TraceRecorder,
104
117
  updateTraceContextExportMetadata,
@@ -513,6 +526,9 @@ function createLazyStealthClient(
513
526
  async fetch(...args) {
514
527
  return (await getSession()).fetch(...args);
515
528
  },
529
+ async replayChallenged(...args) {
530
+ return (await getSession()).replayChallenged(...args);
531
+ },
516
532
  redirects: {
517
533
  async run(...args) {
518
534
  return (await getSession()).redirects.run(...args);
@@ -553,7 +569,7 @@ function bindResolverSignalWithoutRuntime(
553
569
  defaultSignal: AbortSignal | undefined,
554
570
  ): ResolverContext {
555
571
  if (!defaultSignal) return resolver;
556
- return {
572
+ const boundResolver: ResolverContext = {
557
573
  solve(challenge, signal = defaultSignal, traceRecorder?: TraceRecorder) {
558
574
  // Forward the instrumentation trace recorder so resolver.vendor.* spans
559
575
  // survive this wrapper. See ResolverSolveWithRecorder in resolver-shared.ts.
@@ -564,6 +580,8 @@ function bindResolverSignalWithoutRuntime(
564
580
  );
565
581
  },
566
582
  };
583
+ inheritResolverTelemetryBinding(resolver, boundResolver);
584
+ return boundResolver;
567
585
  }
568
586
 
569
587
  function getProviderStealthBaseUrl(provider: ProviderDefinition): string | undefined {
@@ -579,6 +597,99 @@ function getProviderStealthProfile(provider: ProviderDefinition) {
579
597
  return provider.stealth ? getStealthProfile(provider.stealth) : undefined;
580
598
  }
581
599
 
600
+ type ResolverRuntimeOptions = ResolverRuntimeModule.ResolverRuntimeOptions;
601
+
602
+ /** One option set for ctx.resolver and the automatic SBSD solve, so both share identity scope, cache, proxy intent, and telemetry. */
603
+ function createResolverRuntimeOptions(
604
+ provider: ProviderDefinition,
605
+ cache: ReturnType<typeof createProviderCache>,
606
+ identityScope: string,
607
+ proxyPolicy: ProviderProxyPolicy | undefined,
608
+ proxyClientOptions: Omit<
609
+ NonNullable<ResolverRuntimeOptions["proxyIntent"]>,
610
+ "mode" | "userAgent"
611
+ >,
612
+ stealthProfile: { readonly userAgent: string } | undefined,
613
+ telemetry: ResolverTelemetryCollector,
614
+ ): ResolverRuntimeOptions {
615
+ return {
616
+ allowedHosts: provider.allowedHosts,
617
+ cache,
618
+ telemetry,
619
+ identityScope,
620
+ ...(proxyPolicy
621
+ ? {
622
+ proxyIntent: {
623
+ mode: proxyPolicy.mode,
624
+ ...proxyClientOptions,
625
+ ...(stealthProfile ? { userAgent: stealthProfile.userAgent } : {}),
626
+ },
627
+ }
628
+ : {}),
629
+ };
630
+ }
631
+
632
+ /** Shared predicate for the SBSD challenge runtime and the ceremony egress lease. */
633
+ function declaresAkamaiSbsdResolver(provider: ProviderDefinition): boolean {
634
+ return provider.resolver?.kinds.some((kind) => kind === "akamai_sbsd") === true;
635
+ }
636
+
637
+ function createStealthChallengeDetection(
638
+ provider: ProviderDefinition,
639
+ resolverRuntime: typeof ResolverRuntimeModule | undefined,
640
+ resolverOverride: ResolverContext | undefined,
641
+ resolverOptions: ResolverRuntimeOptions,
642
+ trace: RuntimeTraceContext,
643
+ signal: AbortSignal | undefined,
644
+ ): StealthChallengeRuntime | undefined {
645
+ const resolverDeclared = declaresAkamaiSbsdResolver(provider);
646
+ const detectOnly = provider.stealth?.challengeDetection?.akamaiSbsd === true;
647
+ if (!resolverDeclared && !detectOnly) {
648
+ return undefined;
649
+ }
650
+ return {
651
+ akamaiSbsd: {
652
+ allowedHosts: provider.allowedHosts ?? [],
653
+ // The profile belongs to the SBSD resolver; a resolver declared for other kinds
654
+ // may name a different browser without affecting detect-only classification.
655
+ ...(resolverDeclared && provider.resolver?.clientProfile
656
+ ? { clientProfile: provider.resolver.clientProfile }
657
+ : {}),
658
+ ...(resolverDeclared && resolverRuntime
659
+ ? {
660
+ async solve(challenge, transport, solveSignal) {
661
+ const resolver = resolverRuntime.bindResolverSignal(
662
+ (resolverOverride
663
+ ? bindResolverTelemetry(resolverOverride, resolverOptions.telemetry)
664
+ : undefined) ??
665
+ resolverRuntime.createResolverClientFromEnv(provider.resolver, undefined, {
666
+ allowedHosts: resolverOptions.allowedHosts,
667
+ cache: resolverOptions.cache,
668
+ telemetry: resolverOptions.telemetry,
669
+ identityScope: resolverOptions.identityScope,
670
+ // No proxyIntent: the transport is already bound to the initiating
671
+ // request's lease, and a second identity resolution would allocate
672
+ // again (and fail closed without allocator credentials) for nothing
673
+ // the SBSD adapters consume.
674
+ createTransport: () => transport,
675
+ }),
676
+ signal,
677
+ ) as ResolverContext & Partial<ResolverSolveWithRecorder>;
678
+ // Same span and recorder threading as the instrumented ctx.resolver.solve.
679
+ const recorder = getTraceRecorder(trace);
680
+ const solve = () => resolver.solve(challenge, solveSignal, recorder ?? undefined);
681
+ return recorder
682
+ ? recorder.runSpan("resolver.solve", solve, {
683
+ attributes: { challenge_kind: challenge.kind },
684
+ })
685
+ : solve();
686
+ },
687
+ }
688
+ : {}),
689
+ },
690
+ };
691
+ }
692
+
582
693
  function isProductionProviderBrowserMode(provider: ProviderDefinition, env = process.env): boolean {
583
694
  if (provider.runtime !== "browser") {
584
695
  return false;
@@ -714,6 +825,7 @@ function providerSecretNames(provider: ProviderDefinition): string[] {
714
825
  type RequestScopeContext = {
715
826
  trace: RuntimeTraceContext;
716
827
  telemetry: RequestTelemetry;
828
+ resolverTelemetry: ResolverTelemetryCollector;
717
829
  };
718
830
 
719
831
  function createProviderContext(
@@ -741,16 +853,43 @@ function createProviderContext(
741
853
  proxyClientOptions.affinityKey,
742
854
  request.requestId,
743
855
  );
856
+ const cache = createProviderCache({ providerId: provider.id });
744
857
  let wrappedContext: ProviderContext | undefined;
858
+ const { capabilityModules } = options;
859
+ const resolverOptions = createResolverRuntimeOptions(
860
+ provider,
861
+ cache,
862
+ resolverIdentityScope,
863
+ proxyPolicy,
864
+ proxyClientOptions,
865
+ stealthProfile,
866
+ scope.resolverTelemetry,
867
+ );
868
+ const challengeRuntime = createStealthChallengeDetection(
869
+ provider,
870
+ capabilityModules.resolver,
871
+ options.resolver,
872
+ resolverOptions,
873
+ scope.trace,
874
+ signal,
875
+ );
745
876
  const stealthClientOptions = {
746
877
  upstream: proxyClientOptions.upstream,
747
878
  affinityKey: proxyClientOptions.affinityKey,
748
879
  telemetry: scope.telemetry.proxy,
749
880
  engineCredentials: engineProxyCredentials,
750
881
  ...(signal ? { signal } : {}),
751
- ...(provider.stealth ? { stealth: provider.stealth } : {}),
882
+ // A declared resolver without a `stealth` block still gets the default Chrome client
883
+ // (auth FlowContext is not capability-gated), so the runtime must ride along.
884
+ ...(provider.stealth || challengeRuntime
885
+ ? {
886
+ stealth: {
887
+ ...(provider.stealth ?? {}),
888
+ ...(challengeRuntime ? { challengeRuntime } : {}),
889
+ },
890
+ }
891
+ : {}),
752
892
  };
753
- const { capabilityModules } = options;
754
893
  const logStealthCleanupError = (error: unknown) =>
755
894
  logProviderCleanupError(
756
895
  options.logger,
@@ -777,7 +916,6 @@ function createProviderContext(
777
916
  headers: request.headers ?? {},
778
917
  };
779
918
  const requestState = state.forConnection(requestContext.connectionId);
780
- const cache = createProviderCache({ providerId: provider.id });
781
919
  const bindings: ProviderEngineBindingCandidates = {
782
920
  env,
783
921
  credential,
@@ -829,25 +967,20 @@ function createProviderContext(
829
967
  stt: options.stt ?? createSttClientFromEnv(provider.stt),
830
968
  resolver: capabilityModules.resolver
831
969
  ? capabilityModules.resolver.bindResolverSignal(
832
- options.resolver ??
833
- capabilityModules.resolver.createResolverClientFromEnv(provider.resolver, undefined, {
834
- allowedHosts: provider.allowedHosts,
835
- cache,
836
- identityScope: resolverIdentityScope,
837
- ...(proxyPolicy
838
- ? {
839
- proxyIntent: {
840
- mode: proxyPolicy.mode,
841
- ...proxyClientOptions,
842
- ...(stealthProfile ? { userAgent: stealthProfile.userAgent } : {}),
843
- },
844
- }
845
- : {}),
846
- }),
970
+ (options.resolver
971
+ ? bindResolverTelemetry(options.resolver, resolverOptions.telemetry)
972
+ : undefined) ??
973
+ capabilityModules.resolver.createResolverClientFromEnv(
974
+ provider.resolver,
975
+ undefined,
976
+ resolverOptions,
977
+ ),
847
978
  signal,
848
979
  )
849
980
  : bindResolverSignalWithoutRuntime(
850
- options.resolver ??
981
+ (options.resolver
982
+ ? bindResolverTelemetry(options.resolver, resolverOptions.telemetry)
983
+ : undefined) ??
851
984
  createUnsupportedResolverClient("Provider does not declare resolver capability"),
852
985
  signal,
853
986
  ),
@@ -927,6 +1060,7 @@ export function resolveAuthFlowProxyAffinityKey(
927
1060
  function createAuthFlowContext(
928
1061
  provider: ProviderDefinition,
929
1062
  request: AuthFlowRequest,
1063
+ route: AuthRoute,
930
1064
  options: ProviderServerRuntimeOptions,
931
1065
  state: ProviderRuntimeState,
932
1066
  scope: RequestScopeContext,
@@ -934,6 +1068,7 @@ function createAuthFlowContext(
934
1068
  ): {
935
1069
  context: FlowContext;
936
1070
  getPatch: () => Record<string, unknown | null> | undefined;
1071
+ getEngineState: () => AuthFlowSuccessResponse["engine"];
937
1072
  } {
938
1073
  const baseUrl = getProviderBaseUrl(provider);
939
1074
  const stealthBaseUrl = getProviderStealthBaseUrl(provider);
@@ -951,20 +1086,72 @@ function createAuthFlowContext(
951
1086
  telemetry: scope.telemetry.proxy,
952
1087
  engineCredentials: engineProxyCredentials,
953
1088
  };
1089
+ // The lease pins the proxy egress a challenged ceremony solved on, so only providers
1090
+ // that declare the akamai_sbsd resolver kind carry one (same predicate as the challenge
1091
+ // runtime). `abort` never evaluates the key or tenant: a user must be able to cancel a
1092
+ // stranded flow even when engine provisioning is broken, and an abort is not a
1093
+ // continuation of the ceremony the lease pins.
1094
+ const ceremonyEgressLease =
1095
+ route !== "abort" &&
1096
+ declaresAkamaiSbsdResolver(provider) &&
1097
+ proxyPolicy &&
1098
+ proxyPolicy.mode !== "disabled"
1099
+ ? createCeremonyEgressLeaseRuntime({
1100
+ tenantId: request.tenantId,
1101
+ providerId: provider.id,
1102
+ flowId: request.flowId,
1103
+ affinityKey: proxyClientOptions.affinityKey,
1104
+ ...(request.engine?.egressLease ? { handle: request.engine.egressLease } : {}),
1105
+ })
1106
+ : undefined;
1107
+ if (route !== "abort" && request.engine?.egressLease && !ceremonyEgressLease) {
1108
+ throw new SDKError(
1109
+ "An egress lease was supplied for an auth flow that does not use a ceremony lease",
1110
+ { code: "EGRESS_LEASE_INVALID" },
1111
+ );
1112
+ }
954
1113
  const resolverIdentityScope = resolveProviderResolverIdentityScope(
955
1114
  provider,
956
1115
  proxyClientOptions.affinityKey,
957
1116
  request.requestId,
958
1117
  );
1118
+ const cache = createProviderCache({ providerId: provider.id });
1119
+ const { capabilityModules } = options;
1120
+ const resolverOptions = createResolverRuntimeOptions(
1121
+ provider,
1122
+ cache,
1123
+ resolverIdentityScope,
1124
+ proxyPolicy,
1125
+ proxyClientOptions,
1126
+ stealthProfile,
1127
+ scope.resolverTelemetry,
1128
+ );
1129
+ const challengeRuntime = createStealthChallengeDetection(
1130
+ provider,
1131
+ capabilityModules.resolver,
1132
+ options.resolver,
1133
+ resolverOptions,
1134
+ scope.trace,
1135
+ signal,
1136
+ );
959
1137
  const stealthClientOptions = {
960
1138
  upstream: proxyClientOptions.upstream,
961
1139
  affinityKey: proxyClientOptions.affinityKey,
962
1140
  telemetry: scope.telemetry.proxy,
963
1141
  engineCredentials: engineProxyCredentials,
1142
+ ...(ceremonyEgressLease ? { [ENGINE_CEREMONY_EGRESS_LEASE]: ceremonyEgressLease } : {}),
964
1143
  ...(signal ? { signal } : {}),
965
- ...(provider.stealth ? { stealth: provider.stealth } : {}),
1144
+ // A declared resolver without a `stealth` block still gets the default Chrome client
1145
+ // (auth FlowContext is not capability-gated), so the runtime must ride along.
1146
+ ...(provider.stealth || challengeRuntime
1147
+ ? {
1148
+ stealth: {
1149
+ ...(provider.stealth ?? {}),
1150
+ ...(challengeRuntime ? { challengeRuntime } : {}),
1151
+ },
1152
+ }
1153
+ : {}),
966
1154
  };
967
- const { capabilityModules } = options;
968
1155
  const logStealthCleanupError = (error: unknown) =>
969
1156
  logProviderCleanupError(
970
1157
  options.logger,
@@ -983,8 +1170,6 @@ function createAuthFlowContext(
983
1170
  values: request.connection.secrets,
984
1171
  })
985
1172
  : undefined;
986
- const cache = createProviderCache({ providerId: provider.id });
987
-
988
1173
  const context: FlowContext = wrapWithInstrumentation({
989
1174
  flowId: request.flowId,
990
1175
  connectionId: resolveOperationConnectionId(request),
@@ -1026,25 +1211,20 @@ function createAuthFlowContext(
1026
1211
  stt: options.stt ?? createSttClientFromEnv(provider.stt),
1027
1212
  resolver: capabilityModules.resolver
1028
1213
  ? capabilityModules.resolver.bindResolverSignal(
1029
- options.resolver ??
1030
- capabilityModules.resolver.createResolverClientFromEnv(provider.resolver, undefined, {
1031
- allowedHosts: provider.allowedHosts,
1032
- cache,
1033
- identityScope: resolverIdentityScope,
1034
- ...(proxyPolicy
1035
- ? {
1036
- proxyIntent: {
1037
- mode: proxyPolicy.mode,
1038
- ...proxyClientOptions,
1039
- ...(stealthProfile ? { userAgent: stealthProfile.userAgent } : {}),
1040
- },
1041
- }
1042
- : {}),
1043
- }),
1214
+ (options.resolver
1215
+ ? bindResolverTelemetry(options.resolver, resolverOptions.telemetry)
1216
+ : undefined) ??
1217
+ capabilityModules.resolver.createResolverClientFromEnv(
1218
+ provider.resolver,
1219
+ undefined,
1220
+ resolverOptions,
1221
+ ),
1044
1222
  signal,
1045
1223
  )
1046
1224
  : bindResolverSignalWithoutRuntime(
1047
- options.resolver ??
1225
+ (options.resolver
1226
+ ? bindResolverTelemetry(options.resolver, resolverOptions.telemetry)
1227
+ : undefined) ??
1048
1228
  createUnsupportedResolverClient("Provider does not declare resolver capability"),
1049
1229
  signal,
1050
1230
  ),
@@ -1054,6 +1234,10 @@ function createAuthFlowContext(
1054
1234
  return {
1055
1235
  context,
1056
1236
  getPatch: flowContextStore.getPatch,
1237
+ getEngineState() {
1238
+ const egressLease = ceremonyEgressLease?.handle();
1239
+ return egressLease ? { egressLease } : undefined;
1240
+ },
1057
1241
  };
1058
1242
  }
1059
1243
 
@@ -1082,6 +1266,7 @@ type ProviderServerLogEventBase = ProviderRequestCost & {
1082
1266
  requestedProviderId?: string;
1083
1267
  status: number;
1084
1268
  proxy?: ProxyTelemetryLogPayload;
1269
+ resolver?: ResolverTelemetryLogPayload;
1085
1270
  };
1086
1271
 
1087
1272
  export type ProviderServerLogEvent =
@@ -1167,7 +1352,11 @@ export type ProviderServerOptions<TContext extends Partial<ProviderContext> = Pr
1167
1352
  stt?: SttContext;
1168
1353
  /** Optional OCR override for tests or custom hosts; local/prod normally resolves from env. */
1169
1354
  ocr?: OcrContext;
1170
- /** Optional resolver override for tests or custom hosts; local/prod normally resolves from env. */
1355
+ /**
1356
+ * Optional resolver override for tests or custom hosts; local/prod normally resolves from env.
1357
+ * SDK chains are reconstructed with each request's telemetry sink. An opaque host
1358
+ * resolver is reported as one custom invocation; its internal vendor work is not exposed.
1359
+ */
1171
1360
  resolver?: ResolverContext;
1172
1361
  /** Optional runtime state override for tests or custom hosts. Production resolves Redis from env and fails closed when unavailable. */
1173
1362
  state?: ProviderRuntimeState;
@@ -1888,8 +2077,10 @@ function createRequestScope(input: {
1888
2077
  })
1889
2078
  : createTraceContext();
1890
2079
  const proxyCollector = new ProxyTelemetryCollector();
2080
+ const resolverCollector = new ResolverTelemetryCollector();
1891
2081
  const telemetry = new RequestTelemetry(trace);
1892
2082
  telemetry.register(proxyCollector);
2083
+ telemetry.register(resolverCollector);
1893
2084
  let rootRunner: <T>(fn: () => Promise<T>) => Promise<T> = (fn) => fn();
1894
2085
  let resolveRoot!: (outcome: RequestTerminalOutcome) => void;
1895
2086
  const rootTerminal = new Promise<RequestTerminalOutcome>((resolve) => {
@@ -1967,6 +2158,7 @@ function createRequestScope(input: {
1967
2158
  const scope: RequestScope = {
1968
2159
  trace,
1969
2160
  telemetry,
2161
+ resolverTelemetry: resolverCollector,
1970
2162
  enrich(enrichment): void {
1971
2163
  if (terminalOutcome) return;
1972
2164
  if (enrichment.route !== undefined) details.route = enrichment.route;
@@ -2089,17 +2281,40 @@ function createRequestScope(input: {
2089
2281
  return scope;
2090
2282
  }
2091
2283
 
2284
+ // A raw `Response` result skips `toErrorResponse`, so its body stays
2285
+ // provider-owned, but the observability header is still SDK-owned: derive it
2286
+ // from the status. Only timeouts, rate limits, and 5xx are retryable;
2287
+ // `isRetryableCategory` would otherwise mark every other 4xx (`upstream_http`)
2288
+ // as retryable.
2289
+ function rawResponseErrorObservability(status: number): ErrorObservabilityDetails {
2290
+ return {
2291
+ category: categoryForStatus(status),
2292
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
2293
+ retryable: status === 408 || status === 429 || status >= 500,
2294
+ };
2295
+ }
2296
+
2297
+ // `rawStatusFallback` is decided by the caller from the outcome, not from a
2298
+ // missing snapshot: a failed outcome whose thrown value is `undefined` also has
2299
+ // no `errorObservability`, and its 500 carries the SDK envelope, not a raw body.
2092
2300
  function responseWithRequestScopeHeaders(
2093
2301
  response: Response,
2094
2302
  finished: RequestScopeFinishResult,
2303
+ rawStatusFallback: boolean,
2095
2304
  ): Response {
2096
2305
  const headers = new Headers(response.headers);
2097
2306
  headers.delete(PROVIDER_TELEMETRY_HEADER);
2098
2307
  if (finished.providerTelemetryHeader) {
2099
2308
  headers.set(PROVIDER_TELEMETRY_HEADER, finished.providerTelemetryHeader);
2100
2309
  }
2101
- if (finished.errorObservability) {
2102
- headers.set(ERROR_OBSERVABILITY_HEADER, JSON.stringify(finished.errorObservability));
2310
+ headers.delete(ERROR_OBSERVABILITY_HEADER);
2311
+ const errorObservability =
2312
+ finished.errorObservability ??
2313
+ (rawStatusFallback && response.status >= 400
2314
+ ? rawResponseErrorObservability(response.status)
2315
+ : undefined);
2316
+ if (errorObservability) {
2317
+ headers.set(ERROR_OBSERVABILITY_HEADER, JSON.stringify(errorObservability));
2103
2318
  }
2104
2319
  return new Response(response.body, {
2105
2320
  headers,
@@ -2118,6 +2333,7 @@ function finalizeRequestResponse(
2118
2333
  const finalResponse = responseWithRequestScopeHeaders(
2119
2334
  response,
2120
2335
  scope.snapshotHeaders(error),
2336
+ outcome.kind === "completed",
2121
2337
  );
2122
2338
  scope.terminalize(outcome);
2123
2339
  return finalResponse;
@@ -2484,6 +2700,7 @@ function getHttpStreamTransport(
2484
2700
  function toAuthFlowResponse(
2485
2701
  result: unknown,
2486
2702
  contextPatch: Record<string, unknown | null> | undefined,
2703
+ engine: AuthFlowSuccessResponse["engine"],
2487
2704
  ): Response | AuthFlowSuccessResponse {
2488
2705
  if (result instanceof Response) {
2489
2706
  return result;
@@ -2496,6 +2713,7 @@ function toAuthFlowResponse(
2496
2713
  return {
2497
2714
  data: result,
2498
2715
  ...(contextPatch ? { contextPatch } : {}),
2716
+ ...(engine ? { engine } : {}),
2499
2717
  };
2500
2718
  }
2501
2719
 
@@ -2672,10 +2890,12 @@ async function handleAuthFlow(
2672
2890
  // depend on declared secrets (client ids/secrets), so fail structured before
2673
2891
  // any flow code runs instead of at whatever point the ceremony first reads
2674
2892
  // the env. `abort` stays exempt: a user must always be able to cancel a
2675
- // stranded flow even when provisioning is broken.
2676
- const { context, getPatch } = createAuthFlowContext(
2893
+ // stranded flow even when provisioning is broken (createAuthFlowContext
2894
+ // applies the same exemption to the engine ceremony lease).
2895
+ const { context, getPatch, getEngineState } = createAuthFlowContext(
2677
2896
  provider,
2678
2897
  request,
2898
+ route,
2679
2899
  options,
2680
2900
  state,
2681
2901
  scope,
@@ -2715,10 +2935,10 @@ async function handleAuthFlow(
2715
2935
  isAuthTurn(result)
2716
2936
  ? materializeAuthFlowTurn(provider, request, result)
2717
2937
  : result;
2718
- return toAuthFlowResponse(materializedResult, getPatch());
2938
+ return toAuthFlowResponse(materializedResult, getPatch(), getEngineState());
2719
2939
  } catch (error) {
2720
2940
  if (error instanceof AuthAbortError) {
2721
- return toAuthFlowResponse(error.turn, getPatch());
2941
+ return toAuthFlowResponse(error.turn, getPatch(), getEngineState());
2722
2942
  }
2723
2943
  throw error;
2724
2944
  } finally {
@@ -3185,7 +3405,7 @@ function createServerAppWithCapabilityModules(
3185
3405
  );
3186
3406
  const response = handled instanceof Response ? handled : c.json(handled);
3187
3407
  return streaming
3188
- ? responseWithRequestScopeHeaders(response, requestScope.snapshotHeaders())
3408
+ ? responseWithRequestScopeHeaders(response, requestScope.snapshotHeaders(), true)
3189
3409
  : response;
3190
3410
  };
3191
3411
  const response = streaming