@apifuse/provider-sdk 2.2.0-beta.58 → 2.2.0-beta.60

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 (150) hide show
  1. package/AUTHORING.md +25 -0
  2. package/CHANGELOG.md +8 -0
  3. package/README.md +18 -2
  4. package/bin/apifuse-dev.ts +2 -1
  5. package/bin/apifuse-pack-types.ts +49 -0
  6. package/bin/apifuse-perf.ts +41 -4
  7. package/bin/apifuse-record.ts +9 -2
  8. package/dist/cli/templates/provider/README.md.tpl +15 -3
  9. package/dist/config/loader.d.ts +7 -0
  10. package/dist/config/loader.js +43 -18
  11. package/dist/declaration-validation.d.ts +2 -1
  12. package/dist/declaration-validation.js +39 -0
  13. package/dist/define.d.ts +4 -4
  14. package/dist/define.js +25 -1
  15. package/dist/engine.d.ts +16 -0
  16. package/dist/engine.js +59 -3
  17. package/dist/error-resolution.js +13 -0
  18. package/dist/index.d.ts +7 -4
  19. package/dist/index.js +4 -2
  20. package/dist/lint.js +38 -17
  21. package/dist/provider.d.ts +3 -3
  22. package/dist/provider.js +2 -2
  23. package/dist/runtime/bounded-expiring-map.d.ts +21 -0
  24. package/dist/runtime/bounded-expiring-map.js +64 -0
  25. package/dist/runtime/browser.js +2 -1
  26. package/dist/runtime/cache.d.ts +2 -0
  27. package/dist/runtime/cache.js +11 -3
  28. package/dist/runtime/diagnostic-env-values.d.ts +5 -0
  29. package/dist/runtime/diagnostic-env-values.js +57 -0
  30. package/dist/runtime/diagnostic-env.d.ts +11 -0
  31. package/dist/runtime/diagnostic-env.js +35 -0
  32. package/dist/runtime/diagnostic-redactor.d.ts +49 -0
  33. package/dist/runtime/diagnostic-redactor.js +569 -0
  34. package/dist/runtime/egress-lease.d.ts +65 -0
  35. package/dist/runtime/egress-lease.js +236 -0
  36. package/dist/runtime/env.js +2 -1
  37. package/dist/runtime/http.js +1 -0
  38. package/dist/runtime/instrumentation.js +3 -0
  39. package/dist/runtime/ocr.js +2 -1
  40. package/dist/runtime/otlp.js +5 -4
  41. package/dist/runtime/proxy-nodemaven.js +3 -2
  42. package/dist/runtime/proxy-retry-policy.d.ts +4 -1
  43. package/dist/runtime/proxy-retry-policy.js +11 -2
  44. package/dist/runtime/request-options.d.ts +7 -0
  45. package/dist/runtime/request-options.js +62 -22
  46. package/dist/runtime/request-telemetry.d.ts +7 -9
  47. package/dist/runtime/resolver-config.d.ts +7 -0
  48. package/dist/runtime/resolver-config.js +8 -0
  49. package/dist/runtime/resolver-public.d.ts +3 -1
  50. package/dist/runtime/resolver-public.js +1 -1
  51. package/dist/runtime/resolver-shared.d.ts +9 -0
  52. package/dist/runtime/resolver-shared.js +57 -0
  53. package/dist/runtime/resolver-telemetry.d.ts +134 -0
  54. package/dist/runtime/resolver-telemetry.js +204 -0
  55. package/dist/runtime/resolver-usage.d.ts +24 -0
  56. package/dist/runtime/resolver-usage.js +60 -0
  57. package/dist/runtime/resolver-vendors/bindings.d.ts +5 -0
  58. package/dist/runtime/resolver-vendors/bindings.js +5 -0
  59. package/dist/runtime/resolver-vendors/browser.js +0 -1
  60. package/dist/runtime/resolver-vendors/capsolver.d.ts +2 -2
  61. package/dist/runtime/resolver-vendors/capsolver.js +21 -9
  62. package/dist/runtime/resolver-vendors/hypersolutions.d.ts +27 -0
  63. package/dist/runtime/resolver-vendors/hypersolutions.js +369 -0
  64. package/dist/runtime/resolver-vendors/twocaptcha.d.ts +2 -2
  65. package/dist/runtime/resolver-vendors/twocaptcha.js +26 -10
  66. package/dist/runtime/resolver-vendors/types.d.ts +35 -5
  67. package/dist/runtime/resolver-vendors/types.js +20 -10
  68. package/dist/runtime/resolver.d.ts +5 -1
  69. package/dist/runtime/resolver.js +400 -111
  70. package/dist/runtime/stealth-akamai-sbsd.d.ts +54 -0
  71. package/dist/runtime/stealth-akamai-sbsd.js +142 -0
  72. package/dist/runtime/stealth.js +547 -53
  73. package/dist/runtime/stt.js +3 -2
  74. package/dist/runtime/trace.d.ts +5 -0
  75. package/dist/runtime/trace.js +115 -20
  76. package/dist/schema.d.ts +20 -0
  77. package/dist/schema.js +72 -2
  78. package/dist/server/index.d.ts +2 -0
  79. package/dist/server/index.js +1 -0
  80. package/dist/server/self-test-redaction.js +8 -33
  81. package/dist/server/self-test.d.ts +4 -0
  82. package/dist/server/self-test.js +10 -1
  83. package/dist/server/sensitive-values.d.ts +26 -0
  84. package/dist/server/sensitive-values.js +131 -0
  85. package/dist/server/serve-implementation.d.ts +7 -1
  86. package/dist/server/serve-implementation.js +394 -125
  87. package/dist/server/trace-output.d.ts +2 -1
  88. package/dist/server/trace-output.js +46 -14
  89. package/dist/server/types.d.ts +6 -0
  90. package/dist/server/types.js +14 -0
  91. package/dist/stealth/profiles.d.ts +7 -1
  92. package/dist/stealth/profiles.js +9 -0
  93. package/dist/testing/run.js +6 -0
  94. package/dist/trace-sanitization.d.ts +4 -3
  95. package/dist/trace-sanitization.js +45 -22
  96. package/dist/types.d.ts +106 -10
  97. package/package.json +2 -1
  98. package/src/cli/templates/provider/README.md.tpl +15 -3
  99. package/src/config/loader.ts +47 -18
  100. package/src/declaration-validation.ts +47 -1
  101. package/src/define.ts +45 -3
  102. package/src/engine.ts +78 -3
  103. package/src/error-resolution.ts +13 -0
  104. package/src/index.ts +34 -1
  105. package/src/lint.ts +44 -18
  106. package/src/provider.ts +10 -0
  107. package/src/runtime/bounded-expiring-map.ts +67 -0
  108. package/src/runtime/browser.ts +2 -1
  109. package/src/runtime/cache.ts +13 -3
  110. package/src/runtime/diagnostic-env-values.ts +56 -0
  111. package/src/runtime/diagnostic-env.ts +55 -0
  112. package/src/runtime/diagnostic-redactor.ts +662 -0
  113. package/src/runtime/egress-lease.ts +341 -0
  114. package/src/runtime/env.ts +2 -1
  115. package/src/runtime/http.ts +1 -0
  116. package/src/runtime/instrumentation.ts +3 -0
  117. package/src/runtime/ocr.ts +2 -1
  118. package/src/runtime/otlp.ts +5 -4
  119. package/src/runtime/proxy-nodemaven.ts +3 -2
  120. package/src/runtime/proxy-retry-policy.ts +12 -1
  121. package/src/runtime/request-options.ts +76 -28
  122. package/src/runtime/request-telemetry.ts +5 -7
  123. package/src/runtime/resolver-config.ts +12 -0
  124. package/src/runtime/resolver-public.ts +21 -0
  125. package/src/runtime/resolver-shared.ts +75 -0
  126. package/src/runtime/resolver-telemetry.ts +362 -0
  127. package/src/runtime/resolver-usage.ts +96 -0
  128. package/src/runtime/resolver-vendors/bindings.ts +5 -0
  129. package/src/runtime/resolver-vendors/browser.ts +0 -1
  130. package/src/runtime/resolver-vendors/capsolver.ts +22 -3
  131. package/src/runtime/resolver-vendors/hypersolutions.ts +480 -0
  132. package/src/runtime/resolver-vendors/twocaptcha.ts +26 -12
  133. package/src/runtime/resolver-vendors/types.ts +60 -12
  134. package/src/runtime/resolver.ts +468 -126
  135. package/src/runtime/stealth-akamai-sbsd.ts +207 -0
  136. package/src/runtime/stealth.ts +727 -95
  137. package/src/runtime/stt.ts +3 -2
  138. package/src/runtime/trace.ts +168 -26
  139. package/src/schema.ts +80 -2
  140. package/src/server/index.ts +21 -0
  141. package/src/server/self-test-redaction.ts +10 -40
  142. package/src/server/self-test.ts +13 -0
  143. package/src/server/sensitive-values.ts +200 -0
  144. package/src/server/serve-implementation.ts +561 -136
  145. package/src/server/trace-output.ts +77 -17
  146. package/src/server/types.ts +14 -0
  147. package/src/stealth/profiles.ts +10 -0
  148. package/src/testing/run.ts +6 -0
  149. package/src/trace-sanitization.ts +70 -23
  150. package/src/types.ts +124 -19
package/src/lint.ts CHANGED
@@ -550,18 +550,29 @@ function uniqueFields(fields: string[]): string[] {
550
550
  return Array.from(new Set(fields));
551
551
  }
552
552
 
553
- function isSensitiveSchema(schema: unknown): boolean {
554
- if (!schema || typeof schema !== "object" || !("meta" in schema)) {
555
- return false;
553
+ function readSensitivityDeclaration(
554
+ schema: unknown,
555
+ seen = new Set<unknown>(),
556
+ ): boolean | undefined {
557
+ // Declarations sit on the leaf; same-path wrappers such as .optional(),
558
+ // .nullable(), .pipe() or z.lazy() carry no metadata of their own, so look
559
+ // through them.
560
+ if (!isSchema(schema) || seen.has(schema)) return undefined;
561
+ seen.add(schema);
562
+ const value = Reflect.get(getSchemaMetadata(schema), APIFUSE_SENSITIVE_META_KEY);
563
+ if (typeof value === "boolean") return value;
564
+ const def = getSchemaDef(schema);
565
+ const children =
566
+ def.type === "pipe"
567
+ ? [def.in, def.out]
568
+ : def.type === "lazy" && typeof def.getter === "function"
569
+ ? [def.getter()]
570
+ : [def.innerType];
571
+ for (const child of children) {
572
+ const declaration = readSensitivityDeclaration(child, seen);
573
+ if (declaration !== undefined) return declaration;
556
574
  }
557
- const meta = schema.meta;
558
- if (typeof meta !== "function") return false;
559
- const metadata = meta.call(schema);
560
- return (
561
- !!metadata &&
562
- typeof metadata === "object" &&
563
- Reflect.get(metadata, APIFUSE_SENSITIVE_META_KEY) === true
564
- );
575
+ return undefined;
565
576
  }
566
577
 
567
578
  function getSchemaMetadata(schema: SchemaLike): Record<string, unknown> {
@@ -591,9 +602,24 @@ const SENSITIVE_FIELD_NAMES = new Set([
591
602
  "paymenturl",
592
603
  ]);
593
604
 
605
+ // Only phone-shaped keys can hold reviewed public data (facility phones); the
606
+ // rest are credentials, which a public declaration must not launder.
607
+ const PUBLIC_DECLARABLE_FIELD_NAMES = new Set(["phone", "phonenumber"]);
608
+
609
+ function normalizeFieldName(name: string): string {
610
+ return name.toLowerCase().replace(/[-_\s]/g, "");
611
+ }
612
+
594
613
  function isSensitiveFieldName(name: string): boolean {
595
- const normalized = name.toLowerCase().replace(/[-_\s]/g, "");
596
- return SENSITIVE_FIELD_NAMES.has(normalized);
614
+ return SENSITIVE_FIELD_NAMES.has(normalizeFieldName(name));
615
+ }
616
+
617
+ function isDeclaredSensitiveField(key: string, schema: unknown): boolean {
618
+ const declaration = readSensitivityDeclaration(schema);
619
+ return (
620
+ declaration === true ||
621
+ (declaration === false && PUBLIC_DECLARABLE_FIELD_NAMES.has(normalizeFieldName(key)))
622
+ );
597
623
  }
598
624
 
599
625
  function collectUnmarkedSensitiveFields(
@@ -608,7 +634,7 @@ function collectUnmarkedSensitiveFields(
608
634
  const out: string[] = [];
609
635
  for (const [key, child] of Object.entries(getObjectShape(schema))) {
610
636
  const childPath = basePath ? `${basePath}.${key}` : key;
611
- if (isSensitiveFieldName(key) && !isSensitiveSchema(child)) {
637
+ if (isSensitiveFieldName(key) && !isDeclaredSensitiveField(key, child)) {
612
638
  out.push(childPath);
613
639
  }
614
640
  out.push(...collectUnmarkedSensitiveFields(child, childPath, seen));
@@ -1032,9 +1058,9 @@ function browserVersionLiteralMessage(finding: BrowserVersionLiteralFinding): st
1032
1058
  case "profile":
1033
1059
  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
1060
  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.`;
1061
+ 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
1062
  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.';
1063
+ 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
1064
  }
1039
1065
  }
1040
1066
 
@@ -1401,7 +1427,7 @@ export function lintOperation(op: {
1401
1427
  rule: "sensitive-field-unmarked",
1402
1428
  level: "warn",
1403
1429
  field,
1404
- message: `Schema field "${field}" looks sensitive; mark it with fields.*(), field(..., { sensitive: true }), or sensitive(...).`,
1430
+ message: `Schema field "${field}" looks sensitive; mark it with fields.*() or sensitive(...); publicField(...) is accepted only for reviewed public phone-shaped fields.`,
1405
1431
  });
1406
1432
  }
1407
1433
 
@@ -1410,7 +1436,7 @@ export function lintOperation(op: {
1410
1436
  rule: "sensitive-field-unmarked",
1411
1437
  level: "warn",
1412
1438
  field,
1413
- message: `Schema field "${field}" looks sensitive; mark it with fields.*(), field(..., { sensitive: true }), or sensitive(...).`,
1439
+ message: `Schema field "${field}" looks sensitive; mark it with fields.*() or sensitive(...); publicField(...) is accepted only for reviewed public phone-shaped fields.`,
1414
1440
  });
1415
1441
  }
1416
1442
 
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,
@@ -173,11 +179,13 @@ export {
173
179
  field,
174
180
  fields,
175
181
  isSensitiveSchema,
182
+ publicField,
176
183
  redactPayload,
177
184
  type SensitiveFieldKind,
178
185
  type SensitiveFieldOptions,
179
186
  type SensitivePath,
180
187
  sensitive,
188
+ untrustedContent,
181
189
  z,
182
190
  } from "./schema.js";
183
191
  export type {
@@ -261,6 +269,7 @@ export type {
261
269
  ProviderResolvedFile,
262
270
  ProviderRuntimeTarget,
263
271
  ProviderRuntimeState,
272
+ ProviderStealthConfig,
264
273
  ProviderStateDurationString,
265
274
  ProviderStateNamespace,
266
275
  ProviderSupportLevel,
@@ -273,6 +282,7 @@ export type {
273
282
  StateNamespaceScope,
274
283
  StateValue,
275
284
  StateWriteOptions,
285
+ StealthChallengeClassification,
276
286
  } from "./types.js";
277
287
  export {
278
288
  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
+ }
@@ -1,3 +1,4 @@
1
+ import { readDiagnosticEnv } from "./diagnostic-env.js";
1
2
  import { createRequire } from "node:module";
2
3
  import type { CDPSession, Frame, LaunchOptions, Locator, Page, Request, Route } from "playwright";
3
4
 
@@ -495,7 +496,7 @@ type SupportedBrowserClient = {
495
496
  };
496
497
 
497
498
  function getDefaultCdpPoolUrl(env = process.env): string | undefined {
498
- return env.APIFUSE__CDP_POOL__URL;
499
+ return readDiagnosticEnv("APIFUSE__CDP_POOL__URL", env);
499
500
  }
500
501
 
501
502
  async function importOptionalModule<T extends object>(moduleName: string): Promise<T> {
@@ -1,3 +1,4 @@
1
+ import { readDiagnosticEnv } from "./diagnostic-env.js";
1
2
  import { createHash, createHmac } from "node:crypto";
2
3
 
3
4
  import { providerCacheRedisUrlFromEnv } from "../config/loader.js";
@@ -362,7 +363,7 @@ async function withRedisFallback<T>(operation: () => Promise<T>): Promise<T | un
362
363
 
363
364
  export function createProviderCache(options: ProviderCacheOptions): ProviderCache {
364
365
  const redisUrl = options.redisUrl ?? providerCacheRedisUrlFromEnv();
365
- const configuredPepper = process.env[APIFUSE__CACHE__KEY_PEPPER_ENV];
366
+ const configuredPepper = readDiagnosticEnv(APIFUSE__CACHE__KEY_PEPPER_ENV);
366
367
  const pepper = configuredPepper && configuredPepper.length > 0 ? configuredPepper : undefined;
367
368
  const backend = getSharedBackend(redisUrl);
368
369
  const memoryMaxEntries = Math.max(1, options.memoryMaxEntries ?? DEFAULT_MEMORY_MAX_ENTRIES);
@@ -589,15 +590,22 @@ export function createProviderCache(options: ProviderCacheOptions): ProviderCach
589
590
  };
590
591
  }
591
592
 
593
+ const bypassCaches = new WeakSet<ProviderCache>();
594
+
595
+ /** Internal cache capability, separate from hit/miss metadata. */
596
+ export function isProviderCacheBypassed(cache: ProviderCache): boolean {
597
+ return bypassCaches.has(cache);
598
+ }
599
+
592
600
  export function createBypassProviderCache(
593
601
  options: Pick<ProviderCacheOptions, "providerId">,
594
602
  ): ProviderCache {
595
- const configuredPepper = process.env[APIFUSE__CACHE__KEY_PEPPER_ENV];
603
+ const configuredPepper = readDiagnosticEnv(APIFUSE__CACHE__KEY_PEPPER_ENV);
596
604
  const pepper = configuredPepper && configuredPepper.length > 0 ? configuredPepper : undefined;
597
605
  const events: ProviderCacheLookupMeta[] = [];
598
606
  const secretScopedKeys = new Set<string>();
599
607
 
600
- return {
608
+ const cache: ProviderCache = {
601
609
  key(namespace, parts, keyOptions?: ProviderCacheKeyOptions) {
602
610
  const extra = new Set((keyOptions?.redactFields ?? []).map((field) => field.toLowerCase()));
603
611
  const normalized = normalizeKeyPart(parts, extra, pepper);
@@ -643,6 +651,8 @@ export function createBypassProviderCache(
643
651
  };
644
652
  },
645
653
  };
654
+ bypassCaches.add(cache);
655
+ return cache;
646
656
  }
647
657
 
648
658
  export function resetProviderCacheForTests(): void {
@@ -0,0 +1,56 @@
1
+ export function resolvedStringVariants(value: string | undefined): string[] {
2
+ if (value === undefined) return [];
3
+ return [...new Set([value, value.trim()])].filter((candidate) => candidate.length > 0);
4
+ }
5
+
6
+ function decodedComponent(value: string): string {
7
+ try {
8
+ return decodeURIComponent(value);
9
+ } catch {
10
+ return value;
11
+ }
12
+ }
13
+
14
+ export function urlCredentialComponents(
15
+ value: string,
16
+ options: { includePathAndQuery: boolean },
17
+ ): string[] {
18
+ let url: URL;
19
+ try {
20
+ url = new URL(value.trim());
21
+ } catch {
22
+ return [];
23
+ }
24
+ const username = decodedComponent(url.username);
25
+ const password = decodedComponent(url.password);
26
+ const components = [username, password, username && password ? `${username}:${password}` : ""];
27
+ if (options.includePathAndQuery) {
28
+ for (const segment of url.pathname.split("/")) {
29
+ if (segment) components.push(decodedComponent(segment));
30
+ }
31
+ for (const [key, entryValue] of url.searchParams) {
32
+ if (key) components.push(key);
33
+ if (entryValue) {
34
+ components.push(entryValue, `${key}=${entryValue}`);
35
+ }
36
+ }
37
+ }
38
+ return [...new Set(components.flatMap((component) => resolvedStringVariants(component)))];
39
+ }
40
+
41
+ export function otlpHeaderCredentialValues(value: string): string[] {
42
+ const variants = resolvedStringVariants(value);
43
+ for (const member of value.split(",")) {
44
+ const separator = member.indexOf("=");
45
+ if (separator <= 0) continue;
46
+ const headerValue = decodedComponent(member.slice(separator + 1).trim());
47
+ variants.push(...resolvedStringVariants(headerValue));
48
+ const headerName = decodedComponent(member.slice(0, separator).trim()).toLowerCase();
49
+ if (headerName === "authorization" || headerName === "proxy-authorization") {
50
+ // A transport may echo just the credential without its HTTP auth scheme.
51
+ const credential = /^(?:Bearer|Basic)\s+(.+)$/i.exec(headerValue)?.[1];
52
+ if (credential) variants.push(...resolvedStringVariants(credential));
53
+ }
54
+ }
55
+ return [...new Set(variants)];
56
+ }
@@ -0,0 +1,55 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+
3
+ import {
4
+ resolvedStringVariants,
5
+ urlCredentialComponents,
6
+ otlpHeaderCredentialValues,
7
+ } from "./diagnostic-env-values.js";
8
+ import {
9
+ type CompiledDiagnosticSensitiveValues,
10
+ registerProcessDiagnosticValues,
11
+ suppressProcessDiagnosticValues,
12
+ withAppDiagnosticFallback,
13
+ } from "./diagnostic-redactor.js";
14
+
15
+ type EnvObserver = (name: string, value: string) => void;
16
+ export interface DiagnosticEnvScope {
17
+ observe: EnvObserver;
18
+ finished: boolean;
19
+ staticValues?: CompiledDiagnosticSensitiveValues;
20
+ }
21
+ const observers = new AsyncLocalStorage<DiagnosticEnvScope>();
22
+
23
+ /** Resolve and register atomically, before any consumer can use a live env credential. */
24
+ export function readDiagnosticEnv(
25
+ name: string,
26
+ env: Readonly<Record<string, string | undefined>> = process.env,
27
+ ): string | undefined {
28
+ const value = env[name];
29
+ if (value !== undefined) {
30
+ const scope = observers.getStore();
31
+ if (scope && !scope.finished) scope.observe(name, value);
32
+ else {
33
+ // ALS survives the response. Finished requests register into the app's
34
+ // inherited fallback lifetime, never a dead request's private inventory.
35
+ if (Buffer.byteLength(value) > 64 * 1024) suppressProcessDiagnosticValues();
36
+ else
37
+ registerProcessDiagnosticValues([
38
+ ...resolvedStringVariants(value),
39
+ ...urlCredentialComponents(value, {
40
+ includePathAndQuery: name === "APIFUSE__CDP_POOL__URL",
41
+ }),
42
+ ...(name === "OTEL_EXPORTER_OTLP_HEADERS" || name === "OTEL_EXPORTER_OTLP_TRACES_HEADERS"
43
+ ? otlpHeaderCredentialValues(value)
44
+ : []),
45
+ ]);
46
+ }
47
+ }
48
+ return value;
49
+ }
50
+
51
+ export function withDiagnosticEnv<T>(observer: EnvObserver | DiagnosticEnvScope, fn: () => T): T {
52
+ const scope = typeof observer === "function" ? { observe: observer, finished: false } : observer;
53
+ const run = () => observers.run(scope, fn);
54
+ return scope.staticValues ? withAppDiagnosticFallback(scope.staticValues, run) : run();
55
+ }