@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.
- package/AUTHORING.md +25 -0
- package/CHANGELOG.md +8 -0
- package/README.md +18 -2
- package/bin/apifuse-dev.ts +2 -1
- package/bin/apifuse-pack-types.ts +49 -0
- package/bin/apifuse-perf.ts +41 -4
- package/bin/apifuse-record.ts +9 -2
- package/dist/cli/templates/provider/README.md.tpl +15 -3
- package/dist/config/loader.d.ts +7 -0
- package/dist/config/loader.js +43 -18
- package/dist/declaration-validation.d.ts +2 -1
- package/dist/declaration-validation.js +39 -0
- package/dist/define.d.ts +4 -4
- package/dist/define.js +25 -1
- package/dist/engine.d.ts +16 -0
- package/dist/engine.js +59 -3
- package/dist/error-resolution.js +13 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.js +4 -2
- package/dist/lint.js +38 -17
- package/dist/provider.d.ts +3 -3
- package/dist/provider.js +2 -2
- package/dist/runtime/bounded-expiring-map.d.ts +21 -0
- package/dist/runtime/bounded-expiring-map.js +64 -0
- package/dist/runtime/browser.js +2 -1
- package/dist/runtime/cache.d.ts +2 -0
- package/dist/runtime/cache.js +11 -3
- package/dist/runtime/diagnostic-env-values.d.ts +5 -0
- package/dist/runtime/diagnostic-env-values.js +57 -0
- package/dist/runtime/diagnostic-env.d.ts +11 -0
- package/dist/runtime/diagnostic-env.js +35 -0
- package/dist/runtime/diagnostic-redactor.d.ts +49 -0
- package/dist/runtime/diagnostic-redactor.js +569 -0
- package/dist/runtime/egress-lease.d.ts +65 -0
- package/dist/runtime/egress-lease.js +236 -0
- package/dist/runtime/env.js +2 -1
- package/dist/runtime/http.js +1 -0
- package/dist/runtime/instrumentation.js +3 -0
- package/dist/runtime/ocr.js +2 -1
- package/dist/runtime/otlp.js +5 -4
- package/dist/runtime/proxy-nodemaven.js +3 -2
- package/dist/runtime/proxy-retry-policy.d.ts +4 -1
- package/dist/runtime/proxy-retry-policy.js +11 -2
- package/dist/runtime/request-options.d.ts +7 -0
- package/dist/runtime/request-options.js +62 -22
- package/dist/runtime/request-telemetry.d.ts +7 -9
- package/dist/runtime/resolver-config.d.ts +7 -0
- package/dist/runtime/resolver-config.js +8 -0
- package/dist/runtime/resolver-public.d.ts +3 -1
- package/dist/runtime/resolver-public.js +1 -1
- package/dist/runtime/resolver-shared.d.ts +9 -0
- package/dist/runtime/resolver-shared.js +57 -0
- package/dist/runtime/resolver-telemetry.d.ts +134 -0
- package/dist/runtime/resolver-telemetry.js +204 -0
- package/dist/runtime/resolver-usage.d.ts +24 -0
- package/dist/runtime/resolver-usage.js +60 -0
- package/dist/runtime/resolver-vendors/bindings.d.ts +5 -0
- package/dist/runtime/resolver-vendors/bindings.js +5 -0
- package/dist/runtime/resolver-vendors/browser.js +0 -1
- package/dist/runtime/resolver-vendors/capsolver.d.ts +2 -2
- package/dist/runtime/resolver-vendors/capsolver.js +21 -9
- package/dist/runtime/resolver-vendors/hypersolutions.d.ts +27 -0
- package/dist/runtime/resolver-vendors/hypersolutions.js +369 -0
- package/dist/runtime/resolver-vendors/twocaptcha.d.ts +2 -2
- package/dist/runtime/resolver-vendors/twocaptcha.js +26 -10
- package/dist/runtime/resolver-vendors/types.d.ts +35 -5
- package/dist/runtime/resolver-vendors/types.js +20 -10
- package/dist/runtime/resolver.d.ts +5 -1
- package/dist/runtime/resolver.js +400 -111
- package/dist/runtime/stealth-akamai-sbsd.d.ts +54 -0
- package/dist/runtime/stealth-akamai-sbsd.js +142 -0
- package/dist/runtime/stealth.js +547 -53
- package/dist/runtime/stt.js +3 -2
- package/dist/runtime/trace.d.ts +5 -0
- package/dist/runtime/trace.js +115 -20
- package/dist/schema.d.ts +20 -0
- package/dist/schema.js +72 -2
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +1 -0
- package/dist/server/self-test-redaction.js +8 -33
- package/dist/server/self-test.d.ts +4 -0
- package/dist/server/self-test.js +10 -1
- package/dist/server/sensitive-values.d.ts +26 -0
- package/dist/server/sensitive-values.js +131 -0
- package/dist/server/serve-implementation.d.ts +7 -1
- package/dist/server/serve-implementation.js +394 -125
- package/dist/server/trace-output.d.ts +2 -1
- package/dist/server/trace-output.js +46 -14
- package/dist/server/types.d.ts +6 -0
- package/dist/server/types.js +14 -0
- package/dist/stealth/profiles.d.ts +7 -1
- package/dist/stealth/profiles.js +9 -0
- package/dist/testing/run.js +6 -0
- package/dist/trace-sanitization.d.ts +4 -3
- package/dist/trace-sanitization.js +45 -22
- package/dist/types.d.ts +106 -10
- package/package.json +2 -1
- package/src/cli/templates/provider/README.md.tpl +15 -3
- package/src/config/loader.ts +47 -18
- package/src/declaration-validation.ts +47 -1
- package/src/define.ts +45 -3
- package/src/engine.ts +78 -3
- package/src/error-resolution.ts +13 -0
- package/src/index.ts +34 -1
- package/src/lint.ts +44 -18
- package/src/provider.ts +10 -0
- package/src/runtime/bounded-expiring-map.ts +67 -0
- package/src/runtime/browser.ts +2 -1
- package/src/runtime/cache.ts +13 -3
- package/src/runtime/diagnostic-env-values.ts +56 -0
- package/src/runtime/diagnostic-env.ts +55 -0
- package/src/runtime/diagnostic-redactor.ts +662 -0
- package/src/runtime/egress-lease.ts +341 -0
- package/src/runtime/env.ts +2 -1
- package/src/runtime/http.ts +1 -0
- package/src/runtime/instrumentation.ts +3 -0
- package/src/runtime/ocr.ts +2 -1
- package/src/runtime/otlp.ts +5 -4
- package/src/runtime/proxy-nodemaven.ts +3 -2
- package/src/runtime/proxy-retry-policy.ts +12 -1
- package/src/runtime/request-options.ts +76 -28
- package/src/runtime/request-telemetry.ts +5 -7
- package/src/runtime/resolver-config.ts +12 -0
- package/src/runtime/resolver-public.ts +21 -0
- package/src/runtime/resolver-shared.ts +75 -0
- package/src/runtime/resolver-telemetry.ts +362 -0
- package/src/runtime/resolver-usage.ts +96 -0
- package/src/runtime/resolver-vendors/bindings.ts +5 -0
- package/src/runtime/resolver-vendors/browser.ts +0 -1
- package/src/runtime/resolver-vendors/capsolver.ts +22 -3
- package/src/runtime/resolver-vendors/hypersolutions.ts +480 -0
- package/src/runtime/resolver-vendors/twocaptcha.ts +26 -12
- package/src/runtime/resolver-vendors/types.ts +60 -12
- package/src/runtime/resolver.ts +468 -126
- package/src/runtime/stealth-akamai-sbsd.ts +207 -0
- package/src/runtime/stealth.ts +727 -95
- package/src/runtime/stt.ts +3 -2
- package/src/runtime/trace.ts +168 -26
- package/src/schema.ts +80 -2
- package/src/server/index.ts +21 -0
- package/src/server/self-test-redaction.ts +10 -40
- package/src/server/self-test.ts +13 -0
- package/src/server/sensitive-values.ts +200 -0
- package/src/server/serve-implementation.ts +561 -136
- package/src/server/trace-output.ts +77 -17
- package/src/server/types.ts +14 -0
- package/src/stealth/profiles.ts +10 -0
- package/src/testing/run.ts +6 -0
- package/src/trace-sanitization.ts +70 -23
- 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
|
|
554
|
-
|
|
555
|
-
|
|
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
|
-
|
|
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
|
-
|
|
596
|
-
|
|
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) && !
|
|
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
|
|
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" }
|
|
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.*()
|
|
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.*()
|
|
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
|
+
}
|
package/src/runtime/browser.ts
CHANGED
|
@@ -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
|
|
499
|
+
return readDiagnosticEnv("APIFUSE__CDP_POOL__URL", env);
|
|
499
500
|
}
|
|
500
501
|
|
|
501
502
|
async function importOptionalModule<T extends object>(moduleName: string): Promise<T> {
|
package/src/runtime/cache.ts
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
+
}
|