@apifuse/provider-sdk 2.2.0-beta.58 → 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 (106) hide show
  1. package/AUTHORING.md +9 -0
  2. package/CHANGELOG.md +4 -0
  3. package/README.md +18 -2
  4. package/bin/apifuse-pack-types.ts +49 -0
  5. package/bin/apifuse-record.ts +6 -0
  6. package/dist/cli/templates/provider/README.md.tpl +15 -3
  7. package/dist/config/loader.d.ts +7 -0
  8. package/dist/config/loader.js +30 -6
  9. package/dist/declaration-validation.d.ts +2 -1
  10. package/dist/declaration-validation.js +39 -0
  11. package/dist/define.d.ts +4 -4
  12. package/dist/define.js +25 -1
  13. package/dist/engine.d.ts +16 -0
  14. package/dist/engine.js +56 -1
  15. package/dist/error-resolution.js +13 -0
  16. package/dist/index.d.ts +6 -4
  17. package/dist/index.js +4 -2
  18. package/dist/lint.js +2 -2
  19. package/dist/provider.d.ts +3 -3
  20. package/dist/provider.js +2 -2
  21. package/dist/runtime/bounded-expiring-map.d.ts +21 -0
  22. package/dist/runtime/bounded-expiring-map.js +64 -0
  23. package/dist/runtime/cache.d.ts +2 -0
  24. package/dist/runtime/cache.js +8 -1
  25. package/dist/runtime/egress-lease.d.ts +63 -0
  26. package/dist/runtime/egress-lease.js +233 -0
  27. package/dist/runtime/http.js +1 -0
  28. package/dist/runtime/proxy-retry-policy.d.ts +4 -1
  29. package/dist/runtime/proxy-retry-policy.js +11 -2
  30. package/dist/runtime/request-telemetry.d.ts +7 -9
  31. package/dist/runtime/resolver-config.d.ts +7 -0
  32. package/dist/runtime/resolver-config.js +8 -0
  33. package/dist/runtime/resolver-public.d.ts +3 -1
  34. package/dist/runtime/resolver-public.js +1 -1
  35. package/dist/runtime/resolver-shared.d.ts +9 -0
  36. package/dist/runtime/resolver-shared.js +57 -0
  37. package/dist/runtime/resolver-telemetry.d.ts +134 -0
  38. package/dist/runtime/resolver-telemetry.js +204 -0
  39. package/dist/runtime/resolver-usage.d.ts +24 -0
  40. package/dist/runtime/resolver-usage.js +60 -0
  41. package/dist/runtime/resolver-vendors/bindings.d.ts +5 -0
  42. package/dist/runtime/resolver-vendors/bindings.js +5 -0
  43. package/dist/runtime/resolver-vendors/capsolver.d.ts +2 -2
  44. package/dist/runtime/resolver-vendors/capsolver.js +21 -9
  45. package/dist/runtime/resolver-vendors/hypersolutions.d.ts +27 -0
  46. package/dist/runtime/resolver-vendors/hypersolutions.js +369 -0
  47. package/dist/runtime/resolver-vendors/twocaptcha.d.ts +2 -2
  48. package/dist/runtime/resolver-vendors/twocaptcha.js +26 -10
  49. package/dist/runtime/resolver-vendors/types.d.ts +35 -5
  50. package/dist/runtime/resolver-vendors/types.js +20 -10
  51. package/dist/runtime/resolver.d.ts +5 -1
  52. package/dist/runtime/resolver.js +397 -109
  53. package/dist/runtime/stealth-akamai-sbsd.d.ts +54 -0
  54. package/dist/runtime/stealth-akamai-sbsd.js +142 -0
  55. package/dist/runtime/stealth.js +547 -53
  56. package/dist/schema.d.ts +9 -0
  57. package/dist/schema.js +16 -0
  58. package/dist/server/index.d.ts +2 -0
  59. package/dist/server/index.js +1 -0
  60. package/dist/server/self-test.d.ts +4 -0
  61. package/dist/server/self-test.js +10 -1
  62. package/dist/server/serve-implementation.d.ts +7 -1
  63. package/dist/server/serve-implementation.js +181 -53
  64. package/dist/server/types.d.ts +6 -0
  65. package/dist/server/types.js +14 -0
  66. package/dist/stealth/profiles.d.ts +7 -1
  67. package/dist/stealth/profiles.js +9 -0
  68. package/dist/testing/run.js +6 -0
  69. package/dist/types.d.ts +106 -10
  70. package/package.json +1 -1
  71. package/src/cli/templates/provider/README.md.tpl +15 -3
  72. package/src/config/loader.ts +34 -6
  73. package/src/declaration-validation.ts +47 -1
  74. package/src/define.ts +45 -3
  75. package/src/engine.ts +75 -1
  76. package/src/error-resolution.ts +13 -0
  77. package/src/index.ts +32 -1
  78. package/src/lint.ts +2 -2
  79. package/src/provider.ts +9 -0
  80. package/src/runtime/bounded-expiring-map.ts +67 -0
  81. package/src/runtime/cache.ts +10 -1
  82. package/src/runtime/egress-lease.ts +336 -0
  83. package/src/runtime/http.ts +1 -0
  84. package/src/runtime/proxy-retry-policy.ts +12 -1
  85. package/src/runtime/request-telemetry.ts +5 -7
  86. package/src/runtime/resolver-config.ts +12 -0
  87. package/src/runtime/resolver-public.ts +21 -0
  88. package/src/runtime/resolver-shared.ts +75 -0
  89. package/src/runtime/resolver-telemetry.ts +362 -0
  90. package/src/runtime/resolver-usage.ts +96 -0
  91. package/src/runtime/resolver-vendors/bindings.ts +5 -0
  92. package/src/runtime/resolver-vendors/capsolver.ts +22 -3
  93. package/src/runtime/resolver-vendors/hypersolutions.ts +480 -0
  94. package/src/runtime/resolver-vendors/twocaptcha.ts +26 -12
  95. package/src/runtime/resolver-vendors/types.ts +60 -12
  96. package/src/runtime/resolver.ts +465 -124
  97. package/src/runtime/stealth-akamai-sbsd.ts +207 -0
  98. package/src/runtime/stealth.ts +727 -95
  99. package/src/schema.ts +17 -0
  100. package/src/server/index.ts +21 -0
  101. package/src/server/self-test.ts +13 -0
  102. package/src/server/serve-implementation.ts +268 -48
  103. package/src/server/types.ts +14 -0
  104. package/src/stealth/profiles.ts +10 -0
  105. package/src/testing/run.ts +6 -0
  106. package/src/types.ts +124 -19
package/AUTHORING.md CHANGED
@@ -130,6 +130,12 @@ not list `APIFUSE__PROXY__SMARTPROXY_APP_KEY`,
130
130
  `APIFUSE__PROXY__NODEMAVEN_PASSWORD` in provider `secrets`; `proxy` contains
131
131
  policy intent only.
132
132
 
133
+ Hosted runtime settings are engine-owned as well: every `APIFUSE__CDP_POOL__*`
134
+ variable, the Cloudflare OCR/STT tokens and account identifier,
135
+ `APIFUSE__OCR__API_KEY`, `APIFUSE__CACHE__KEY_PEPPER`, and
136
+ `APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET`. `defineProvider`
137
+ rejects them in `secrets`; the engine reads them from its own environment.
138
+
133
139
  ### Factored operations
134
140
 
135
141
  `defineProvider(declaration)` returns the builder that accepts `operations`, so
@@ -501,6 +507,9 @@ SDK observability is emitted separately in the
501
507
  ```
502
508
 
503
509
  Treat this header as telemetry, not as provider-controlled public error detail.
510
+ It is SDK-owned even when a handler returns a raw `Response`: a caller-set
511
+ header is dropped, and a raw status of 400 or above gets a status-derived one
512
+ (retryable only for 408, 429, and 5xx) while the body is left untouched.
504
513
  Its category, taxonomy version, retryability, and optional upstream status match
505
514
  the structured `provider_request_failed` log event.
506
515
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @apifuse/provider-sdk Changelog
2
2
 
3
+ ## 2.2.0-beta.59
4
+
5
+ - Release candidate for main commit 55ae556fcf33e28a228c2d54ce76e7afa41e5dc7.
6
+
3
7
  ## 2.2.0-beta.58
4
8
 
5
9
  - Release candidate for main commit e137940caa8bc32ce768571a6197651e581c2270.
package/README.md CHANGED
@@ -76,7 +76,14 @@ The operation request body is the same envelope used by the APIFuse gateway:
76
76
  | `requestId` | yes | Any unique string is fine for local debugging; it is echoed in structured errors. |
77
77
  | `input` | yes | The operation input after schema validation. |
78
78
  | `headers` | no | Extra caller headers to expose through `ctx.request.headers`. |
79
- | `connection` | no | Omit for no-auth/public operations. For credential debugging, pass an object with `id`, `mode`, `secrets`, `metadata`, and `externalRef`. Do not pass `null`. |
79
+ | `connectionId` | no | Connection identity only; it does not include credentials. The gateway sends it for `optional` connection mode, and only when the caller supplied a connection that passed authorization. Exposed as `ctx.request.connectionId`. |
80
+ | `connection` | no | Credential-bearing connection data. The gateway sends it for `required` connection mode; omit it for no-auth/public (`none` mode) operations. For local debugging, pass an object with `id`, `mode`, `secrets`, `metadata`, and `externalRef`. Do not pass `null`. |
81
+
82
+ The gateway sends only `connection` for `required` mode, only `connectionId`
83
+ for `optional` mode (and nothing when the caller passed no connection), and
84
+ neither field for `none` mode. If a malformed or manually constructed envelope
85
+ contains both, a non-empty nested `connection.id` takes precedence over the
86
+ top-level `connectionId`; an empty string is treated as absent.
80
87
 
81
88
  Credential-bearing local smoke example:
82
89
 
@@ -146,7 +153,11 @@ the bad request path; provider/runtime failures include `code`, `message`, and
146
153
  a session with `ctx.stealth.createSession()` and use `session.redirects.run()`;
147
154
  inspect accumulated cookies through `session.cookies`. Select an SDK stealth
148
155
  identity with structured `stealth: { browser, os }` options; omitted `os`
149
- explicitly defaults to `macos`. Do not pin a browser version
156
+ explicitly defaults to `macos`. The transport owns `User-Agent`,
157
+ `Accept-Encoding`, `sec-ch-ua*`, and every `Sec-Fetch-*` header and throws
158
+ `STEALTH_HEADER_OVERRIDE_UNSUPPORTED` if you set one; declare
159
+ `stealth.requestClass: "navigation" | "xhr" | "post"` instead. Do not pin a
160
+ browser version
150
161
  or tune JA3, HTTP/2 SETTINGS, or
151
162
  pseudo-header order in provider code. Chrome, Firefox, and Safari profiles
152
163
  are supported; use `ctx.browser` when the provider needs browser execution.
@@ -325,3 +336,8 @@ External bounty contributors should submit standalone Provider source plus
325
336
  `bun run check` / `bun run test` evidence. APIFuse maintainers own monorepo
326
337
  import, registry generation, deployment projection checks, and release
327
338
  publishing.
339
+
340
+ Relayed user-generated text (listing bodies, reviews, posts) is contained by the
341
+ platform, not by the provider. Wrap such output fields in `untrustedContent(...)`
342
+ so the schema carries `x-apifuse-content-trust: untrusted` and
343
+ `x-apifuse-content-provenance: external`; do not strip instruction-like prose.
@@ -401,6 +401,55 @@ const NEGATIVE_CONTROLS = [
401
401
  "",
402
402
  ].join("\n"),
403
403
  },
404
+ {
405
+ filename: "negative-control-resolver-telemetry-description.ts",
406
+ expectedCode: "TS2353",
407
+ description: "resolver gateway telemetry rejects vendor error descriptions",
408
+ source: [
409
+ 'import type { ResolverTelemetryHeaderPayload } from "@apifuse/provider-sdk";',
410
+ "",
411
+ "export const mustNotCompile: ResolverTelemetryHeaderPayload = {",
412
+ "\tattempts: 0,",
413
+ "\tfailovers: 0,",
414
+ "\tvendorChain: [],",
415
+ "\tpollCount: 0,",
416
+ '\tvendorErrorDescription: "free text must stay log-only",',
417
+ "};",
418
+ "",
419
+ ].join("\n"),
420
+ },
421
+ {
422
+ filename: "negative-control-resolver-telemetry-phase.ts",
423
+ expectedCode: "TS2322",
424
+ description: "resolver gateway telemetry rejects phases outside its closed vocabulary",
425
+ source: [
426
+ 'import { closedEnum, type ResolverTelemetryHeaderPayload } from "@apifuse/provider-sdk";',
427
+ "",
428
+ "export const mustNotCompile: ResolverTelemetryHeaderPayload = {",
429
+ "\tattempts: 1,",
430
+ "\tfailovers: 0,",
431
+ '\tvendorChain: [closedEnum("hypersolutions")],',
432
+ "\tpollCount: 0,",
433
+ '\tattemptSamples: [{ v: closedEnum("hypersolutions"), p: closedEnum("solve_exchange"), o: closedEnum("error"), ms: 1 }],',
434
+ "};",
435
+ "",
436
+ ].join("\n"),
437
+ },
438
+ {
439
+ filename: "positive-control-resolver-telemetry-contributor.ts",
440
+ expectedCode: "",
441
+ description: "resolver telemetry contributor satisfies the gateway-ingestible contract",
442
+ source: [
443
+ 'import { closedEnum, ResolverTelemetryCollector, type GatewayIngestible, type ResolverTelemetryHeaderPayload, type ResolverTelemetryLogPayload, type ResolverTelemetryPhase, type TelemetryContributor } from "@apifuse/provider-sdk";',
444
+ "",
445
+ "export const resolver: TelemetryContributor<ResolverTelemetryLogPayload, ResolverTelemetryHeaderPayload> = new ResolverTelemetryCollector();",
446
+ 'const hyperPhases: ResolverTelemetryPhase[] = ["measure_ip", "fetch_script", "generate_payload", "post_payload"];',
447
+ 'export const hyperPhaseHeader: ResolverTelemetryHeaderPayload = { attempts: 1, failovers: 0, vendorChain: [closedEnum("hypersolutions")], pollCount: 0, attemptSamples: hyperPhases.map((phase) => ({ v: closedEnum("hypersolutions"), p: closedEnum(phase), o: closedEnum("ok"), ms: 1 })) };',
448
+ "declare const header: ResolverTelemetryHeaderPayload;",
449
+ "export const ingestible: GatewayIngestible<ResolverTelemetryHeaderPayload> = header;",
450
+ "",
451
+ ].join("\n"),
452
+ },
404
453
  ] as const;
405
454
 
406
455
  const tempRoot = mkdtempSync(join(tmpdir(), "apifuse-provider-sdk-pack-types-"));
@@ -832,6 +832,12 @@ function proxyStealthSession(
832
832
  onResponse(order, response);
833
833
  return response;
834
834
  },
835
+ replayChallenged: async (...args: Parameters<StealthSession["replayChallenged"]>) => {
836
+ const order = reserveOrder();
837
+ const response = await session.replayChallenged(...args);
838
+ onResponse(order, response);
839
+ return response;
840
+ },
835
841
  cookies: session.cookies,
836
842
  redirects: {
837
843
  run: async (...args: Parameters<StealthSession["redirects"]["run"]>) => {
@@ -97,9 +97,21 @@ The `POST /v1/{operation}` body is a request envelope:
97
97
  - `requestId` is required and can be any unique local debugging string.
98
98
  - `input` contains the operation input shape.
99
99
  - `headers` is optional.
100
- - `connection` is optional; omit it for no-auth/public operations. For
101
- credential debugging, pass `{ "id", "mode", "secrets", "metadata",
102
- "externalRef" }` with local-only secret values.
100
+ - `connectionId` is optional connection identity only and does not include
101
+ credentials. The gateway sends it for `optional` connection mode, and only
102
+ when the caller supplied a connection that passed authorization. It is
103
+ exposed as `ctx.request.connectionId`.
104
+ - `connection` is optional credential-bearing connection data. The gateway
105
+ sends it for `required` connection mode; omit it for no-auth/public (`none`
106
+ mode) operations. For local debugging, pass
107
+ `{ "id", "mode", "secrets", "metadata", "externalRef" }` with local-only
108
+ secret values.
109
+
110
+ The gateway sends only `connection` for `required` mode, only `connectionId`
111
+ for `optional` mode (and nothing when the caller passed no connection), and
112
+ neither field for `none` mode. If a malformed or manually constructed envelope
113
+ contains both, a non-empty nested `connection.id` takes precedence over the
114
+ top-level `connectionId`; an empty string is treated as absent.
103
115
 
104
116
  Structured errors return an `error` object with `code`, `message`,
105
117
  `requestId`, and optional `details`; validation failures include field paths in
@@ -140,6 +140,13 @@ export declare function providerStateRedisUrlFromEnv(): string | undefined;
140
140
  /** @internal Test-only hook for exercising shared proxy-cache behavior. */
141
141
  export declare function __setProxyRedisForTests(redis: ProxyRedisClient | undefined): void;
142
142
  export declare function __setSmartproxyAllocatorDeadlineMsForTests(deadlineMs: number | undefined): void;
143
+ /** @internal Test-only hook for exercising the pool-cache capacity bound at a small size. */
144
+ export declare function __setSmartproxyPoolCacheMaxEntriesForTests(maxEntries: number | undefined): void;
145
+ /** @internal Test-only view of the in-memory Smartproxy cache sizes. */
146
+ export declare function __getSmartproxyCacheSizesForTests(): {
147
+ pools: number;
148
+ tombstones: number;
149
+ };
143
150
  export declare function resolveProxyConfig(options?: ProxyResolutionOptions): ResolvedProxyConfig;
144
151
  export declare function resolveProxyConfigAsync(options?: ProxyResolutionOptions): Promise<ResolvedProxyConfig>;
145
152
  /**
@@ -2,6 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { existsSync } from "node:fs";
3
3
  import { createRequire } from "node:module";
4
4
  import path from "node:path";
5
+ import { BoundedExpiringMap } from "../runtime/bounded-expiring-map.js";
5
6
  import { NODEMAVEN_DEFAULT_PROTOCOL, NODEMAVEN_FILTER_ENV, NODEMAVEN_MAX_POOL_SIZE, NODEMAVEN_PASSWORD_ENV, NODEMAVEN_USERNAME_ENV, nodemavenPoolSize, synthesizeNodemavenProxy, } from "../runtime/proxy-nodemaven.js";
6
7
  // "smartproxy" here is api.smartproxy.org — a residential proxy with an IP
7
8
  // extraction API (app_key → raw ip:port pool). It is NOT the company formerly
@@ -33,7 +34,13 @@ export class ProxyResolutionError extends Error {
33
34
  this.protocol = options?.protocol;
34
35
  }
35
36
  }
36
- const proxyCache = new Map();
37
+ // Keyed per (policy, affinity). Connection affinities are minted by callers, so
38
+ // without a cap a long-lived process accumulates one pool per affinity it ever
39
+ // saw. Evicting a still-fresh pool forces a fresh allocation (a different
40
+ // egress endpoint) for that affinity, so the cap is generous and every
41
+ // capacity eviction is reported as `diagnostics.poolCacheEvictions`.
42
+ const SMARTPROXY_POOL_CACHE_MAX_ENTRIES = 10_000;
43
+ let proxyCache = createSmartproxyPoolCache(SMARTPROXY_POOL_CACHE_MAX_ENTRIES);
37
44
  const require = createRequire(import.meta.url);
38
45
  const proxyInflight = new Map();
39
46
  const invalidatedProxyKeys = new Map();
@@ -76,6 +83,17 @@ export function __setProxyRedisForTests(redis) {
76
83
  export function __setSmartproxyAllocatorDeadlineMsForTests(deadlineMs) {
77
84
  smartproxyAllocatorDeadlineMsForTests = deadlineMs;
78
85
  }
86
+ /** @internal Test-only hook for exercising the pool-cache capacity bound at a small size. */
87
+ export function __setSmartproxyPoolCacheMaxEntriesForTests(maxEntries) {
88
+ proxyCache = createSmartproxyPoolCache(maxEntries ?? SMARTPROXY_POOL_CACHE_MAX_ENTRIES);
89
+ }
90
+ /** @internal Test-only view of the in-memory Smartproxy cache sizes. */
91
+ export function __getSmartproxyCacheSizesForTests() {
92
+ return { pools: proxyCache.size, tombstones: invalidatedProxyKeys.size };
93
+ }
94
+ function createSmartproxyPoolCache(maxEntries) {
95
+ return new BoundedExpiringMap(maxEntries, (pool) => pool.expiresAt);
96
+ }
79
97
  function getProxyRedis() {
80
98
  if (proxyRedisForTests)
81
99
  return proxyRedisForTests;
@@ -457,6 +475,7 @@ export async function resolveWithVendor(vendor, policy, options, context) {
457
475
  ...allocated.pool.diagnostics,
458
476
  poolSize: allocated.pool.urls.length,
459
477
  poolIndex,
478
+ poolCacheEvictions: proxyCache.capacityEvictions,
460
479
  },
461
480
  };
462
481
  }
@@ -695,8 +714,8 @@ async function allocateSmartproxy(policy, appKey, lifetimeMinutes, affinityKey,
695
714
  const now = startedAt;
696
715
  const invalidatedUntil = invalidatedProxyKeys.get(cacheKey) ?? 0;
697
716
  const skipCached = invalidatedUntil > now;
698
- const cached = proxyCache.get(cacheKey);
699
- if (!skipCached && cached && isFresh(cached, now)) {
717
+ const cached = skipCached ? undefined : proxyCache.get(cacheKey, now);
718
+ if (cached) {
700
719
  if (shouldSoftRefresh(cached, now)) {
701
720
  void refreshSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes, protocol, ambientDefaults, sharedCache);
702
721
  return {
@@ -751,7 +770,7 @@ async function readSmartproxyRedisPool(cacheKey, startedAt) {
751
770
  const now = Date.now();
752
771
  if (!isFresh(pool, now))
753
772
  return null;
754
- proxyCache.set(cacheKey, pool);
773
+ proxyCache.set(cacheKey, pool, now);
755
774
  return {
756
775
  pool,
757
776
  telemetry: telemetryForPool(pool, "redis_hit", startedAt, { redisReadMs }),
@@ -962,7 +981,7 @@ async function allocateAndStoreSmartproxyPool(cacheKey, policy, appKey, lifetime
962
981
  rawConnect: true,
963
982
  },
964
983
  };
965
- proxyCache.set(cacheKey, result);
984
+ proxyCache.set(cacheKey, result, allocatedAt);
966
985
  if (options.redis && options.poolKey) {
967
986
  const redis = options.redis;
968
987
  const poolKey = options.poolKey;
@@ -1164,7 +1183,12 @@ function markSmartproxyCacheInvalidated(options = {}) {
1164
1183
  if (!appKey)
1165
1184
  return undefined;
1166
1185
  const cacheKey = buildSmartproxyCacheKey(policy, appKey, options.affinityKey, lifetimeMinutes, options.protocol ?? VENDOR_DEFAULT_PROTOCOL.smartproxy);
1167
- invalidatedProxyKeys.set(cacheKey, Date.now() + SMARTPROXY_INVALIDATION_SKIP_REDIS_MS);
1186
+ const now = Date.now();
1187
+ for (const [key, invalidatedUntil] of invalidatedProxyKeys) {
1188
+ if (invalidatedUntil <= now)
1189
+ invalidatedProxyKeys.delete(key);
1190
+ }
1191
+ invalidatedProxyKeys.set(cacheKey, now + SMARTPROXY_INVALIDATION_SKIP_REDIS_MS);
1168
1192
  proxyCache.delete(cacheKey);
1169
1193
  proxyInflight.delete(cacheKey);
1170
1194
  return cacheKey;
@@ -13,6 +13,7 @@ export declare const DECLARATION_RULE_IDS: {
13
13
  readonly proxySmartproxyGeo: "proxy-smartproxy-country-only";
14
14
  readonly operationUpstreamProxy: "operation-upstream-proxy-unsupported";
15
15
  readonly pinnedWireFieldPathValid: "public-schema-pinned-wire-field-valid";
16
+ readonly resolverClientProfileFamily: "resolver-client-profile-family";
16
17
  };
17
18
  export type DeclarationRuleId = (typeof DECLARATION_RULE_IDS)[keyof typeof DECLARATION_RULE_IDS];
18
19
  export type DeclarationViolation = {
@@ -24,7 +25,7 @@ export type DeclarationViolation = {
24
25
  export declare function declarationInvalidError(violations: readonly DeclarationViolation[]): ProviderError;
25
26
  /** Enforces declaration rules whose runtime behavior would otherwise fail open. */
26
27
  export declare function validateFailClosedDeclaration(provider: ProviderDefinition): void;
27
- type ProviderDeclarationRulesInput = Pick<ProviderDefinition, "healthJourneys" | "meta" | "proxy">;
28
+ type ProviderDeclarationRulesInput = Pick<ProviderDefinition, "healthJourneys" | "meta" | "proxy" | "resolver" | "stealth">;
28
29
  type OperationDeclarationRulesInput = Pick<ProviderDefinition, "operations">;
29
30
  /** Enforces fail-closed rules that only depend on the provider declaration. */
30
31
  export declare function validateFailClosedProviderDeclaration(provider: ProviderDeclarationRulesInput): void;
@@ -1,6 +1,8 @@
1
1
  import { describeSchema } from "./contract-serialization.js";
2
2
  import { ProviderError } from "./errors.js";
3
3
  import { HealthScenarioSchema } from "./health-scenario.js";
4
+ import { kindRequiresClientProfile } from "./runtime/resolver-config.js";
5
+ import { resolverClientProfileFamily, resolveStealthProfileSelection } from "./stealth/profiles.js";
4
6
  export const DECLARATION_INVALID_CODE = "DECLARATION_INVALID";
5
7
  export const DECLARATION_RULE_IDS = {
6
8
  challengeShape: "credentials-challenge-shape",
@@ -14,6 +16,7 @@ export const DECLARATION_RULE_IDS = {
14
16
  proxySmartproxyGeo: "proxy-smartproxy-country-only",
15
17
  operationUpstreamProxy: "operation-upstream-proxy-unsupported",
16
18
  pinnedWireFieldPathValid: "public-schema-pinned-wire-field-valid",
19
+ resolverClientProfileFamily: "resolver-client-profile-family",
17
20
  };
18
21
  export function declarationInvalidError(violations) {
19
22
  const summary = violations
@@ -32,6 +35,7 @@ export function validateFailClosedDeclaration(provider) {
32
35
  validatePinnedWireFieldPaths(provider, violations);
33
36
  validateSchemaDeclaration(provider, violations);
34
37
  validateProxyDeclaration(provider, violations);
38
+ validateResolverClientProfileDeclaration(provider, violations);
35
39
  validateOperationDeclaration(provider, violations);
36
40
  if (violations.length > 0)
37
41
  throw declarationInvalidError(violations);
@@ -54,6 +58,7 @@ function collectProviderDeclarationViolations(provider, violations) {
54
58
  validateHealthDeclaration(provider, violations);
55
59
  validatePinnedWireFieldPaths(provider, violations);
56
60
  validateProxyDeclaration(provider, violations);
61
+ validateResolverClientProfileDeclaration(provider, violations);
57
62
  }
58
63
  function validatePinnedWireFieldPaths(provider, violations) {
59
64
  const pins = provider.meta?.contract?.pinnedWireFieldPaths;
@@ -245,6 +250,40 @@ function validateProxyDeclaration(provider, violations) {
245
250
  }
246
251
  }
247
252
  }
253
+ /**
254
+ * An akamai_sbsd solve runs on the provider's own stealth session, so the declared
255
+ * resolver.clientProfile must name the browser family that session emulates. The
256
+ * stealth runtime re-checks per request (a fetch may override the browser); this
257
+ * rule fails the declaration instead of the first challenged request.
258
+ */
259
+ function validateResolverClientProfileDeclaration(provider, violations) {
260
+ const resolver = provider.resolver;
261
+ if (!resolver ||
262
+ !Array.isArray(resolver.kinds) ||
263
+ !resolver.kinds.some(kindRequiresClientProfile) ||
264
+ typeof resolver.clientProfile !== "string") {
265
+ return;
266
+ }
267
+ let sessionBrowser;
268
+ try {
269
+ sessionBrowser = resolveStealthProfileSelection(provider.stealth).browser;
270
+ }
271
+ catch {
272
+ // An unsupported stealth selection is reported by the stealth runtime itself.
273
+ return;
274
+ }
275
+ const family = resolverClientProfileFamily(resolver.clientProfile);
276
+ if (family === sessionBrowser)
277
+ return;
278
+ violations.push({
279
+ ruleId: DECLARATION_RULE_IDS.resolverClientProfileFamily,
280
+ path: "resolver.clientProfile",
281
+ message: family
282
+ ? `"${resolver.clientProfile}" names ${family}, but the stealth session browser is ${sessionBrowser}.`
283
+ : `"${resolver.clientProfile}" does not name a supported browser family (chrome, firefox, safari).`,
284
+ fix: `Declare stealth.browser as the family resolver.clientProfile names, or set resolver.clientProfile to the ${sessionBrowser} profile the SBSD session runs on.`,
285
+ });
286
+ }
248
287
  function declaredProxyVendors(policy) {
249
288
  const vendors = [...(policy.providers ?? [])];
250
289
  if (policy.provider !== undefined)
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, ProviderContext, ProviderDefinition, ProviderDeploymentOverrides, ProviderHealthMonitorConfig, ProviderOcrConfig, ProviderProxyConfig, ProviderPublicProfile, ProviderResolverConfig, ProviderReviewed, ProviderRuntimeTarget, ProviderSecretDeclaration, ProviderStreamEvent, ProviderSttConfig, SchemaLike, SmsOtpMatcherDefinition, StealthProfileSelection } from "./types.js";
1
+ import type { AuthConfig, BrowserEngine, ContextDeclaration, CredentialDeclaration, HealthJourneyDefinition, HealthJourneySchedule, HealthScheduleRandomization, InferSchemaOutput, NativeProviderConfig, OperationDefinition, OperationHandlerResult, OperationHttpStreamTransport, OperationSseTransport, OperationWebSocketTransport, ProviderAccessConfig, ProviderContext, ProviderDefinition, ProviderDeploymentOverrides, ProviderHealthMonitorConfig, ProviderOcrConfig, ProviderProxyConfig, ProviderPublicProfile, ProviderResolverConfig, ProviderReviewed, ProviderRuntimeTarget, ProviderSecretDeclaration, ProviderStreamEvent, ProviderSttConfig, ProviderStealthConfig, SchemaLike, SmsOtpMatcherDefinition } 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 {
@@ -8,8 +8,8 @@ interface ProviderImplementationProfile {
8
8
  operatorNotes?: string;
9
9
  visibility: "internal" | "operator";
10
10
  }
11
- export declare const VALID_PROVIDER_RESOLVER_VENDORS: readonly ["browser", "capsolver", "capmonster", "2captcha", "custom"];
12
- export declare const VALID_PROVIDER_CHALLENGE_KINDS: readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha", "cloudflare_interstitial", "aws_waf", "akamai_sec_cpt", "akamai_sensor"];
11
+ export declare const VALID_PROVIDER_RESOLVER_VENDORS: readonly ["browser", "capsolver", "capmonster", "2captcha", "hypersolutions", "custom"];
12
+ export declare const VALID_PROVIDER_CHALLENGE_KINDS: readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha", "cloudflare_interstitial", "aws_waf", "akamai_sec_cpt", "akamai_sensor", "akamai_sbsd"];
13
13
  type ProviderOperation = OperationDefinition<any, any, any>;
14
14
  type OperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike, TContext = ProviderContext> = Omit<OperationDefinition<TInput, TOutput, TContext>, "handler"> & {
15
15
  handler(ctx: TContext, input: InferSchemaOutput<TInput>): OperationHandlerResult<InferSchemaOutput<TOutput>> | Promise<OperationHandlerResult<InferSchemaOutput<TOutput>>>;
@@ -60,7 +60,7 @@ export interface ProviderDeclaration {
60
60
  /** Declares upstream host policy; this does not add a `ctx.allowedHosts` member. */
61
61
  allowedHosts?: string[];
62
62
  native?: NativeProviderConfig;
63
- stealth?: StealthProfileSelection;
63
+ stealth?: ProviderStealthConfig;
64
64
  /** Declares proxy policy; this is provider intent and does not add a `ctx.proxy` member. */
65
65
  proxy?: ProviderProxyConfig;
66
66
  ocr?: ProviderOcrConfig;
package/dist/define.js CHANGED
@@ -2,8 +2,9 @@ import ms from "ms";
2
2
  import { validateFailClosedOperationDeclaration, validateFailClosedProviderDeclaration, } from "./declaration-validation.js";
3
3
  import { SDK_RUNTIME_OWNED_ERROR_CODES } from "./error-resolution.js";
4
4
  import { ProviderError, ValidationError } from "./errors.js";
5
- import { isEngineOwnedProxyCredentialName, isEngineOwnedTelemetryEnvName } from "./engine.js";
5
+ import { engineOwnedResolverCredentialTarget, isEngineOwnedEnvName, isEngineOwnedProxyCredentialName, isEngineOwnedRuntimeEnvName, isEngineOwnedTelemetryEnvName, } from "./engine.js";
6
6
  import { HealthScenarioSchema } from "./health-scenario.js";
7
+ import { kindRequiresClientProfile } from "./runtime/resolver-config.js";
7
8
  import { NativeEgressPolicyValidationError, validateNativeProviderConfig, } from "./native-egress-policy.js";
8
9
  import { safeParseSchemaSync } from "./schema.js";
9
10
  import { resolveHealthCheckInputDateTokens } from "./server/self-test-input-tokens.js";
@@ -61,6 +62,7 @@ export const VALID_PROVIDER_RESOLVER_VENDORS = exhaustiveLiteralArray()([
61
62
  "capsolver",
62
63
  "capmonster",
63
64
  "2captcha",
65
+ "hypersolutions",
64
66
  "custom",
65
67
  ]);
66
68
  export const VALID_PROVIDER_CHALLENGE_KINDS = exhaustiveLiteralArray()([
@@ -72,6 +74,7 @@ export const VALID_PROVIDER_CHALLENGE_KINDS = exhaustiveLiteralArray()([
72
74
  "aws_waf",
73
75
  "akamai_sec_cpt",
74
76
  "akamai_sensor",
77
+ "akamai_sbsd",
75
78
  ]);
76
79
  const RESERVED_OPERATION_IDS = new Set(["auth", "health"]);
77
80
  const VALID_OPERATION_RISK_CLASSES = ["read", "write", "destructive", "external-send"];
@@ -633,11 +636,27 @@ function validateProviderImplementationShape(config) {
633
636
  }
634
637
  function validateProviderProxy(config) {
635
638
  for (const secret of config.secrets ?? []) {
639
+ const engineName = engineOwnedResolverCredentialTarget(secret.name);
640
+ if (engineName !== undefined) {
641
+ throw new ValidationError(`Provider "${config.id}" cannot declare engine-owned resolver credential "${secret.name}"`, {
642
+ fix: `Remove "${secret.name}" from provider secrets; configure the solver key only on the provider engine as ${engineName}.`,
643
+ });
644
+ }
636
645
  if (isEngineOwnedTelemetryEnvName(secret.name)) {
637
646
  throw new ValidationError(`Provider "${config.id}" cannot declare engine-owned telemetry variable "${secret.name}"`, {
638
647
  fix: `Remove "${secret.name}" from provider secrets; trace export is configured only on the provider engine.`,
639
648
  });
640
649
  }
650
+ if (isEngineOwnedRuntimeEnvName(secret.name)) {
651
+ throw new ValidationError(`Provider "${config.id}" cannot declare engine-owned runtime variable "${secret.name}"`, {
652
+ fix: `Remove "${secret.name}" from provider secrets; the provider engine reads it from its own environment.`,
653
+ });
654
+ }
655
+ if (isEngineOwnedEnvName(secret.name) && !isEngineOwnedProxyCredentialName(secret.name)) {
656
+ throw new ValidationError(`Provider "${config.id}" cannot declare engine-owned control variable "${secret.name}"`, {
657
+ fix: `Remove "${secret.name}" from provider secrets; configure it only on the provider engine.`,
658
+ });
659
+ }
641
660
  if (!isEngineOwnedProxyCredentialName(secret.name))
642
661
  continue;
643
662
  throw new ValidationError(`Provider "${config.id}" cannot declare engine-owned proxy credential "${secret.name}"`, {
@@ -785,6 +804,11 @@ function validateProviderResolver(config) {
785
804
  fix: `Set resolver.clientProfile for provider "${config.id}" to a transport-owned profile name.`,
786
805
  });
787
806
  }
807
+ if (resolver.kinds.some(kindRequiresClientProfile) && resolver.clientProfile === undefined) {
808
+ throw new ValidationError(`Provider "${config.id}" must declare resolver.clientProfile for akamai_sbsd.`, {
809
+ fix: `Set resolver.clientProfile for provider "${config.id}" to the transport-owned profile used by its Akamai session.`,
810
+ });
811
+ }
788
812
  }
789
813
  function validateResolverLiteralArray(value, field, validValues, providerId) {
790
814
  if (!Array.isArray(value)) {
package/dist/engine.d.ts CHANGED
@@ -4,6 +4,15 @@ export declare const PROVIDER_ENGINE_PROTOCOL_VERSION: "provider-engine.v1";
4
4
  /** Credential names owned by the engine and forbidden in provider declarations. */
5
5
  export declare const ENGINE_OWNED_PROXY_CREDENTIAL_ENV_NAMES: readonly ["APIFUSE__PROXY__SMARTPROXY_APP_KEY", "APIFUSE__PROXY__NODEMAVEN_USERNAME", "APIFUSE__PROXY__NODEMAVEN_PASSWORD"];
6
6
  export declare function isEngineOwnedProxyCredentialName(name: string): boolean;
7
+ /** Hosted resolver credentials owned by the engine and never projected to providers. */
8
+ export declare const ENGINE_OWNED_RESOLVER_CREDENTIAL_ENV_NAMES: readonly ["APIFUSE__RESOLVER__2CAPTCHA__API_KEY", "APIFUSE__RESOLVER__CAPSOLVER__API_KEY", "APIFUSE__RESOLVER__CAPMONSTER__API_KEY", "APIFUSE__RESOLVER__HYPERSOLUTIONS__API_KEY"];
9
+ /**
10
+ * Engine variable a provider-declared secret name resolves to, or `undefined` when the
11
+ * name is not an engine-owned resolver credential. Provider-scoped Hyper aliases
12
+ * (`APIFUSE__PROVIDER__<ID>__HYPER_API_KEY`, measured on zozotown) map to the shared key.
13
+ */
14
+ export declare function engineOwnedResolverCredentialTarget(name: string): string | undefined;
15
+ export declare function isEngineOwnedResolverCredentialName(name: string): boolean;
7
16
  /**
8
17
  * Trace-export configuration owned by the engine and forbidden in provider
9
18
  * declarations. The header variables carry collector credentials; the rest are
@@ -11,6 +20,13 @@ export declare function isEngineOwnedProxyCredentialName(name: string): boolean;
11
20
  */
12
21
  export declare const ENGINE_OWNED_TELEMETRY_ENV_NAMES: readonly ["OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "OTEL_EXPORTER_OTLP_ENDPOINT", "OTEL_EXPORTER_OTLP_TRACES_HEADERS", "OTEL_EXPORTER_OTLP_HEADERS", "OTEL_SERVICE_NAME", "OTEL_RESOURCE_ATTRIBUTES"];
13
22
  export declare function isEngineOwnedTelemetryEnvName(name: string): boolean;
23
+ /**
24
+ * Hosted runtime settings the engine reads for OCR, STT, cache keying, choice
25
+ * tokens, and the CDP pool. Providers never own them: declarations are rejected
26
+ * and projections drop them, like the vendor credentials above.
27
+ */
28
+ export declare const ENGINE_OWNED_RUNTIME_ENV_NAMES: readonly ["APIFUSE__STT__CLOUDFLARE_API_TOKEN", "APIFUSE__OCR__CLOUDFLARE_API_TOKEN", "APIFUSE__OCR__API_KEY", "APIFUSE__CLOUDFLARE__ACCOUNT_ID", "APIFUSE__CACHE__KEY_PEPPER", "APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET"];
29
+ export declare function isEngineOwnedRuntimeEnvName(name: string): boolean;
14
30
  /** Every environment name the engine owns: rejected in declarations and filtered from provider projections. */
15
31
  export declare function isEngineOwnedEnvName(name: string): boolean;
16
32
  /** Capture credentials in the engine host before constructing provider bindings. */
package/dist/engine.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ProviderError } from "./errors.js";
2
+ import { APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__HYPERSOLUTIONS__API_KEY, } from "./runtime/resolver-config.js";
2
3
  import { OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_HEADERS, OTEL_RESOURCE_ATTRIBUTES, OTEL_SERVICE_NAME, } from "./runtime/otlp.js";
3
4
  /** Versioned envelope protocol used by out-of-process engine transports. */
4
5
  export const PROVIDER_ENGINE_PROTOCOL_VERSION = "provider-engine.v1";
@@ -20,6 +21,31 @@ function canonicalEnvName(name) {
20
21
  export function isEngineOwnedProxyCredentialName(name) {
21
22
  return ENGINE_OWNED_PROXY_CREDENTIAL_ENV_NAME_SET.has(canonicalEnvName(name));
22
23
  }
24
+ /** Hosted resolver credentials owned by the engine and never projected to providers. */
25
+ export const ENGINE_OWNED_RESOLVER_CREDENTIAL_ENV_NAMES = [
26
+ APIFUSE__RESOLVER__2CAPTCHA__API_KEY,
27
+ APIFUSE__RESOLVER__CAPSOLVER__API_KEY,
28
+ APIFUSE__RESOLVER__CAPMONSTER__API_KEY,
29
+ APIFUSE__RESOLVER__HYPERSOLUTIONS__API_KEY,
30
+ ];
31
+ const ENGINE_OWNED_RESOLVER_CREDENTIAL_ENV_NAME_SET = new Set(ENGINE_OWNED_RESOLVER_CREDENTIAL_ENV_NAMES);
32
+ /**
33
+ * Engine variable a provider-declared secret name resolves to, or `undefined` when the
34
+ * name is not an engine-owned resolver credential. Provider-scoped Hyper aliases
35
+ * (`APIFUSE__PROVIDER__<ID>__HYPER_API_KEY`, measured on zozotown) map to the shared key.
36
+ */
37
+ export function engineOwnedResolverCredentialTarget(name) {
38
+ const canonical = canonicalEnvName(name);
39
+ if (ENGINE_OWNED_RESOLVER_CREDENTIAL_ENV_NAME_SET.has(canonical))
40
+ return canonical;
41
+ if (/^APIFUSE__PROVIDER__.+__HYPER_API_KEY$/u.test(canonical)) {
42
+ return APIFUSE__RESOLVER__HYPERSOLUTIONS__API_KEY;
43
+ }
44
+ return undefined;
45
+ }
46
+ export function isEngineOwnedResolverCredentialName(name) {
47
+ return engineOwnedResolverCredentialTarget(name) !== undefined;
48
+ }
23
49
  /**
24
50
  * Trace-export configuration owned by the engine and forbidden in provider
25
51
  * declarations. The header variables carry collector credentials; the rest are
@@ -37,9 +63,38 @@ const ENGINE_OWNED_TELEMETRY_ENV_NAME_SET = new Set(ENGINE_OWNED_TELEMETRY_ENV_N
37
63
  export function isEngineOwnedTelemetryEnvName(name) {
38
64
  return ENGINE_OWNED_TELEMETRY_ENV_NAME_SET.has(canonicalEnvName(name));
39
65
  }
66
+ /** Engine control-plane configuration is never declarable or visible to provider code. */
67
+ function isEngineOwnedEngineEnvName(name) {
68
+ return canonicalEnvName(name).startsWith("APIFUSE__ENGINE__");
69
+ }
70
+ /**
71
+ * Hosted runtime settings the engine reads for OCR, STT, cache keying, choice
72
+ * tokens, and the CDP pool. Providers never own them: declarations are rejected
73
+ * and projections drop them, like the vendor credentials above.
74
+ */
75
+ export const ENGINE_OWNED_RUNTIME_ENV_NAMES = [
76
+ "APIFUSE__STT__CLOUDFLARE_API_TOKEN",
77
+ "APIFUSE__OCR__CLOUDFLARE_API_TOKEN",
78
+ "APIFUSE__OCR__API_KEY",
79
+ "APIFUSE__CLOUDFLARE__ACCOUNT_ID",
80
+ "APIFUSE__CACHE__KEY_PEPPER",
81
+ "APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET",
82
+ ];
83
+ const ENGINE_OWNED_RUNTIME_ENV_NAME_SET = new Set(ENGINE_OWNED_RUNTIME_ENV_NAMES);
84
+ /** CDP pool settings form a prefix family (`APIFUSE__CDP_POOL__URL` today); the whole prefix is engine-owned. */
85
+ const ENGINE_OWNED_CDP_POOL_ENV_PREFIX = "APIFUSE__CDP_POOL__";
86
+ export function isEngineOwnedRuntimeEnvName(name) {
87
+ const canonical = canonicalEnvName(name);
88
+ return (ENGINE_OWNED_RUNTIME_ENV_NAME_SET.has(canonical) ||
89
+ canonical.startsWith(ENGINE_OWNED_CDP_POOL_ENV_PREFIX));
90
+ }
40
91
  /** Every environment name the engine owns: rejected in declarations and filtered from provider projections. */
41
92
  export function isEngineOwnedEnvName(name) {
42
- return isEngineOwnedProxyCredentialName(name) || isEngineOwnedTelemetryEnvName(name);
93
+ return (isEngineOwnedEngineEnvName(name) ||
94
+ isEngineOwnedProxyCredentialName(name) ||
95
+ isEngineOwnedResolverCredentialName(name) ||
96
+ isEngineOwnedRuntimeEnvName(name) ||
97
+ isEngineOwnedTelemetryEnvName(name));
43
98
  }
44
99
  /** Capture credentials in the engine host before constructing provider bindings. */
45
100
  export function readEngineProxyCredentials(environment = process.env) {
@@ -8,6 +8,7 @@ export const SDK_OWNED_PROVIDER_ERROR_CODES = new Set([
8
8
  "BROWSER_CDP_POOL_REQUIRED",
9
9
  "BROWSER_RUNTIME_UNSUPPORTED",
10
10
  "STEALTH_RUNTIME_UNSUPPORTED",
11
+ "STEALTH_HEADER_OVERRIDE_UNSUPPORTED",
11
12
  "SSE_EVENT_UNDECLARED",
12
13
  "STREAM_EVENT_TOO_LARGE",
13
14
  "STREAM_CHUNK_TOO_LARGE",
@@ -49,6 +50,13 @@ export const SDK_OWNED_PROVIDER_ERROR_CODES = new Set([
49
50
  "http_redirect_missing_location",
50
51
  "http_redirect_loop",
51
52
  "transport_invalid_url",
53
+ "EGRESS_LEASE_INVALID",
54
+ "EGRESS_LEASE_BINDING_INVALID",
55
+ "EGRESS_LEASE_KEY_MISSING",
56
+ "EGRESS_LEASE_KEY_WEAK",
57
+ "STEALTH_BODY_UNSUPPORTED",
58
+ "REPLAY_SESSION_MISMATCH",
59
+ "REPLAY_ALREADY_ATTEMPTED",
52
60
  "retry_exhausted",
53
61
  "auth_abort_unsafe_data",
54
62
  "credentials_auth_missing_credential_keys",
@@ -112,6 +120,11 @@ export const SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES = new Map([
112
120
  // contract): the upstream evaluated the request and said no under its
113
121
  // own rules — a conflict with upstream state, never a 5xx.
114
122
  ["UPSTREAM_REJECTED", 409],
123
+ // Only an unverifiable or foreign handle is the caller's: key and binding
124
+ // faults stay unmapped (500) because the engine host, not the caller, owns them.
125
+ ["EGRESS_LEASE_INVALID", 409],
126
+ ["REPLAY_SESSION_MISMATCH", 409],
127
+ ["REPLAY_ALREADY_ATTEMPTED", 409],
115
128
  ["UPSTREAM_ERROR", 502],
116
129
  ["BLOCKED", 502],
117
130
  ["OCR_UNAVAILABLE", 503],