@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/config/loader.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readDiagnosticEnv } from "../runtime/diagnostic-env.js";
|
|
1
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
3
|
import { existsSync } from "node:fs";
|
|
3
4
|
import { createRequire } from "node:module";
|
|
@@ -6,6 +7,7 @@ import path from "node:path";
|
|
|
6
7
|
import type { Redis } from "ioredis";
|
|
7
8
|
|
|
8
9
|
import type { ProviderProxyPolicy, ProviderProxyProvider, TraceConfig } from "../types.js";
|
|
10
|
+
import { BoundedExpiringMap } from "../runtime/bounded-expiring-map.js";
|
|
9
11
|
import {
|
|
10
12
|
NODEMAVEN_DEFAULT_PROTOCOL,
|
|
11
13
|
NODEMAVEN_FILTER_ENV,
|
|
@@ -223,7 +225,13 @@ type ProxyRedisClient = Pick<
|
|
|
223
225
|
"connect" | "del" | "eval" | "get" | "on" | "pttl" | "set" | "status"
|
|
224
226
|
>;
|
|
225
227
|
|
|
226
|
-
|
|
228
|
+
// Keyed per (policy, affinity). Connection affinities are minted by callers, so
|
|
229
|
+
// without a cap a long-lived process accumulates one pool per affinity it ever
|
|
230
|
+
// saw. Evicting a still-fresh pool forces a fresh allocation (a different
|
|
231
|
+
// egress endpoint) for that affinity, so the cap is generous and every
|
|
232
|
+
// capacity eviction is reported as `diagnostics.poolCacheEvictions`.
|
|
233
|
+
const SMARTPROXY_POOL_CACHE_MAX_ENTRIES = 10_000;
|
|
234
|
+
let proxyCache = createSmartproxyPoolCache(SMARTPROXY_POOL_CACHE_MAX_ENTRIES);
|
|
227
235
|
const require = createRequire(import.meta.url);
|
|
228
236
|
const proxyInflight = new Map<string, Promise<SmartproxyAllocationResult>>();
|
|
229
237
|
const invalidatedProxyKeys = new Map<string, number>();
|
|
@@ -249,8 +257,8 @@ const SMARTPROXY_EXTRACTION_SOFT_REFRESH_MS = 10_000;
|
|
|
249
257
|
|
|
250
258
|
function redisUrlFromEnv(): string | undefined {
|
|
251
259
|
return (
|
|
252
|
-
|
|
253
|
-
|
|
260
|
+
readDiagnosticEnv(PROVIDER_CACHE_REDIS_URL_ENV)?.trim() ||
|
|
261
|
+
readDiagnosticEnv(REDIS_URL_ENV)?.trim() ||
|
|
254
262
|
undefined
|
|
255
263
|
);
|
|
256
264
|
}
|
|
@@ -261,9 +269,9 @@ export function providerCacheRedisUrlFromEnv(): string | undefined {
|
|
|
261
269
|
|
|
262
270
|
export function providerStateRedisUrlFromEnv(): string | undefined {
|
|
263
271
|
return (
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
272
|
+
readDiagnosticEnv(PROVIDER_STATE_REDIS_URL_ENV)?.trim() ||
|
|
273
|
+
readDiagnosticEnv(PROVIDER_CACHE_REDIS_URL_ENV)?.trim() ||
|
|
274
|
+
readDiagnosticEnv(REDIS_URL_ENV)?.trim() ||
|
|
267
275
|
undefined
|
|
268
276
|
);
|
|
269
277
|
}
|
|
@@ -277,6 +285,22 @@ export function __setSmartproxyAllocatorDeadlineMsForTests(deadlineMs: number |
|
|
|
277
285
|
smartproxyAllocatorDeadlineMsForTests = deadlineMs;
|
|
278
286
|
}
|
|
279
287
|
|
|
288
|
+
/** @internal Test-only hook for exercising the pool-cache capacity bound at a small size. */
|
|
289
|
+
export function __setSmartproxyPoolCacheMaxEntriesForTests(maxEntries: number | undefined): void {
|
|
290
|
+
proxyCache = createSmartproxyPoolCache(maxEntries ?? SMARTPROXY_POOL_CACHE_MAX_ENTRIES);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** @internal Test-only view of the in-memory Smartproxy cache sizes. */
|
|
294
|
+
export function __getSmartproxyCacheSizesForTests(): { pools: number; tombstones: number } {
|
|
295
|
+
return { pools: proxyCache.size, tombstones: invalidatedProxyKeys.size };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function createSmartproxyPoolCache(
|
|
299
|
+
maxEntries: number,
|
|
300
|
+
): BoundedExpiringMap<string, CachedProxyPool> {
|
|
301
|
+
return new BoundedExpiringMap(maxEntries, (pool) => pool.expiresAt);
|
|
302
|
+
}
|
|
303
|
+
|
|
280
304
|
function getProxyRedis(): ProxyRedisClient | undefined {
|
|
281
305
|
if (proxyRedisForTests) return proxyRedisForTests;
|
|
282
306
|
const redisUrl = redisUrlFromEnv();
|
|
@@ -670,12 +694,12 @@ export async function resolveWithVendor(
|
|
|
670
694
|
const startedAt = Date.now();
|
|
671
695
|
const username = (
|
|
672
696
|
context.credentials === undefined
|
|
673
|
-
?
|
|
697
|
+
? readDiagnosticEnv(NODEMAVEN_USERNAME_ENV)
|
|
674
698
|
: context.credentials[NODEMAVEN_USERNAME_ENV]
|
|
675
699
|
)?.trim();
|
|
676
700
|
const password = (
|
|
677
701
|
context.credentials === undefined
|
|
678
|
-
?
|
|
702
|
+
? readDiagnosticEnv(NODEMAVEN_PASSWORD_ENV)
|
|
679
703
|
: context.credentials[NODEMAVEN_PASSWORD_ENV]
|
|
680
704
|
)?.trim();
|
|
681
705
|
const filter =
|
|
@@ -726,7 +750,7 @@ export async function resolveWithVendor(
|
|
|
726
750
|
// smartproxy allocation-style vendor.
|
|
727
751
|
const appKey = (
|
|
728
752
|
context.credentials === undefined
|
|
729
|
-
?
|
|
753
|
+
? readDiagnosticEnv(SMARTPROXY_APP_KEY_ENV)
|
|
730
754
|
: context.credentials[SMARTPROXY_APP_KEY_ENV]
|
|
731
755
|
)?.trim();
|
|
732
756
|
if (!appKey) {
|
|
@@ -760,6 +784,7 @@ export async function resolveWithVendor(
|
|
|
760
784
|
...allocated.pool.diagnostics,
|
|
761
785
|
poolSize: allocated.pool.urls.length,
|
|
762
786
|
poolIndex,
|
|
787
|
+
poolCacheEvictions: proxyCache.capacityEvictions,
|
|
763
788
|
},
|
|
764
789
|
};
|
|
765
790
|
}
|
|
@@ -814,18 +839,18 @@ function vendorHasCredentials(
|
|
|
814
839
|
if (vendor === "nodemaven") {
|
|
815
840
|
return Boolean(
|
|
816
841
|
(credentials === undefined
|
|
817
|
-
?
|
|
842
|
+
? readDiagnosticEnv(NODEMAVEN_USERNAME_ENV)
|
|
818
843
|
: credentials[NODEMAVEN_USERNAME_ENV]
|
|
819
844
|
)?.trim() &&
|
|
820
845
|
(credentials === undefined
|
|
821
|
-
?
|
|
846
|
+
? readDiagnosticEnv(NODEMAVEN_PASSWORD_ENV)
|
|
822
847
|
: credentials[NODEMAVEN_PASSWORD_ENV]
|
|
823
848
|
)?.trim(),
|
|
824
849
|
);
|
|
825
850
|
}
|
|
826
851
|
return Boolean(
|
|
827
852
|
(credentials === undefined
|
|
828
|
-
?
|
|
853
|
+
? readDiagnosticEnv(SMARTPROXY_APP_KEY_ENV)
|
|
829
854
|
: credentials[SMARTPROXY_APP_KEY_ENV]
|
|
830
855
|
)?.trim(),
|
|
831
856
|
);
|
|
@@ -1082,8 +1107,8 @@ async function allocateSmartproxy(
|
|
|
1082
1107
|
const now = startedAt;
|
|
1083
1108
|
const invalidatedUntil = invalidatedProxyKeys.get(cacheKey) ?? 0;
|
|
1084
1109
|
const skipCached = invalidatedUntil > now;
|
|
1085
|
-
const cached = proxyCache.get(cacheKey);
|
|
1086
|
-
if (
|
|
1110
|
+
const cached = skipCached ? undefined : proxyCache.get(cacheKey, now);
|
|
1111
|
+
if (cached) {
|
|
1087
1112
|
if (shouldSoftRefresh(cached, now)) {
|
|
1088
1113
|
void refreshSmartproxyPool(
|
|
1089
1114
|
cacheKey,
|
|
@@ -1158,7 +1183,7 @@ async function readSmartproxyRedisPool(
|
|
|
1158
1183
|
}
|
|
1159
1184
|
const now = Date.now();
|
|
1160
1185
|
if (!isFresh(pool, now)) return null;
|
|
1161
|
-
proxyCache.set(cacheKey, pool);
|
|
1186
|
+
proxyCache.set(cacheKey, pool, now);
|
|
1162
1187
|
return {
|
|
1163
1188
|
pool,
|
|
1164
1189
|
telemetry: telemetryForPool(pool, "redis_hit", startedAt, { redisReadMs }),
|
|
@@ -1482,7 +1507,7 @@ async function allocateAndStoreSmartproxyPool(
|
|
|
1482
1507
|
rawConnect: true,
|
|
1483
1508
|
},
|
|
1484
1509
|
};
|
|
1485
|
-
proxyCache.set(cacheKey, result);
|
|
1510
|
+
proxyCache.set(cacheKey, result, allocatedAt);
|
|
1486
1511
|
if (options.redis && options.poolKey) {
|
|
1487
1512
|
const redis = options.redis;
|
|
1488
1513
|
const poolKey = options.poolKey;
|
|
@@ -1733,7 +1758,7 @@ function markSmartproxyCacheInvalidated(options: ProxyResolutionOptions = {}): s
|
|
|
1733
1758
|
const lifetimeMinutes = resolveSmartproxyLifetime(policy);
|
|
1734
1759
|
const appKey = (
|
|
1735
1760
|
options.engineCredentials === undefined
|
|
1736
|
-
?
|
|
1761
|
+
? readDiagnosticEnv(SMARTPROXY_APP_KEY_ENV)
|
|
1737
1762
|
: options.engineCredentials[SMARTPROXY_APP_KEY_ENV]
|
|
1738
1763
|
)?.trim();
|
|
1739
1764
|
if (!appKey) return undefined;
|
|
@@ -1744,7 +1769,11 @@ function markSmartproxyCacheInvalidated(options: ProxyResolutionOptions = {}): s
|
|
|
1744
1769
|
lifetimeMinutes,
|
|
1745
1770
|
options.protocol ?? VENDOR_DEFAULT_PROTOCOL.smartproxy,
|
|
1746
1771
|
);
|
|
1747
|
-
|
|
1772
|
+
const now = Date.now();
|
|
1773
|
+
for (const [key, invalidatedUntil] of invalidatedProxyKeys) {
|
|
1774
|
+
if (invalidatedUntil <= now) invalidatedProxyKeys.delete(key);
|
|
1775
|
+
}
|
|
1776
|
+
invalidatedProxyKeys.set(cacheKey, now + SMARTPROXY_INVALIDATION_SKIP_REDIS_MS);
|
|
1748
1777
|
proxyCache.delete(cacheKey);
|
|
1749
1778
|
proxyInflight.delete(cacheKey);
|
|
1750
1779
|
return cacheKey;
|
|
@@ -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
|
import type {
|
|
5
7
|
HealthJourneyDefinition,
|
|
6
8
|
ProviderDefinition,
|
|
@@ -22,6 +24,7 @@ export const DECLARATION_RULE_IDS = {
|
|
|
22
24
|
proxySmartproxyGeo: "proxy-smartproxy-country-only",
|
|
23
25
|
operationUpstreamProxy: "operation-upstream-proxy-unsupported",
|
|
24
26
|
pinnedWireFieldPathValid: "public-schema-pinned-wire-field-valid",
|
|
27
|
+
resolverClientProfileFamily: "resolver-client-profile-family",
|
|
25
28
|
} as const;
|
|
26
29
|
|
|
27
30
|
export type DeclarationRuleId =
|
|
@@ -57,11 +60,15 @@ export function validateFailClosedDeclaration(provider: ProviderDefinition): voi
|
|
|
57
60
|
validatePinnedWireFieldPaths(provider, violations);
|
|
58
61
|
validateSchemaDeclaration(provider, violations);
|
|
59
62
|
validateProxyDeclaration(provider, violations);
|
|
63
|
+
validateResolverClientProfileDeclaration(provider, violations);
|
|
60
64
|
validateOperationDeclaration(provider, violations);
|
|
61
65
|
if (violations.length > 0) throw declarationInvalidError(violations);
|
|
62
66
|
}
|
|
63
67
|
|
|
64
|
-
type ProviderDeclarationRulesInput = Pick<
|
|
68
|
+
type ProviderDeclarationRulesInput = Pick<
|
|
69
|
+
ProviderDefinition,
|
|
70
|
+
"healthJourneys" | "meta" | "proxy" | "resolver" | "stealth"
|
|
71
|
+
>;
|
|
65
72
|
type OperationDeclarationRulesInput = Pick<ProviderDefinition, "operations">;
|
|
66
73
|
|
|
67
74
|
/** Enforces fail-closed rules that only depend on the provider declaration. */
|
|
@@ -89,6 +96,7 @@ function collectProviderDeclarationViolations(
|
|
|
89
96
|
validateHealthDeclaration(provider, violations);
|
|
90
97
|
validatePinnedWireFieldPaths(provider, violations);
|
|
91
98
|
validateProxyDeclaration(provider, violations);
|
|
99
|
+
validateResolverClientProfileDeclaration(provider, violations);
|
|
92
100
|
}
|
|
93
101
|
|
|
94
102
|
function validatePinnedWireFieldPaths(
|
|
@@ -307,6 +315,44 @@ function validateProxyDeclaration(
|
|
|
307
315
|
}
|
|
308
316
|
}
|
|
309
317
|
|
|
318
|
+
/**
|
|
319
|
+
* An akamai_sbsd solve runs on the provider's own stealth session, so the declared
|
|
320
|
+
* resolver.clientProfile must name the browser family that session emulates. The
|
|
321
|
+
* stealth runtime re-checks per request (a fetch may override the browser); this
|
|
322
|
+
* rule fails the declaration instead of the first challenged request.
|
|
323
|
+
*/
|
|
324
|
+
function validateResolverClientProfileDeclaration(
|
|
325
|
+
provider: ProviderDeclarationRulesInput,
|
|
326
|
+
violations: DeclarationViolation[],
|
|
327
|
+
): void {
|
|
328
|
+
const resolver = provider.resolver;
|
|
329
|
+
if (
|
|
330
|
+
!resolver ||
|
|
331
|
+
!Array.isArray(resolver.kinds) ||
|
|
332
|
+
!resolver.kinds.some(kindRequiresClientProfile) ||
|
|
333
|
+
typeof resolver.clientProfile !== "string"
|
|
334
|
+
) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
let sessionBrowser: string;
|
|
338
|
+
try {
|
|
339
|
+
sessionBrowser = resolveStealthProfileSelection(provider.stealth).browser;
|
|
340
|
+
} catch {
|
|
341
|
+
// An unsupported stealth selection is reported by the stealth runtime itself.
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const family = resolverClientProfileFamily(resolver.clientProfile);
|
|
345
|
+
if (family === sessionBrowser) return;
|
|
346
|
+
violations.push({
|
|
347
|
+
ruleId: DECLARATION_RULE_IDS.resolverClientProfileFamily,
|
|
348
|
+
path: "resolver.clientProfile",
|
|
349
|
+
message: family
|
|
350
|
+
? `"${resolver.clientProfile}" names ${family}, but the stealth session browser is ${sessionBrowser}.`
|
|
351
|
+
: `"${resolver.clientProfile}" does not name a supported browser family (chrome, firefox, safari).`,
|
|
352
|
+
fix: `Declare stealth.browser as the family resolver.clientProfile names, or set resolver.clientProfile to the ${sessionBrowser} profile the SBSD session runs on.`,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
310
356
|
function declaredProxyVendors(policy: ProviderProxyPolicy): ProviderProxyProvider[] {
|
|
311
357
|
const vendors = [...(policy.providers ?? [])];
|
|
312
358
|
if (policy.provider !== undefined) vendors.push(policy.provider);
|
package/src/define.ts
CHANGED
|
@@ -6,8 +6,15 @@ import {
|
|
|
6
6
|
} from "./declaration-validation.js";
|
|
7
7
|
import { SDK_RUNTIME_OWNED_ERROR_CODES } from "./error-resolution.js";
|
|
8
8
|
import { ProviderError, ValidationError } from "./errors.js";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
engineOwnedResolverCredentialTarget,
|
|
11
|
+
isEngineOwnedEnvName,
|
|
12
|
+
isEngineOwnedProxyCredentialName,
|
|
13
|
+
isEngineOwnedRuntimeEnvName,
|
|
14
|
+
isEngineOwnedTelemetryEnvName,
|
|
15
|
+
} from "./engine.js";
|
|
10
16
|
import { HealthScenarioSchema } from "./health-scenario.js";
|
|
17
|
+
import { kindRequiresClientProfile } from "./runtime/resolver-config.js";
|
|
11
18
|
import {
|
|
12
19
|
NativeEgressPolicyValidationError,
|
|
13
20
|
validateNativeProviderConfig,
|
|
@@ -49,9 +56,9 @@ import type {
|
|
|
49
56
|
ProviderSecretDeclaration,
|
|
50
57
|
ProviderStreamEvent,
|
|
51
58
|
ProviderSttConfig,
|
|
59
|
+
ProviderStealthConfig,
|
|
52
60
|
SchemaLike,
|
|
53
61
|
SmsOtpMatcherDefinition,
|
|
54
|
-
StealthProfileSelection,
|
|
55
62
|
} from "./types.js";
|
|
56
63
|
import {
|
|
57
64
|
HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX,
|
|
@@ -151,6 +158,7 @@ export const VALID_PROVIDER_RESOLVER_VENDORS = exhaustiveLiteralArray<ProviderRe
|
|
|
151
158
|
"capsolver",
|
|
152
159
|
"capmonster",
|
|
153
160
|
"2captcha",
|
|
161
|
+
"hypersolutions",
|
|
154
162
|
"custom",
|
|
155
163
|
] as const);
|
|
156
164
|
export const VALID_PROVIDER_CHALLENGE_KINDS = exhaustiveLiteralArray<ProviderChallengeKind>()([
|
|
@@ -162,6 +170,7 @@ export const VALID_PROVIDER_CHALLENGE_KINDS = exhaustiveLiteralArray<ProviderCha
|
|
|
162
170
|
"aws_waf",
|
|
163
171
|
"akamai_sec_cpt",
|
|
164
172
|
"akamai_sensor",
|
|
173
|
+
"akamai_sbsd",
|
|
165
174
|
] as const);
|
|
166
175
|
const RESERVED_OPERATION_IDS = new Set(["auth", "health"]);
|
|
167
176
|
const VALID_OPERATION_RISK_CLASSES = ["read", "write", "destructive", "external-send"] as const;
|
|
@@ -584,7 +593,7 @@ export interface ProviderDeclaration {
|
|
|
584
593
|
/** Declares upstream host policy; this does not add a `ctx.allowedHosts` member. */
|
|
585
594
|
allowedHosts?: string[];
|
|
586
595
|
native?: NativeProviderConfig;
|
|
587
|
-
stealth?:
|
|
596
|
+
stealth?: ProviderStealthConfig;
|
|
588
597
|
/** Declares proxy policy; this is provider intent and does not add a `ctx.proxy` member. */
|
|
589
598
|
proxy?: ProviderProxyConfig;
|
|
590
599
|
ocr?: ProviderOcrConfig;
|
|
@@ -939,6 +948,15 @@ function validateProviderProxy(config: {
|
|
|
939
948
|
secrets?: ProviderSecretDeclaration[];
|
|
940
949
|
}): void {
|
|
941
950
|
for (const secret of config.secrets ?? []) {
|
|
951
|
+
const engineName = engineOwnedResolverCredentialTarget(secret.name);
|
|
952
|
+
if (engineName !== undefined) {
|
|
953
|
+
throw new ValidationError(
|
|
954
|
+
`Provider "${config.id}" cannot declare engine-owned resolver credential "${secret.name}"`,
|
|
955
|
+
{
|
|
956
|
+
fix: `Remove "${secret.name}" from provider secrets; configure the solver key only on the provider engine as ${engineName}.`,
|
|
957
|
+
},
|
|
958
|
+
);
|
|
959
|
+
}
|
|
942
960
|
if (isEngineOwnedTelemetryEnvName(secret.name)) {
|
|
943
961
|
throw new ValidationError(
|
|
944
962
|
`Provider "${config.id}" cannot declare engine-owned telemetry variable "${secret.name}"`,
|
|
@@ -947,6 +965,22 @@ function validateProviderProxy(config: {
|
|
|
947
965
|
},
|
|
948
966
|
);
|
|
949
967
|
}
|
|
968
|
+
if (isEngineOwnedRuntimeEnvName(secret.name)) {
|
|
969
|
+
throw new ValidationError(
|
|
970
|
+
`Provider "${config.id}" cannot declare engine-owned runtime variable "${secret.name}"`,
|
|
971
|
+
{
|
|
972
|
+
fix: `Remove "${secret.name}" from provider secrets; the provider engine reads it from its own environment.`,
|
|
973
|
+
},
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
if (isEngineOwnedEnvName(secret.name) && !isEngineOwnedProxyCredentialName(secret.name)) {
|
|
977
|
+
throw new ValidationError(
|
|
978
|
+
`Provider "${config.id}" cannot declare engine-owned control variable "${secret.name}"`,
|
|
979
|
+
{
|
|
980
|
+
fix: `Remove "${secret.name}" from provider secrets; configure it only on the provider engine.`,
|
|
981
|
+
},
|
|
982
|
+
);
|
|
983
|
+
}
|
|
950
984
|
if (!isEngineOwnedProxyCredentialName(secret.name)) continue;
|
|
951
985
|
throw new ValidationError(
|
|
952
986
|
`Provider "${config.id}" cannot declare engine-owned proxy credential "${secret.name}"`,
|
|
@@ -1163,6 +1197,14 @@ function validateProviderResolver(config: { id: string; resolver?: ProviderResol
|
|
|
1163
1197
|
},
|
|
1164
1198
|
);
|
|
1165
1199
|
}
|
|
1200
|
+
if (resolver.kinds.some(kindRequiresClientProfile) && resolver.clientProfile === undefined) {
|
|
1201
|
+
throw new ValidationError(
|
|
1202
|
+
`Provider "${config.id}" must declare resolver.clientProfile for akamai_sbsd.`,
|
|
1203
|
+
{
|
|
1204
|
+
fix: `Set resolver.clientProfile for provider "${config.id}" to the transport-owned profile used by its Akamai session.`,
|
|
1205
|
+
},
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1166
1208
|
}
|
|
1167
1209
|
|
|
1168
1210
|
function validateResolverLiteralArray<TValue extends string>(
|
package/src/engine.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
import { readDiagnosticEnv } from "./runtime/diagnostic-env.js";
|
|
1
2
|
import { ProviderError } from "./errors.js";
|
|
3
|
+
import {
|
|
4
|
+
APIFUSE__RESOLVER__2CAPTCHA__API_KEY,
|
|
5
|
+
APIFUSE__RESOLVER__CAPMONSTER__API_KEY,
|
|
6
|
+
APIFUSE__RESOLVER__CAPSOLVER__API_KEY,
|
|
7
|
+
APIFUSE__RESOLVER__HYPERSOLUTIONS__API_KEY,
|
|
8
|
+
} from "./runtime/resolver-config.js";
|
|
2
9
|
import {
|
|
3
10
|
OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
4
11
|
OTEL_EXPORTER_OTLP_HEADERS,
|
|
@@ -55,6 +62,36 @@ export function isEngineOwnedProxyCredentialName(name: string): boolean {
|
|
|
55
62
|
return ENGINE_OWNED_PROXY_CREDENTIAL_ENV_NAME_SET.has(canonicalEnvName(name));
|
|
56
63
|
}
|
|
57
64
|
|
|
65
|
+
/** Hosted resolver credentials owned by the engine and never projected to providers. */
|
|
66
|
+
export const ENGINE_OWNED_RESOLVER_CREDENTIAL_ENV_NAMES = [
|
|
67
|
+
APIFUSE__RESOLVER__2CAPTCHA__API_KEY,
|
|
68
|
+
APIFUSE__RESOLVER__CAPSOLVER__API_KEY,
|
|
69
|
+
APIFUSE__RESOLVER__CAPMONSTER__API_KEY,
|
|
70
|
+
APIFUSE__RESOLVER__HYPERSOLUTIONS__API_KEY,
|
|
71
|
+
] as const;
|
|
72
|
+
|
|
73
|
+
const ENGINE_OWNED_RESOLVER_CREDENTIAL_ENV_NAME_SET = new Set<string>(
|
|
74
|
+
ENGINE_OWNED_RESOLVER_CREDENTIAL_ENV_NAMES,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Engine variable a provider-declared secret name resolves to, or `undefined` when the
|
|
79
|
+
* name is not an engine-owned resolver credential. Provider-scoped Hyper aliases
|
|
80
|
+
* (`APIFUSE__PROVIDER__<ID>__HYPER_API_KEY`, measured on zozotown) map to the shared key.
|
|
81
|
+
*/
|
|
82
|
+
export function engineOwnedResolverCredentialTarget(name: string): string | undefined {
|
|
83
|
+
const canonical = canonicalEnvName(name);
|
|
84
|
+
if (ENGINE_OWNED_RESOLVER_CREDENTIAL_ENV_NAME_SET.has(canonical)) return canonical;
|
|
85
|
+
if (/^APIFUSE__PROVIDER__.+__HYPER_API_KEY$/u.test(canonical)) {
|
|
86
|
+
return APIFUSE__RESOLVER__HYPERSOLUTIONS__API_KEY;
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function isEngineOwnedResolverCredentialName(name: string): boolean {
|
|
92
|
+
return engineOwnedResolverCredentialTarget(name) !== undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
58
95
|
/**
|
|
59
96
|
* Trace-export configuration owned by the engine and forbidden in provider
|
|
60
97
|
* declarations. The header variables carry collector credentials; the rest are
|
|
@@ -75,9 +112,47 @@ export function isEngineOwnedTelemetryEnvName(name: string): boolean {
|
|
|
75
112
|
return ENGINE_OWNED_TELEMETRY_ENV_NAME_SET.has(canonicalEnvName(name));
|
|
76
113
|
}
|
|
77
114
|
|
|
115
|
+
/** Engine control-plane configuration is never declarable or visible to provider code. */
|
|
116
|
+
function isEngineOwnedEngineEnvName(name: string): boolean {
|
|
117
|
+
return canonicalEnvName(name).startsWith("APIFUSE__ENGINE__");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Hosted runtime settings the engine reads for OCR, STT, cache keying, choice
|
|
122
|
+
* tokens, and the CDP pool. Providers never own them: declarations are rejected
|
|
123
|
+
* and projections drop them, like the vendor credentials above.
|
|
124
|
+
*/
|
|
125
|
+
export const ENGINE_OWNED_RUNTIME_ENV_NAMES = [
|
|
126
|
+
"APIFUSE__STT__CLOUDFLARE_API_TOKEN",
|
|
127
|
+
"APIFUSE__OCR__CLOUDFLARE_API_TOKEN",
|
|
128
|
+
"APIFUSE__OCR__API_KEY",
|
|
129
|
+
"APIFUSE__CLOUDFLARE__ACCOUNT_ID",
|
|
130
|
+
"APIFUSE__CACHE__KEY_PEPPER",
|
|
131
|
+
"APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET",
|
|
132
|
+
] as const;
|
|
133
|
+
|
|
134
|
+
const ENGINE_OWNED_RUNTIME_ENV_NAME_SET = new Set<string>(ENGINE_OWNED_RUNTIME_ENV_NAMES);
|
|
135
|
+
|
|
136
|
+
/** CDP pool settings form a prefix family (`APIFUSE__CDP_POOL__URL` today); the whole prefix is engine-owned. */
|
|
137
|
+
const ENGINE_OWNED_CDP_POOL_ENV_PREFIX = "APIFUSE__CDP_POOL__";
|
|
138
|
+
|
|
139
|
+
export function isEngineOwnedRuntimeEnvName(name: string): boolean {
|
|
140
|
+
const canonical = canonicalEnvName(name);
|
|
141
|
+
return (
|
|
142
|
+
ENGINE_OWNED_RUNTIME_ENV_NAME_SET.has(canonical) ||
|
|
143
|
+
canonical.startsWith(ENGINE_OWNED_CDP_POOL_ENV_PREFIX)
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
78
147
|
/** Every environment name the engine owns: rejected in declarations and filtered from provider projections. */
|
|
79
148
|
export function isEngineOwnedEnvName(name: string): boolean {
|
|
80
|
-
return
|
|
149
|
+
return (
|
|
150
|
+
isEngineOwnedEngineEnvName(name) ||
|
|
151
|
+
isEngineOwnedProxyCredentialName(name) ||
|
|
152
|
+
isEngineOwnedResolverCredentialName(name) ||
|
|
153
|
+
isEngineOwnedRuntimeEnvName(name) ||
|
|
154
|
+
isEngineOwnedTelemetryEnvName(name)
|
|
155
|
+
);
|
|
81
156
|
}
|
|
82
157
|
|
|
83
158
|
/** Capture credentials in the engine host before constructing provider bindings. */
|
|
@@ -86,7 +161,7 @@ export function readEngineProxyCredentials(
|
|
|
86
161
|
): Readonly<Record<string, string>> {
|
|
87
162
|
return Object.fromEntries(
|
|
88
163
|
ENGINE_OWNED_PROXY_CREDENTIAL_ENV_NAMES.flatMap((name) => {
|
|
89
|
-
const value = environment
|
|
164
|
+
const value = readDiagnosticEnv(name, environment)?.trim();
|
|
90
165
|
return value ? [[name, value] as const] : [];
|
|
91
166
|
}),
|
|
92
167
|
);
|
|
@@ -100,7 +175,7 @@ export function createProviderEnvironment(
|
|
|
100
175
|
return Object.fromEntries(
|
|
101
176
|
declaredNames.flatMap((name) => {
|
|
102
177
|
if (isEngineOwnedEnvName(name)) return [];
|
|
103
|
-
const value = environment
|
|
178
|
+
const value = readDiagnosticEnv(name, environment);
|
|
104
179
|
return value === undefined ? [] : [[name, value] as const];
|
|
105
180
|
}),
|
|
106
181
|
);
|
package/src/error-resolution.ts
CHANGED
|
@@ -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 {
|
|
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,
|
|
@@ -249,7 +275,10 @@ export {
|
|
|
249
275
|
type Span,
|
|
250
276
|
type TraceContext,
|
|
251
277
|
} from "./runtime/trace.js";
|
|
278
|
+
export type { DiagnosticRedactor } from "./runtime/diagnostic-redactor.js";
|
|
252
279
|
export {
|
|
280
|
+
APIFUSE_CONTENT_PROVENANCE_META_KEY,
|
|
281
|
+
APIFUSE_CONTENT_TRUST_META_KEY,
|
|
253
282
|
APIFUSE_DESCRIPTION_KEY_META_KEY,
|
|
254
283
|
APIFUSE_REDACTION_MARKER,
|
|
255
284
|
APIFUSE_SENSITIVE_KIND_META_KEY,
|
|
@@ -259,11 +288,13 @@ export {
|
|
|
259
288
|
field,
|
|
260
289
|
fields,
|
|
261
290
|
isSensitiveSchema,
|
|
291
|
+
publicField,
|
|
262
292
|
redactPayload,
|
|
263
293
|
type SensitiveFieldKind,
|
|
264
294
|
type SensitiveFieldOptions,
|
|
265
295
|
type SensitivePath,
|
|
266
296
|
sensitive,
|
|
297
|
+
untrustedContent,
|
|
267
298
|
z,
|
|
268
299
|
} from "./schema.js";
|
|
269
300
|
export {
|
|
@@ -432,6 +463,7 @@ export type {
|
|
|
432
463
|
ProviderRuntimeTarget,
|
|
433
464
|
ProviderRuntimeState,
|
|
434
465
|
ProviderSecretDeclaration,
|
|
466
|
+
ProviderStealthConfig,
|
|
435
467
|
ProviderStateDurationString,
|
|
436
468
|
ProviderStateNamespace,
|
|
437
469
|
ProviderStreamEvent,
|
|
@@ -455,6 +487,7 @@ export type {
|
|
|
455
487
|
StateWriteOptions,
|
|
456
488
|
StealthClient,
|
|
457
489
|
StealthBrowser,
|
|
490
|
+
StealthChallengeClassification,
|
|
458
491
|
StealthCookieStore,
|
|
459
492
|
StealthCookieStoreV1,
|
|
460
493
|
StealthFetchOptions,
|