@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/engine.ts CHANGED
@@ -1,4 +1,10 @@
1
1
  import { ProviderError } from "./errors.js";
2
+ import {
3
+ APIFUSE__RESOLVER__2CAPTCHA__API_KEY,
4
+ APIFUSE__RESOLVER__CAPMONSTER__API_KEY,
5
+ APIFUSE__RESOLVER__CAPSOLVER__API_KEY,
6
+ APIFUSE__RESOLVER__HYPERSOLUTIONS__API_KEY,
7
+ } from "./runtime/resolver-config.js";
2
8
  import {
3
9
  OTEL_EXPORTER_OTLP_ENDPOINT,
4
10
  OTEL_EXPORTER_OTLP_HEADERS,
@@ -55,6 +61,36 @@ export function isEngineOwnedProxyCredentialName(name: string): boolean {
55
61
  return ENGINE_OWNED_PROXY_CREDENTIAL_ENV_NAME_SET.has(canonicalEnvName(name));
56
62
  }
57
63
 
64
+ /** Hosted resolver credentials owned by the engine and never projected to providers. */
65
+ export const ENGINE_OWNED_RESOLVER_CREDENTIAL_ENV_NAMES = [
66
+ APIFUSE__RESOLVER__2CAPTCHA__API_KEY,
67
+ APIFUSE__RESOLVER__CAPSOLVER__API_KEY,
68
+ APIFUSE__RESOLVER__CAPMONSTER__API_KEY,
69
+ APIFUSE__RESOLVER__HYPERSOLUTIONS__API_KEY,
70
+ ] as const;
71
+
72
+ const ENGINE_OWNED_RESOLVER_CREDENTIAL_ENV_NAME_SET = new Set<string>(
73
+ ENGINE_OWNED_RESOLVER_CREDENTIAL_ENV_NAMES,
74
+ );
75
+
76
+ /**
77
+ * Engine variable a provider-declared secret name resolves to, or `undefined` when the
78
+ * name is not an engine-owned resolver credential. Provider-scoped Hyper aliases
79
+ * (`APIFUSE__PROVIDER__<ID>__HYPER_API_KEY`, measured on zozotown) map to the shared key.
80
+ */
81
+ export function engineOwnedResolverCredentialTarget(name: string): string | undefined {
82
+ const canonical = canonicalEnvName(name);
83
+ if (ENGINE_OWNED_RESOLVER_CREDENTIAL_ENV_NAME_SET.has(canonical)) return canonical;
84
+ if (/^APIFUSE__PROVIDER__.+__HYPER_API_KEY$/u.test(canonical)) {
85
+ return APIFUSE__RESOLVER__HYPERSOLUTIONS__API_KEY;
86
+ }
87
+ return undefined;
88
+ }
89
+
90
+ export function isEngineOwnedResolverCredentialName(name: string): boolean {
91
+ return engineOwnedResolverCredentialTarget(name) !== undefined;
92
+ }
93
+
58
94
  /**
59
95
  * Trace-export configuration owned by the engine and forbidden in provider
60
96
  * declarations. The header variables carry collector credentials; the rest are
@@ -75,9 +111,47 @@ export function isEngineOwnedTelemetryEnvName(name: string): boolean {
75
111
  return ENGINE_OWNED_TELEMETRY_ENV_NAME_SET.has(canonicalEnvName(name));
76
112
  }
77
113
 
114
+ /** Engine control-plane configuration is never declarable or visible to provider code. */
115
+ function isEngineOwnedEngineEnvName(name: string): boolean {
116
+ return canonicalEnvName(name).startsWith("APIFUSE__ENGINE__");
117
+ }
118
+
119
+ /**
120
+ * Hosted runtime settings the engine reads for OCR, STT, cache keying, choice
121
+ * tokens, and the CDP pool. Providers never own them: declarations are rejected
122
+ * and projections drop them, like the vendor credentials above.
123
+ */
124
+ export const ENGINE_OWNED_RUNTIME_ENV_NAMES = [
125
+ "APIFUSE__STT__CLOUDFLARE_API_TOKEN",
126
+ "APIFUSE__OCR__CLOUDFLARE_API_TOKEN",
127
+ "APIFUSE__OCR__API_KEY",
128
+ "APIFUSE__CLOUDFLARE__ACCOUNT_ID",
129
+ "APIFUSE__CACHE__KEY_PEPPER",
130
+ "APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET",
131
+ ] as const;
132
+
133
+ const ENGINE_OWNED_RUNTIME_ENV_NAME_SET = new Set<string>(ENGINE_OWNED_RUNTIME_ENV_NAMES);
134
+
135
+ /** CDP pool settings form a prefix family (`APIFUSE__CDP_POOL__URL` today); the whole prefix is engine-owned. */
136
+ const ENGINE_OWNED_CDP_POOL_ENV_PREFIX = "APIFUSE__CDP_POOL__";
137
+
138
+ export function isEngineOwnedRuntimeEnvName(name: string): boolean {
139
+ const canonical = canonicalEnvName(name);
140
+ return (
141
+ ENGINE_OWNED_RUNTIME_ENV_NAME_SET.has(canonical) ||
142
+ canonical.startsWith(ENGINE_OWNED_CDP_POOL_ENV_PREFIX)
143
+ );
144
+ }
145
+
78
146
  /** Every environment name the engine owns: rejected in declarations and filtered from provider projections. */
79
147
  export function isEngineOwnedEnvName(name: string): boolean {
80
- return isEngineOwnedProxyCredentialName(name) || isEngineOwnedTelemetryEnvName(name);
148
+ return (
149
+ isEngineOwnedEngineEnvName(name) ||
150
+ isEngineOwnedProxyCredentialName(name) ||
151
+ isEngineOwnedResolverCredentialName(name) ||
152
+ isEngineOwnedRuntimeEnvName(name) ||
153
+ isEngineOwnedTelemetryEnvName(name)
154
+ );
81
155
  }
82
156
 
83
157
  /** Capture credentials in the engine host before constructing provider bindings. */
@@ -199,6 +273,7 @@ function declaresCapability(
199
273
  provider: ProviderDefinition,
200
274
  capability: ProviderCapabilityKey,
201
275
  ): boolean {
276
+ if (capability === "credential" && provider.auth?.mode === "platform-managed") return true;
202
277
  return Object.hasOwn(provider, capability) && provider[capability] !== undefined;
203
278
  }
204
279
 
@@ -10,6 +10,7 @@ export const SDK_OWNED_PROVIDER_ERROR_CODES = new Set([
10
10
  "BROWSER_CDP_POOL_REQUIRED",
11
11
  "BROWSER_RUNTIME_UNSUPPORTED",
12
12
  "STEALTH_RUNTIME_UNSUPPORTED",
13
+ "STEALTH_HEADER_OVERRIDE_UNSUPPORTED",
13
14
  "SSE_EVENT_UNDECLARED",
14
15
  "STREAM_EVENT_TOO_LARGE",
15
16
  "STREAM_CHUNK_TOO_LARGE",
@@ -51,6 +52,13 @@ export const SDK_OWNED_PROVIDER_ERROR_CODES = new Set([
51
52
  "http_redirect_missing_location",
52
53
  "http_redirect_loop",
53
54
  "transport_invalid_url",
55
+ "EGRESS_LEASE_INVALID",
56
+ "EGRESS_LEASE_BINDING_INVALID",
57
+ "EGRESS_LEASE_KEY_MISSING",
58
+ "EGRESS_LEASE_KEY_WEAK",
59
+ "STEALTH_BODY_UNSUPPORTED",
60
+ "REPLAY_SESSION_MISMATCH",
61
+ "REPLAY_ALREADY_ATTEMPTED",
54
62
  "retry_exhausted",
55
63
  "auth_abort_unsafe_data",
56
64
  "credentials_auth_missing_credential_keys",
@@ -117,6 +125,11 @@ export const SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES: ReadonlyMap<string, Provide
117
125
  // contract): the upstream evaluated the request and said no under its
118
126
  // own rules — a conflict with upstream state, never a 5xx.
119
127
  ["UPSTREAM_REJECTED", 409],
128
+ // Only an unverifiable or foreign handle is the caller's: key and binding
129
+ // faults stay unmapped (500) because the engine host, not the caller, owns them.
130
+ ["EGRESS_LEASE_INVALID", 409],
131
+ ["REPLAY_SESSION_MISMATCH", 409],
132
+ ["REPLAY_ALREADY_ATTEMPTED", 409],
120
133
  ["UPSTREAM_ERROR", 502],
121
134
  ["BLOCKED", 502],
122
135
  ["OCR_UNAVAILABLE", 503],
package/src/index.ts CHANGED
@@ -198,17 +198,43 @@ export {
198
198
  type TelemetryKey,
199
199
  type TenantNeutral,
200
200
  } from "./runtime/request-telemetry.js";
201
+ export { APIFUSE__ENGINE__CEREMONY_LEASE_KEY } from "./runtime/egress-lease.js";
202
+ export {
203
+ ResolverTelemetryCollector,
204
+ type ResolverCacheReadTelemetryEvent,
205
+ type ResolverCacheWriteTelemetryEvent,
206
+ type ResolverFailoverTelemetryEvent,
207
+ type ResolverIdentityTelemetryEvent,
208
+ type ResolverOutcomeTelemetryEvent,
209
+ type ResolverTelemetryAttemptOutcome,
210
+ type ResolverAttemptSample,
211
+ type ResolverTelemetryCacheStatus,
212
+ type ResolverTelemetryCacheWriteReason,
213
+ type ResolverTelemetryErrorClass,
214
+ type ResolverTelemetryHeaderPayload,
215
+ type ResolverTelemetryIdentitySource,
216
+ type ResolverTelemetryLogPayload,
217
+ type ResolverTelemetryOutcome,
218
+ type ResolverTelemetryPhase,
219
+ type ResolverTelemetrySink,
220
+ type ResolverVendorAttemptTelemetryEvent,
221
+ } from "./runtime/resolver-telemetry.js";
201
222
  export {
202
223
  APIFUSE__CDP_POOL__URL,
203
224
  APIFUSE__RESOLVER__2CAPTCHA__API_KEY,
204
225
  APIFUSE__RESOLVER__CAPMONSTER__API_KEY,
205
226
  APIFUSE__RESOLVER__CAPSOLVER__API_KEY,
227
+ APIFUSE__RESOLVER__HYPERSOLUTIONS__API_KEY,
206
228
  APIFUSE__RESOLVER__TIMEOUT_MS,
207
229
  DEFAULT_RESOLVER_TIMEOUT_MS,
208
230
  } from "./runtime/resolver-config.js";
209
231
  export { createUnsupportedResolverClient } from "./runtime/resolver-shared.js";
210
232
  export type { ResolverRuntimeOptions } from "./runtime/resolver.js";
211
- export type { ResolverVendorTransport } from "./runtime/resolver-vendors/types.js";
233
+ export type {
234
+ ResolverPaidUsageContext,
235
+ ResolverVendorTransport,
236
+ ResolverVendorUnavailableReason,
237
+ } from "./runtime/resolver-vendors/types.js";
212
238
  export {
213
239
  assertRequiredSecretsPresent,
214
240
  listMissingRequiredSecrets,
@@ -250,6 +276,8 @@ export {
250
276
  type TraceContext,
251
277
  } from "./runtime/trace.js";
252
278
  export {
279
+ APIFUSE_CONTENT_PROVENANCE_META_KEY,
280
+ APIFUSE_CONTENT_TRUST_META_KEY,
253
281
  APIFUSE_DESCRIPTION_KEY_META_KEY,
254
282
  APIFUSE_REDACTION_MARKER,
255
283
  APIFUSE_SENSITIVE_KIND_META_KEY,
@@ -264,6 +292,7 @@ export {
264
292
  type SensitiveFieldOptions,
265
293
  type SensitivePath,
266
294
  sensitive,
295
+ untrustedContent,
267
296
  z,
268
297
  } from "./schema.js";
269
298
  export {
@@ -432,6 +461,7 @@ export type {
432
461
  ProviderRuntimeTarget,
433
462
  ProviderRuntimeState,
434
463
  ProviderSecretDeclaration,
464
+ ProviderStealthConfig,
435
465
  ProviderStateDurationString,
436
466
  ProviderStateNamespace,
437
467
  ProviderStreamEvent,
@@ -455,6 +485,7 @@ export type {
455
485
  StateWriteOptions,
456
486
  StealthClient,
457
487
  StealthBrowser,
488
+ StealthChallengeClassification,
458
489
  StealthCookieStore,
459
490
  StealthCookieStoreV1,
460
491
  StealthFetchOptions,
package/src/lint.ts CHANGED
@@ -398,7 +398,7 @@ function lintAuthModel(provider: {
398
398
  rule: "platform-managed-no-credential-keys",
399
399
  level: "error",
400
400
  field: "credential.keys",
401
- message: `${providerLabel} must not declare credential.keys for platform-managed auth mode.`,
401
+ message: `${providerLabel} must not declare credential.keys because platform-managed auth mode already implies the unfiltered credential capability.`,
402
402
  });
403
403
  }
404
404
 
@@ -1032,9 +1032,9 @@ function browserVersionLiteralMessage(finding: BrowserVersionLiteralFinding): st
1032
1032
  case "profile":
1033
1033
  return `Hardcoded stealth profile "${finding.literal}" pins a browser version and will rot. Select the browser and OS structurally, for example stealth: { browser: "chrome", os: "macos" }.`;
1034
1034
  case "user-agent":
1035
- return `Hardcoded User-Agent browser version "${finding.literal}" can disagree with the stealth TLS fingerprint. Remove the literal and derive it from the structured profile, for example getStealthProfile({ browser: "chrome", os: "macos" }).userAgent.`;
1035
+ return `Hardcoded User-Agent browser version "${finding.literal}" can disagree with the stealth TLS fingerprint. Remove the literal. ctx.stealth owns User-Agent and rejects a caller value with STEALTH_HEADER_OVERRIDE_UNSUPPORTED, so omit the header there and select the profile with stealth: { browser: "chrome", os: "macos" }. For ctx.http only, derive it from getStealthProfile({ browser: "chrome", os: "macos" }).userAgent.`;
1036
1036
  case "sec-ch-ua":
1037
- return 'Hardcoded sec-ch-ua versions can disagree with the stealth TLS fingerprint. Remove the literal and let ctx.stealth generate client hints from stealth: { browser: "chrome", os: "macos" }; derive any explicit User-Agent with getStealthProfile({ browser: "chrome", os: "macos" }).userAgent.';
1037
+ return 'Hardcoded sec-ch-ua versions can disagree with the stealth TLS fingerprint. Remove the literal and let ctx.stealth generate client hints from stealth: { browser: "chrome", os: "macos" }. ctx.stealth also owns User-Agent, so omit that header there as well; for ctx.http only, derive it from getStealthProfile({ browser: "chrome", os: "macos" }).userAgent.';
1038
1038
  }
1039
1039
  }
1040
1040
 
package/src/provider.ts CHANGED
@@ -133,9 +133,13 @@ export {
133
133
  createProviderEnvironment,
134
134
  createInProcessProviderEngine,
135
135
  ENGINE_OWNED_PROXY_CREDENTIAL_ENV_NAMES,
136
+ ENGINE_OWNED_RESOLVER_CREDENTIAL_ENV_NAMES,
137
+ ENGINE_OWNED_RUNTIME_ENV_NAMES,
136
138
  ENGINE_OWNED_TELEMETRY_ENV_NAMES,
137
139
  isEngineOwnedEnvName,
138
140
  isEngineOwnedProxyCredentialName,
141
+ isEngineOwnedResolverCredentialName,
142
+ isEngineOwnedRuntimeEnvName,
139
143
  isEngineOwnedTelemetryEnvName,
140
144
  PROVIDER_CAPABILITY_KEYS,
141
145
  PROVIDER_ENGINE_PROTOCOL_VERSION,
@@ -164,6 +168,8 @@ export {
164
168
  PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
165
169
  } from "./runtime/choice.js";
166
170
  export {
171
+ APIFUSE_CONTENT_PROVENANCE_META_KEY,
172
+ APIFUSE_CONTENT_TRUST_META_KEY,
167
173
  APIFUSE_DESCRIPTION_KEY_META_KEY,
168
174
  APIFUSE_REDACTION_MARKER,
169
175
  APIFUSE_SENSITIVE_KIND_META_KEY,
@@ -178,6 +184,7 @@ export {
178
184
  type SensitiveFieldOptions,
179
185
  type SensitivePath,
180
186
  sensitive,
187
+ untrustedContent,
181
188
  z,
182
189
  } from "./schema.js";
183
190
  export type {
@@ -261,6 +268,7 @@ export type {
261
268
  ProviderResolvedFile,
262
269
  ProviderRuntimeTarget,
263
270
  ProviderRuntimeState,
271
+ ProviderStealthConfig,
264
272
  ProviderStateDurationString,
265
273
  ProviderStateNamespace,
266
274
  ProviderSupportLevel,
@@ -273,6 +281,7 @@ export type {
273
281
  StateNamespaceScope,
274
282
  StateValue,
275
283
  StateWriteOptions,
284
+ StealthChallengeClassification,
276
285
  } from "./types.js";
277
286
  export {
278
287
  NativeEgressGrantExpiredError,
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Map whose entries carry an expiry and whose size is capped.
3
+ *
4
+ * Expired entries are dropped when read and swept on every insert, so a
5
+ * process that keeps minting new keys does not retain dead values. When the
6
+ * cap is still exceeded after the sweep, the least recently used entry is
7
+ * evicted and counted in `capacityEvictions` so callers can tell the two
8
+ * kinds of removal apart. Callers pass `now` explicitly so a single timestamp
9
+ * is used across a lookup-then-store sequence.
10
+ */
11
+ export class BoundedExpiringMap<K, V> {
12
+ readonly #entries = new Map<K, V>();
13
+ readonly #maxEntries: number;
14
+ readonly #expiresAt: (value: V) => number;
15
+ #capacityEvictions = 0;
16
+
17
+ constructor(maxEntries: number, expiresAt: (value: V) => number) {
18
+ if (!Number.isInteger(maxEntries) || maxEntries <= 0) {
19
+ throw new RangeError("maxEntries must be a positive integer");
20
+ }
21
+ this.#maxEntries = maxEntries;
22
+ this.#expiresAt = expiresAt;
23
+ }
24
+
25
+ get size(): number {
26
+ return this.#entries.size;
27
+ }
28
+
29
+ /** Entries removed to satisfy `maxEntries`; expiry removals are not counted. */
30
+ get capacityEvictions(): number {
31
+ return this.#capacityEvictions;
32
+ }
33
+
34
+ get(key: K, now: number): V | undefined {
35
+ if (!this.#entries.has(key)) return undefined;
36
+ const value = this.#entries.get(key) as V;
37
+ this.#entries.delete(key);
38
+ if (this.#expiresAt(value) <= now) return undefined;
39
+ // Re-insert so Map iteration order doubles as the LRU order.
40
+ this.#entries.set(key, value);
41
+ return value;
42
+ }
43
+
44
+ set(key: K, value: V, now: number): void {
45
+ for (const [existingKey, existing] of this.#entries) {
46
+ if (this.#expiresAt(existing) <= now) this.#entries.delete(existingKey);
47
+ }
48
+ this.#entries.delete(key);
49
+ if (this.#expiresAt(value) <= now) return;
50
+ this.#entries.set(key, value);
51
+ if (this.#entries.size > this.#maxEntries) {
52
+ const oldest = this.#entries.keys().next();
53
+ if (!oldest.done) {
54
+ this.#entries.delete(oldest.value);
55
+ this.#capacityEvictions += 1;
56
+ }
57
+ }
58
+ }
59
+
60
+ delete(key: K): boolean {
61
+ return this.#entries.delete(key);
62
+ }
63
+
64
+ clear(): void {
65
+ this.#entries.clear();
66
+ }
67
+ }
@@ -589,6 +589,13 @@ export function createProviderCache(options: ProviderCacheOptions): ProviderCach
589
589
  };
590
590
  }
591
591
 
592
+ const bypassCaches = new WeakSet<ProviderCache>();
593
+
594
+ /** Internal cache capability, separate from hit/miss metadata. */
595
+ export function isProviderCacheBypassed(cache: ProviderCache): boolean {
596
+ return bypassCaches.has(cache);
597
+ }
598
+
592
599
  export function createBypassProviderCache(
593
600
  options: Pick<ProviderCacheOptions, "providerId">,
594
601
  ): ProviderCache {
@@ -597,7 +604,7 @@ export function createBypassProviderCache(
597
604
  const events: ProviderCacheLookupMeta[] = [];
598
605
  const secretScopedKeys = new Set<string>();
599
606
 
600
- return {
607
+ const cache: ProviderCache = {
601
608
  key(namespace, parts, keyOptions?: ProviderCacheKeyOptions) {
602
609
  const extra = new Set((keyOptions?.redactFields ?? []).map((field) => field.toLowerCase()));
603
610
  const normalized = normalizeKeyPart(parts, extra, pepper);
@@ -643,6 +650,8 @@ export function createBypassProviderCache(
643
650
  };
644
651
  },
645
652
  };
653
+ bypassCaches.add(cache);
654
+ return cache;
646
655
  }
647
656
 
648
657
  export function resetProviderCacheForTests(): void {