@apifuse/provider-sdk 2.2.0-beta.23 → 2.2.0-beta.25
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/CHANGELOG.md +8 -0
- package/bin/apifuse-dev.ts +2 -0
- package/bin/apifuse-pack-types.ts +234 -38
- package/bin/apifuse-perf.ts +15 -12
- package/bin/apifuse-record.ts +2 -0
- package/bin/apifuse-submit-check.ts +15 -2
- package/dist/config/loader.d.ts +8 -19
- package/dist/config/loader.js +28 -86
- package/dist/define.d.ts +4 -1
- package/dist/define.js +64 -6
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -1
- package/dist/provider.d.ts +1 -1
- package/dist/runtime/auth-flow.js +2 -0
- package/dist/runtime/browser.js +50 -0
- package/dist/runtime/cache.d.ts +1 -0
- package/dist/runtime/cache.js +169 -15
- package/dist/runtime/http.js +0 -1
- package/dist/runtime/instrumentation.js +26 -1
- package/dist/runtime/resolver-vendors/bindings.d.ts +8 -0
- package/dist/runtime/resolver-vendors/bindings.js +15 -0
- package/dist/runtime/resolver-vendors/browser.d.ts +24 -0
- package/dist/runtime/resolver-vendors/browser.js +287 -0
- package/dist/runtime/resolver-vendors/types.d.ts +42 -0
- package/dist/runtime/resolver-vendors/types.js +57 -0
- package/dist/runtime/resolver.d.ts +39 -0
- package/dist/runtime/resolver.js +414 -0
- package/dist/runtime/state.d.ts +3 -0
- package/dist/runtime/state.js +245 -141
- package/dist/runtime/stealth.js +3 -6
- package/dist/server/serve.d.ts +4 -1
- package/dist/server/serve.js +35 -8
- package/dist/testing/run.js +7 -0
- package/dist/types.d.ts +115 -7
- package/package.json +1 -1
- package/src/config/loader.ts +35 -111
- package/src/define.ts +105 -8
- package/src/index.ts +21 -1
- package/src/provider.ts +1 -0
- package/src/runtime/auth-flow.ts +2 -0
- package/src/runtime/browser.ts +69 -0
- package/src/runtime/cache.ts +189 -14
- package/src/runtime/http.ts +0 -1
- package/src/runtime/instrumentation.ts +36 -2
- package/src/runtime/resolver-vendors/bindings.ts +31 -0
- package/src/runtime/resolver-vendors/browser.ts +420 -0
- package/src/runtime/resolver-vendors/types.ts +113 -0
- package/src/runtime/resolver.ts +668 -0
- package/src/runtime/state.ts +323 -166
- package/src/runtime/stealth.ts +3 -6
- package/src/server/serve.ts +73 -5
- package/src/testing/run.ts +8 -0
- package/src/types.ts +133 -7
package/src/define.ts
CHANGED
|
@@ -20,21 +20,24 @@ import type {
|
|
|
20
20
|
HealthJourneySchedule,
|
|
21
21
|
HealthScheduleRandomization,
|
|
22
22
|
InferSchemaOutput,
|
|
23
|
+
NativeProviderConfig,
|
|
23
24
|
OperationDefinition,
|
|
24
25
|
OperationHandlerResult,
|
|
25
26
|
OperationHttpStreamTransport,
|
|
26
27
|
OperationSseTransport,
|
|
27
28
|
OperationTransport,
|
|
28
29
|
OperationWebSocketTransport,
|
|
29
|
-
NativeProviderConfig,
|
|
30
|
-
ProviderOcrConfig,
|
|
31
30
|
ProviderAccessConfig,
|
|
31
|
+
ProviderChallengeKind,
|
|
32
32
|
ProviderDefinition,
|
|
33
|
+
ProviderOcrConfig,
|
|
33
34
|
ProviderDeploymentOverrides,
|
|
34
35
|
ProviderHealthMonitorConfig,
|
|
35
36
|
ProviderProxyConfig,
|
|
36
37
|
ProviderProxyProvider,
|
|
37
38
|
ProviderPublicProfile,
|
|
39
|
+
ProviderResolverConfig,
|
|
40
|
+
ProviderResolverVendor,
|
|
38
41
|
ProviderReviewed,
|
|
39
42
|
ProviderSecretDeclaration,
|
|
40
43
|
ProviderStreamEvent,
|
|
@@ -126,6 +129,30 @@ const VALID_PROVIDER_PROXY_AFFINITIES = [
|
|
|
126
129
|
] as const;
|
|
127
130
|
const VALID_PROVIDER_OCR_MODES = ["optional", "required"] as const;
|
|
128
131
|
const VALID_PROVIDER_STT_MODES = ["optional", "required"] as const;
|
|
132
|
+
function exhaustiveLiteralArray<TUnion extends string>() {
|
|
133
|
+
return <const TValues extends readonly TUnion[]>(
|
|
134
|
+
values: TValues,
|
|
135
|
+
..._missing: Exclude<TUnion, TValues[number]> extends never
|
|
136
|
+
? []
|
|
137
|
+
: ["Missing runtime values", Exclude<TUnion, TValues[number]>]
|
|
138
|
+
): TValues => values;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export const VALID_PROVIDER_RESOLVER_VENDORS = exhaustiveLiteralArray<ProviderResolverVendor>()([
|
|
142
|
+
"browser",
|
|
143
|
+
"capsolver",
|
|
144
|
+
"capmonster",
|
|
145
|
+
"2captcha",
|
|
146
|
+
"custom",
|
|
147
|
+
] as const);
|
|
148
|
+
export const VALID_PROVIDER_CHALLENGE_KINDS = exhaustiveLiteralArray<ProviderChallengeKind>()([
|
|
149
|
+
"turnstile",
|
|
150
|
+
"recaptcha_v2",
|
|
151
|
+
"recaptcha_v3",
|
|
152
|
+
"hcaptcha",
|
|
153
|
+
"cloudflare_interstitial",
|
|
154
|
+
"aws_waf",
|
|
155
|
+
] as const);
|
|
129
156
|
const SMARTPROXY_APP_KEY_SECRET = "APIFUSE__PROXY__SMARTPROXY_APP_KEY";
|
|
130
157
|
const NODEMAVEN_USERNAME_SECRET = "APIFUSE__PROXY__NODEMAVEN_USERNAME";
|
|
131
158
|
const NODEMAVEN_PASSWORD_SECRET = "APIFUSE__PROXY__NODEMAVEN_PASSWORD";
|
|
@@ -134,9 +161,8 @@ const NODEMAVEN_PASSWORD_SECRET = "APIFUSE__PROXY__NODEMAVEN_PASSWORD";
|
|
|
134
161
|
// credential fails at build/validation time rather than during a live outage: a
|
|
135
162
|
// declared-but-uncredentialed fallback leg is a silently dead SPOF, which is
|
|
136
163
|
// exactly the failure class the multi-vendor chain exists to remove. Vendors
|
|
137
|
-
// absent from this map (
|
|
138
|
-
//
|
|
139
|
-
// no declaration requirement.
|
|
164
|
+
// absent from this map (the deprecated `custom`/`decodo` values have no managed
|
|
165
|
+
// adapter) impose no declaration requirement.
|
|
140
166
|
const VENDOR_REQUIRED_SECRETS: Partial<Record<ProviderProxyProvider, readonly string[]>> = {
|
|
141
167
|
smartproxy: [SMARTPROXY_APP_KEY_SECRET],
|
|
142
168
|
nodemaven: [NODEMAVEN_USERNAME_SECRET, NODEMAVEN_PASSWORD_SECRET],
|
|
@@ -252,6 +278,7 @@ export interface ProviderConfig<TOperations extends Record<string, ProviderOpera
|
|
|
252
278
|
proxy?: ProviderProxyConfig;
|
|
253
279
|
ocr?: ProviderOcrConfig;
|
|
254
280
|
stt?: ProviderSttConfig;
|
|
281
|
+
resolver?: ProviderResolverConfig;
|
|
255
282
|
browser?: { engine: BrowserEngine };
|
|
256
283
|
auth?: AuthConfig;
|
|
257
284
|
reviewed?: ProviderReviewed;
|
|
@@ -681,9 +708,21 @@ function validateProviderProxy(config: {
|
|
|
681
708
|
const deprecatedVendors = vendorChain.filter(
|
|
682
709
|
(vendor) => vendor === "decodo" || vendor === "custom",
|
|
683
710
|
);
|
|
711
|
+
if (
|
|
712
|
+
proxy.mode === "required" &&
|
|
713
|
+
vendorChain.length > 0 &&
|
|
714
|
+
deprecatedVendors.length === vendorChain.length
|
|
715
|
+
) {
|
|
716
|
+
throw new ValidationError(
|
|
717
|
+
`Provider "${config.id}" requires proxy egress but declares only deprecated proxy vendor(s): ${deprecatedVendors.join(", ")}.`,
|
|
718
|
+
{
|
|
719
|
+
fix: `Use proxy.provider or proxy.providers with "smartproxy" and/or "nodemaven".`,
|
|
720
|
+
},
|
|
721
|
+
);
|
|
722
|
+
}
|
|
684
723
|
if (deprecatedVendors.length > 0) {
|
|
685
724
|
console.warn(
|
|
686
|
-
`[provider-sdk] Provider "${config.id}" uses deprecated proxy vendor(s): ${deprecatedVendors.join(", ")}. Use "smartproxy"/"nodemaven"
|
|
725
|
+
`[provider-sdk] Provider "${config.id}" uses deprecated proxy vendor(s): ${deprecatedVendors.join(", ")}. Use "smartproxy"/"nodemaven".`,
|
|
687
726
|
);
|
|
688
727
|
}
|
|
689
728
|
}
|
|
@@ -712,6 +751,53 @@ function validateProviderOcr(config: { id: string; ocr?: ProviderOcrConfig }): v
|
|
|
712
751
|
assertLiteralField(ocr.mode, "ocr.mode", VALID_PROVIDER_OCR_MODES, config.id);
|
|
713
752
|
}
|
|
714
753
|
|
|
754
|
+
function validateProviderResolver(config: { id: string; resolver?: ProviderResolverConfig }): void {
|
|
755
|
+
const resolver = config.resolver;
|
|
756
|
+
if (resolver === undefined) return;
|
|
757
|
+
if (!resolver || typeof resolver !== "object" || Array.isArray(resolver)) {
|
|
758
|
+
throw new ValidationError(`Provider "${config.id}" has invalid resolver: must be an object.`, {
|
|
759
|
+
fix: `Set resolver for provider "${config.id}" to { vendors: ["2captcha"], kinds: ["turnstile"] }.`,
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
rejectUnknownFields(resolver, new Set(["vendors", "kinds"]), "resolver", config.id);
|
|
763
|
+
validateResolverLiteralArray(
|
|
764
|
+
resolver.vendors,
|
|
765
|
+
"resolver.vendors",
|
|
766
|
+
VALID_PROVIDER_RESOLVER_VENDORS,
|
|
767
|
+
config.id,
|
|
768
|
+
);
|
|
769
|
+
validateResolverLiteralArray(
|
|
770
|
+
resolver.kinds,
|
|
771
|
+
"resolver.kinds",
|
|
772
|
+
VALID_PROVIDER_CHALLENGE_KINDS,
|
|
773
|
+
config.id,
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function validateResolverLiteralArray<TValue extends string>(
|
|
778
|
+
value: readonly TValue[],
|
|
779
|
+
field: string,
|
|
780
|
+
validValues: readonly TValue[],
|
|
781
|
+
providerId: string,
|
|
782
|
+
): void {
|
|
783
|
+
if (!Array.isArray(value)) {
|
|
784
|
+
throw new ValidationError(`Provider "${providerId}" has invalid ${field}: must be an array.`, {
|
|
785
|
+
fix: `Set ${field} for provider "${providerId}" to an array containing only: ${validValues.join(", ")}.`,
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
for (const [index, item] of value.entries()) {
|
|
789
|
+
if (typeof item === "string" && validValues.some((validValue) => validValue === item)) {
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
throw new ValidationError(
|
|
793
|
+
`Provider "${providerId}" has invalid ${field}[${index}]: ${JSON.stringify(item)}. Expected one of: ${validValues.join(", ")}`,
|
|
794
|
+
{
|
|
795
|
+
fix: `Set ${field}[${index}] for provider "${providerId}" to one of ${validValues.map((validValue) => `"${validValue}"`).join(", ")}.`,
|
|
796
|
+
},
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
715
801
|
function validateOperationIds(
|
|
716
802
|
providerId: string,
|
|
717
803
|
operations: Record<string, ProviderOperation>,
|
|
@@ -1309,7 +1395,12 @@ function suggestField(unknown: string, candidates: ReadonlySet<string>): string
|
|
|
1309
1395
|
return best;
|
|
1310
1396
|
}
|
|
1311
1397
|
|
|
1312
|
-
function rejectUnknownFields(
|
|
1398
|
+
function rejectUnknownFields(
|
|
1399
|
+
value: object,
|
|
1400
|
+
allowed: ReadonlySet<string>,
|
|
1401
|
+
fieldPath: string,
|
|
1402
|
+
providerId?: string,
|
|
1403
|
+
): void {
|
|
1313
1404
|
for (const key of Object.keys(value)) {
|
|
1314
1405
|
if (allowed.has(key)) continue;
|
|
1315
1406
|
const hint = suggestField(key, allowed);
|
|
@@ -1317,7 +1408,11 @@ function rejectUnknownFields(value: object, allowed: ReadonlySet<string>, fieldP
|
|
|
1317
1408
|
hint
|
|
1318
1409
|
? `Unknown field "${key}" on ${fieldPath}. Did you mean "${hint}"?`
|
|
1319
1410
|
: `Unknown field "${key}" on ${fieldPath}.`,
|
|
1320
|
-
{
|
|
1411
|
+
{
|
|
1412
|
+
fix: providerId
|
|
1413
|
+
? `Remove ${fieldPath}.${key} from provider "${providerId}" or rename it.`
|
|
1414
|
+
: `Remove ${fieldPath}.${key} or rename it.`,
|
|
1415
|
+
},
|
|
1321
1416
|
);
|
|
1322
1417
|
}
|
|
1323
1418
|
}
|
|
@@ -2403,6 +2498,7 @@ export function defineProvider<
|
|
|
2403
2498
|
validateProviderProxy(config);
|
|
2404
2499
|
validateProviderOcr(config);
|
|
2405
2500
|
validateProviderStt(config);
|
|
2501
|
+
validateProviderResolver(config);
|
|
2406
2502
|
if (config.runtime === "browser" && !config.browser)
|
|
2407
2503
|
throw new ProviderError(
|
|
2408
2504
|
`Provider "${config.id}" must define browser.engine when runtime is "browser"`,
|
|
@@ -2428,6 +2524,7 @@ export function defineProvider<
|
|
|
2428
2524
|
proxy: config.proxy,
|
|
2429
2525
|
ocr: config.ocr,
|
|
2430
2526
|
stt: config.stt,
|
|
2527
|
+
resolver: config.resolver,
|
|
2431
2528
|
browser: config.browser,
|
|
2432
2529
|
auth: config.auth,
|
|
2433
2530
|
reviewed: config.reviewed,
|
package/src/index.ts
CHANGED
|
@@ -6,7 +6,6 @@ export * from "./choice-token.js";
|
|
|
6
6
|
export type {
|
|
7
7
|
ApiFuseConfig,
|
|
8
8
|
BrowserConfig,
|
|
9
|
-
ProxyConfig,
|
|
10
9
|
ProxyProtocol,
|
|
11
10
|
ProxyResolutionOptions,
|
|
12
11
|
ProxyResolutionSource,
|
|
@@ -53,6 +52,7 @@ export { createFlowContext, createScratchpad } from "./runtime/auth-flow.js";
|
|
|
53
52
|
export type { BrowserClientOptions } from "./runtime/browser.js";
|
|
54
53
|
export { BrowserClient, createBrowserClient } from "./runtime/browser.js";
|
|
55
54
|
export {
|
|
55
|
+
APIFUSE__CACHE__KEY_PEPPER_ENV,
|
|
56
56
|
createBypassProviderCache,
|
|
57
57
|
createProviderCache,
|
|
58
58
|
type ProviderCacheOptions,
|
|
@@ -101,6 +101,18 @@ export {
|
|
|
101
101
|
} from "./runtime/instrumentation.js";
|
|
102
102
|
export { type PrevalidateResult, prevalidate } from "./runtime/prevalidate.js";
|
|
103
103
|
export { getProviderBaseUrl } from "./runtime/provider.js";
|
|
104
|
+
export {
|
|
105
|
+
APIFUSE__CDP_POOL__URL,
|
|
106
|
+
APIFUSE__RESOLVER__2CAPTCHA__API_KEY,
|
|
107
|
+
APIFUSE__RESOLVER__CAPMONSTER__API_KEY,
|
|
108
|
+
APIFUSE__RESOLVER__CAPSOLVER__API_KEY,
|
|
109
|
+
APIFUSE__RESOLVER__TIMEOUT_MS,
|
|
110
|
+
createResolverClientFromEnv,
|
|
111
|
+
createUnsupportedResolverClient,
|
|
112
|
+
DEFAULT_RESOLVER_TIMEOUT_MS,
|
|
113
|
+
invalidateResolverSolution,
|
|
114
|
+
type ResolverRuntimeOptions,
|
|
115
|
+
} from "./runtime/resolver.js";
|
|
104
116
|
export {
|
|
105
117
|
assertRequiredSecretsPresent,
|
|
106
118
|
listMissingRequiredSecrets,
|
|
@@ -177,6 +189,7 @@ export type {
|
|
|
177
189
|
AuthMode,
|
|
178
190
|
AuthTurn,
|
|
179
191
|
Bcp47Locale,
|
|
192
|
+
BrowserCookie,
|
|
180
193
|
BrowserEngine,
|
|
181
194
|
BrowserOptions,
|
|
182
195
|
BrowserResourceBody,
|
|
@@ -185,6 +198,7 @@ export type {
|
|
|
185
198
|
BrowserResourcePolicy,
|
|
186
199
|
BrowserResourceRequest,
|
|
187
200
|
BrowserResourceRoute,
|
|
201
|
+
ChallengeSolution,
|
|
188
202
|
ConnectionMode,
|
|
189
203
|
ContextDeclaration,
|
|
190
204
|
CookieJar,
|
|
@@ -285,6 +299,8 @@ export type {
|
|
|
285
299
|
ProviderChoiceContext,
|
|
286
300
|
ProviderChoiceIssueOptions,
|
|
287
301
|
ProviderChoiceParseOptions,
|
|
302
|
+
ProviderChallenge,
|
|
303
|
+
ProviderChallengeKind,
|
|
288
304
|
ProviderContext,
|
|
289
305
|
ProviderDefinition,
|
|
290
306
|
ProviderDeploymentOverrides,
|
|
@@ -309,6 +325,8 @@ export type {
|
|
|
309
325
|
ProviderPublicProfile,
|
|
310
326
|
ProviderReviewed,
|
|
311
327
|
ProviderResolvedFile,
|
|
328
|
+
ProviderResolverConfig,
|
|
329
|
+
ProviderResolverVendor,
|
|
312
330
|
ProviderRuntimeState,
|
|
313
331
|
ProviderSecretDeclaration,
|
|
314
332
|
ProviderStateDurationString,
|
|
@@ -319,6 +337,7 @@ export type {
|
|
|
319
337
|
ProviderSupportLevel,
|
|
320
338
|
RequestOptions,
|
|
321
339
|
RedirectRunReason,
|
|
340
|
+
ResolverContext,
|
|
322
341
|
Rfc3339Instant,
|
|
323
342
|
SchemaLike,
|
|
324
343
|
SmsOrigin,
|
|
@@ -328,6 +347,7 @@ export type {
|
|
|
328
347
|
StandardSchemaV1,
|
|
329
348
|
StateCasResult,
|
|
330
349
|
StateNamespaceOptions,
|
|
350
|
+
StateNamespaceScope,
|
|
331
351
|
StateValue,
|
|
332
352
|
StateWriteOptions,
|
|
333
353
|
StealthClient,
|
package/src/provider.ts
CHANGED
package/src/runtime/auth-flow.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
SttContext,
|
|
11
11
|
} from "../types.js";
|
|
12
12
|
import { createUnsupportedOcrClient } from "./ocr.js";
|
|
13
|
+
import { createUnsupportedResolverClient } from "./resolver.js";
|
|
13
14
|
import { createUnsupportedSttClient } from "./stt.js";
|
|
14
15
|
|
|
15
16
|
function normalizeAllowedKeys(allowedKeys: string[]): Set<string> {
|
|
@@ -75,6 +76,7 @@ export function createFlowContext(options: {
|
|
|
75
76
|
context: createScratchpad(options.allowedKeys, options.initialContext),
|
|
76
77
|
ocr: options.ocr ?? createUnsupportedOcrClient(),
|
|
77
78
|
stt: options.stt ?? createUnsupportedSttClient(),
|
|
79
|
+
resolver: createUnsupportedResolverClient("Resolver is not available in auth flow context"),
|
|
78
80
|
auth: createAuthFlowHelpers(),
|
|
79
81
|
};
|
|
80
82
|
}
|
package/src/runtime/browser.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
BrowserChallengeRequest,
|
|
7
7
|
BrowserChallengeResult,
|
|
8
8
|
BrowserClient as BrowserClientContract,
|
|
9
|
+
BrowserCookie,
|
|
9
10
|
BrowserEngine,
|
|
10
11
|
BrowserFrame,
|
|
11
12
|
BrowserLocator,
|
|
@@ -545,6 +546,10 @@ class PlaywrightBrowserPage implements BrowserPageContract {
|
|
|
545
546
|
await this.page.close();
|
|
546
547
|
}
|
|
547
548
|
|
|
549
|
+
async cookies(): Promise<readonly BrowserCookie[]> {
|
|
550
|
+
return (await this.page.context().cookies()).map(toBrowserCookie);
|
|
551
|
+
}
|
|
552
|
+
|
|
548
553
|
async withResourcePolicy<T>(policy: BrowserResourcePolicy, run: () => Promise<T>): Promise<T> {
|
|
549
554
|
const allowedMethods = new Set(policy.allowedMethods ?? DEFAULT_RESOURCE_METHODS);
|
|
550
555
|
const handler = async (route: Route): Promise<void> => {
|
|
@@ -845,6 +850,64 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
845
850
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
846
851
|
}
|
|
847
852
|
|
|
853
|
+
function isBrowserCookieSameSite(value: unknown): value is BrowserCookie["sameSite"] {
|
|
854
|
+
return value === "Strict" || value === "Lax" || value === "None";
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function toBrowserCookie(cookie: {
|
|
858
|
+
readonly name: string;
|
|
859
|
+
readonly value: string;
|
|
860
|
+
readonly domain: string;
|
|
861
|
+
readonly path: string;
|
|
862
|
+
readonly expires?: number;
|
|
863
|
+
readonly httpOnly: boolean;
|
|
864
|
+
readonly secure: boolean;
|
|
865
|
+
readonly sameSite?: unknown;
|
|
866
|
+
}): BrowserCookie {
|
|
867
|
+
return {
|
|
868
|
+
name: cookie.name,
|
|
869
|
+
value: cookie.value,
|
|
870
|
+
domain: cookie.domain,
|
|
871
|
+
path: cookie.path,
|
|
872
|
+
...(cookie.expires !== undefined && cookie.expires > 0 ? { expires: cookie.expires } : {}),
|
|
873
|
+
httpOnly: cookie.httpOnly,
|
|
874
|
+
secure: cookie.secure,
|
|
875
|
+
...(isBrowserCookieSameSite(cookie.sameSite) ? { sameSite: cookie.sameSite } : {}),
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function parseCdpCookies(value: unknown): readonly BrowserCookie[] {
|
|
880
|
+
if (!Array.isArray(value)) {
|
|
881
|
+
throw new Error("CDP Network.getCookies returned an invalid cookie list");
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
return value.map((cookie) => {
|
|
885
|
+
if (
|
|
886
|
+
!isRecord(cookie) ||
|
|
887
|
+
typeof cookie.name !== "string" ||
|
|
888
|
+
typeof cookie.value !== "string" ||
|
|
889
|
+
typeof cookie.domain !== "string" ||
|
|
890
|
+
typeof cookie.path !== "string" ||
|
|
891
|
+
typeof cookie.expires !== "number" ||
|
|
892
|
+
typeof cookie.httpOnly !== "boolean" ||
|
|
893
|
+
typeof cookie.secure !== "boolean"
|
|
894
|
+
) {
|
|
895
|
+
throw new Error("CDP Network.getCookies returned an invalid cookie");
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
return toBrowserCookie({
|
|
899
|
+
name: cookie.name,
|
|
900
|
+
value: cookie.value,
|
|
901
|
+
domain: cookie.domain,
|
|
902
|
+
path: cookie.path,
|
|
903
|
+
expires: cookie.expires,
|
|
904
|
+
httpOnly: cookie.httpOnly,
|
|
905
|
+
secure: cookie.secure,
|
|
906
|
+
sameSite: cookie.sameSite,
|
|
907
|
+
});
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
|
|
848
911
|
function parsePoolAcquireResponse(value: unknown): PoolAcquireResponse {
|
|
849
912
|
if (
|
|
850
913
|
!isRecord(value) ||
|
|
@@ -1200,6 +1263,12 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
1200
1263
|
return Buffer.from(String(result.data ?? ""), "base64");
|
|
1201
1264
|
}
|
|
1202
1265
|
|
|
1266
|
+
async cookies(): Promise<readonly BrowserCookie[]> {
|
|
1267
|
+
await this.initialize();
|
|
1268
|
+
const result = await this.pageClient.send("Network.getCookies");
|
|
1269
|
+
return parseCdpCookies(result.cookies);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1203
1272
|
async close(): Promise<void> {
|
|
1204
1273
|
if (this.closed) {
|
|
1205
1274
|
return;
|
package/src/runtime/cache.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
1
|
+
import { createHash, createHmac } from "node:crypto";
|
|
2
2
|
|
|
3
3
|
import { providerCacheRedisUrlFromEnv } from "../config/loader.js";
|
|
4
|
+
import { ProviderError } from "../errors.js";
|
|
4
5
|
import type {
|
|
5
6
|
ProviderCache,
|
|
6
7
|
ProviderCacheGetOrSetOptions,
|
|
@@ -43,9 +44,12 @@ export type ProviderCacheOptions = {
|
|
|
43
44
|
now?: () => number;
|
|
44
45
|
};
|
|
45
46
|
|
|
47
|
+
export const APIFUSE__CACHE__KEY_PEPPER_ENV = "APIFUSE__CACHE__KEY_PEPPER";
|
|
48
|
+
|
|
46
49
|
const DEFAULT_PREFIX = "apifuse:provider-cache:v1";
|
|
47
50
|
const DEFAULT_MEMORY_MAX_ENTRIES = 1_000;
|
|
48
51
|
const DEFAULT_REDIS_TIMEOUT_MS = 150;
|
|
52
|
+
const SECRET_SCOPED_KEY_MARKER = "[secret-scoped";
|
|
49
53
|
const SECRET_FIELD_NAMES = new Set([
|
|
50
54
|
"authorization",
|
|
51
55
|
"cookie",
|
|
@@ -61,6 +65,7 @@ const SECRET_FIELD_NAMES = new Set([
|
|
|
61
65
|
]);
|
|
62
66
|
|
|
63
67
|
const sharedBackends = new Map<string, SharedCacheBackend>();
|
|
68
|
+
let warnedAboutUnpepperedSecretKeys = false;
|
|
64
69
|
|
|
65
70
|
function backendKey(redisUrl: string | undefined): string {
|
|
66
71
|
return redisUrl ?? "memory";
|
|
@@ -107,19 +112,178 @@ function shouldRedactField(name: string, extra: Set<string>): boolean {
|
|
|
107
112
|
);
|
|
108
113
|
}
|
|
109
114
|
|
|
110
|
-
|
|
115
|
+
type NormalizedKeyPart = {
|
|
116
|
+
value: unknown;
|
|
117
|
+
secretScoped: boolean;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
function unsupportedSecretValue(path: string, reason: string): never {
|
|
121
|
+
throw new ProviderError(`Secret cache-key values must be JSON-safe; ${reason} at ${path}.`, {
|
|
122
|
+
code: "CACHE_KEY_SECRET_VALUE_UNSUPPORTED",
|
|
123
|
+
fix: "Convert the secret cache-key selector to JSON-safe primitives, arrays, or plain objects.",
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function assertJsonSafeSecretValue(
|
|
128
|
+
value: unknown,
|
|
129
|
+
reportedPath: string,
|
|
130
|
+
ancestors = new Set<object>(),
|
|
131
|
+
): void {
|
|
132
|
+
if (value === undefined) return;
|
|
133
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return;
|
|
134
|
+
if (typeof value === "number") {
|
|
135
|
+
if (!Number.isFinite(value))
|
|
136
|
+
unsupportedSecretValue(reportedPath, "non-finite numbers are unsupported");
|
|
137
|
+
if (Object.is(value, -0))
|
|
138
|
+
unsupportedSecretValue(reportedPath, "negative zero is unsupported");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (typeof value !== "object") {
|
|
142
|
+
unsupportedSecretValue(reportedPath, `${typeof value} values are unsupported`);
|
|
143
|
+
}
|
|
144
|
+
if (ancestors.has(value))
|
|
145
|
+
unsupportedSecretValue(reportedPath, "cyclic values are unsupported");
|
|
146
|
+
|
|
147
|
+
ancestors.add(value);
|
|
148
|
+
try {
|
|
149
|
+
if (Array.isArray(value)) {
|
|
150
|
+
if (Object.getOwnPropertySymbols(value).length > 0) {
|
|
151
|
+
unsupportedSecretValue(reportedPath, "symbol-keyed array properties are unsupported");
|
|
152
|
+
}
|
|
153
|
+
const expectedNames = new Set(["length"]);
|
|
154
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
155
|
+
const key = String(index);
|
|
156
|
+
expectedNames.add(key);
|
|
157
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
158
|
+
if (!descriptor)
|
|
159
|
+
unsupportedSecretValue(reportedPath, "sparse arrays are unsupported");
|
|
160
|
+
if (!descriptor.enumerable || !("value" in descriptor)) {
|
|
161
|
+
unsupportedSecretValue(reportedPath, "array accessors are unsupported");
|
|
162
|
+
}
|
|
163
|
+
assertJsonSafeSecretValue(descriptor.value, reportedPath, ancestors);
|
|
164
|
+
}
|
|
165
|
+
if (Object.getOwnPropertyNames(value).some((name) => !expectedNames.has(name))) {
|
|
166
|
+
unsupportedSecretValue(reportedPath, "custom array properties are unsupported");
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const prototype = Object.getPrototypeOf(value);
|
|
172
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
173
|
+
unsupportedSecretValue(reportedPath, "non-plain objects are unsupported");
|
|
174
|
+
}
|
|
175
|
+
if (Object.getOwnPropertySymbols(value).length > 0) {
|
|
176
|
+
unsupportedSecretValue(reportedPath, "symbol-keyed properties are unsupported");
|
|
177
|
+
}
|
|
178
|
+
for (const key of Object.getOwnPropertyNames(value)) {
|
|
179
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
180
|
+
if (!descriptor?.enumerable || !("value" in descriptor)) {
|
|
181
|
+
unsupportedSecretValue(
|
|
182
|
+
reportedPath,
|
|
183
|
+
"non-enumerable properties and accessors are unsupported",
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
assertJsonSafeSecretValue(descriptor.value, reportedPath, ancestors);
|
|
187
|
+
}
|
|
188
|
+
} finally {
|
|
189
|
+
ancestors.delete(value);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function containsUndefined(value: unknown): boolean {
|
|
194
|
+
if (value === undefined) return true;
|
|
195
|
+
if (Array.isArray(value)) return value.some(containsUndefined);
|
|
196
|
+
if (isRecord(value)) return Object.values(value).some(containsUndefined);
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function tagSecretValue(value: unknown): unknown {
|
|
201
|
+
if (value === undefined) return ["undefined"];
|
|
202
|
+
if (value === null) return ["null"];
|
|
203
|
+
if (typeof value === "string") return ["string", value];
|
|
204
|
+
if (typeof value === "number") return ["number", value];
|
|
205
|
+
if (typeof value === "boolean") return ["boolean", value];
|
|
206
|
+
if (Array.isArray(value)) return ["array", value.map(tagSecretValue)];
|
|
207
|
+
return [
|
|
208
|
+
"object",
|
|
209
|
+
Object.entries(value as Record<string, unknown>).map(([key, entry]) => [
|
|
210
|
+
key,
|
|
211
|
+
tagSecretValue(entry),
|
|
212
|
+
]),
|
|
213
|
+
];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function serializeSecretValue(value: unknown): string {
|
|
217
|
+
if (!containsUndefined(value)) return JSON.stringify([value]);
|
|
218
|
+
return `undefined-v1:${JSON.stringify(tagSecretValue(value))}`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function warnAboutUnpepperedSecretKey(): void {
|
|
222
|
+
if (warnedAboutUnpepperedSecretKeys) return;
|
|
223
|
+
warnedAboutUnpepperedSecretKeys = true;
|
|
224
|
+
console.warn(
|
|
225
|
+
JSON.stringify({
|
|
226
|
+
level: "warn",
|
|
227
|
+
event: "provider_cache_secret_key_unpeppered",
|
|
228
|
+
message: `Secret-bearing cache keys are using unkeyed SHA-256 because ${APIFUSE__CACHE__KEY_PEPPER_ENV} is not configured.`,
|
|
229
|
+
}),
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function normalizeKeyPart(
|
|
234
|
+
value: unknown,
|
|
235
|
+
extra: Set<string>,
|
|
236
|
+
pepper: string | undefined,
|
|
237
|
+
): NormalizedKeyPart {
|
|
111
238
|
if (Array.isArray(value)) {
|
|
112
|
-
|
|
239
|
+
const entries = value.map((entry) => normalizeKeyPart(entry, extra, pepper));
|
|
240
|
+
return {
|
|
241
|
+
value: entries.map((entry) => entry.value),
|
|
242
|
+
secretScoped: entries.some((entry) => entry.secretScoped),
|
|
243
|
+
};
|
|
113
244
|
}
|
|
114
245
|
if (isRecord(value)) {
|
|
115
|
-
const normalized: Record<string, unknown> =
|
|
246
|
+
const normalized: Record<string, unknown> = Object.create(null);
|
|
247
|
+
let secretScoped = false;
|
|
116
248
|
for (const key of Object.keys(value).sort()) {
|
|
117
|
-
|
|
118
|
-
|
|
249
|
+
const part = shouldRedactField(key, extra)
|
|
250
|
+
? hashSecretValue(value[key], key, extra, pepper)
|
|
251
|
+
: normalizeKeyPart(value[key], extra, pepper);
|
|
252
|
+
normalized[key] = part.value;
|
|
253
|
+
secretScoped ||= part.secretScoped;
|
|
119
254
|
}
|
|
120
|
-
return normalized;
|
|
255
|
+
return { value: normalized, secretScoped };
|
|
256
|
+
}
|
|
257
|
+
return { value, secretScoped: false };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function hashSecretValue(
|
|
261
|
+
value: unknown,
|
|
262
|
+
fieldName: string,
|
|
263
|
+
extra: Set<string>,
|
|
264
|
+
pepper: string | undefined,
|
|
265
|
+
): NormalizedKeyPart {
|
|
266
|
+
assertJsonSafeSecretValue(value, `${fieldName} (inside secret value)`);
|
|
267
|
+
const canonical = serializeSecretValue(normalizeKeyPart(value, extra, pepper).value);
|
|
268
|
+
if (pepper === undefined) {
|
|
269
|
+
warnAboutUnpepperedSecretKey();
|
|
270
|
+
const digest = createHash("sha256").update(canonical).digest("hex");
|
|
271
|
+
return { value: `sha256:${digest}`, secretScoped: true };
|
|
121
272
|
}
|
|
122
|
-
|
|
273
|
+
const digest = createHmac("sha256", pepper).update(canonical).digest("hex");
|
|
274
|
+
return { value: `hmac-sha256:${digest}`, secretScoped: true };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function metadataKeys(
|
|
278
|
+
events: ProviderCacheLookupMeta[],
|
|
279
|
+
secretScopedKeys: Set<string>,
|
|
280
|
+
): string[] {
|
|
281
|
+
let secretScopedIndex = 0;
|
|
282
|
+
return Array.from(new Set(events.map((event) => event.key))).map((key) => {
|
|
283
|
+
if (!secretScopedKeys.has(key)) return key;
|
|
284
|
+
secretScopedIndex += 1;
|
|
285
|
+
return `${SECRET_SCOPED_KEY_MARKER}#${secretScopedIndex}]`;
|
|
286
|
+
});
|
|
123
287
|
}
|
|
124
288
|
|
|
125
289
|
function stableHash(value: unknown): string {
|
|
@@ -198,10 +362,13 @@ async function withRedisFallback<T>(operation: () => Promise<T>): Promise<T | un
|
|
|
198
362
|
|
|
199
363
|
export function createProviderCache(options: ProviderCacheOptions): ProviderCache {
|
|
200
364
|
const redisUrl = options.redisUrl ?? providerCacheRedisUrlFromEnv();
|
|
365
|
+
const configuredPepper = process.env[APIFUSE__CACHE__KEY_PEPPER_ENV];
|
|
366
|
+
const pepper = configuredPepper && configuredPepper.length > 0 ? configuredPepper : undefined;
|
|
201
367
|
const backend = getSharedBackend(redisUrl);
|
|
202
368
|
const memoryMaxEntries = Math.max(1, options.memoryMaxEntries ?? DEFAULT_MEMORY_MAX_ENTRIES);
|
|
203
369
|
const now = options.now ?? Date.now;
|
|
204
370
|
const events: ProviderCacheLookupMeta[] = [];
|
|
371
|
+
const secretScopedKeys = new Set<string>();
|
|
205
372
|
|
|
206
373
|
function record(meta: ProviderCacheLookupMeta): void {
|
|
207
374
|
events.push(meta);
|
|
@@ -353,8 +520,10 @@ export function createProviderCache(options: ProviderCacheOptions): ProviderCach
|
|
|
353
520
|
return {
|
|
354
521
|
key(namespace, parts, keyOptions?: ProviderCacheKeyOptions) {
|
|
355
522
|
const extra = new Set((keyOptions?.redactFields ?? []).map((field) => field.toLowerCase()));
|
|
356
|
-
const normalized = normalizeKeyPart(parts, extra);
|
|
357
|
-
|
|
523
|
+
const normalized = normalizeKeyPart(parts, extra, pepper);
|
|
524
|
+
const key = `${DEFAULT_PREFIX}:${options.providerId}:${namespace}:${stableHash(normalized.value)}`;
|
|
525
|
+
if (normalized.secretScoped) secretScopedKeys.add(key);
|
|
526
|
+
return key;
|
|
358
527
|
},
|
|
359
528
|
|
|
360
529
|
async get<T = unknown>(key: string): Promise<ProviderCacheResult<T> | null> {
|
|
@@ -413,7 +582,7 @@ export function createProviderCache(options: ProviderCacheOptions): ProviderCach
|
|
|
413
582
|
return {
|
|
414
583
|
hit: events.some((event) => event.hit),
|
|
415
584
|
stale: events.some((event) => event.stale),
|
|
416
|
-
keys:
|
|
585
|
+
keys: metadataKeys(events, secretScopedKeys),
|
|
417
586
|
source: sourceSummary(events),
|
|
418
587
|
};
|
|
419
588
|
},
|
|
@@ -423,13 +592,18 @@ export function createProviderCache(options: ProviderCacheOptions): ProviderCach
|
|
|
423
592
|
export function createBypassProviderCache(
|
|
424
593
|
options: Pick<ProviderCacheOptions, "providerId">,
|
|
425
594
|
): ProviderCache {
|
|
595
|
+
const configuredPepper = process.env[APIFUSE__CACHE__KEY_PEPPER_ENV];
|
|
596
|
+
const pepper = configuredPepper && configuredPepper.length > 0 ? configuredPepper : undefined;
|
|
426
597
|
const events: ProviderCacheLookupMeta[] = [];
|
|
598
|
+
const secretScopedKeys = new Set<string>();
|
|
427
599
|
|
|
428
600
|
return {
|
|
429
601
|
key(namespace, parts, keyOptions?: ProviderCacheKeyOptions) {
|
|
430
602
|
const extra = new Set((keyOptions?.redactFields ?? []).map((field) => field.toLowerCase()));
|
|
431
|
-
const normalized = normalizeKeyPart(parts, extra);
|
|
432
|
-
|
|
603
|
+
const normalized = normalizeKeyPart(parts, extra, pepper);
|
|
604
|
+
const key = `${DEFAULT_PREFIX}:${options.providerId}:${namespace}:${stableHash(normalized.value)}`;
|
|
605
|
+
if (normalized.secretScoped) secretScopedKeys.add(key);
|
|
606
|
+
return key;
|
|
433
607
|
},
|
|
434
608
|
|
|
435
609
|
async get<T = unknown>(_key: string): Promise<ProviderCacheResult<T> | null> {
|
|
@@ -464,7 +638,7 @@ export function createBypassProviderCache(
|
|
|
464
638
|
return {
|
|
465
639
|
hit: false,
|
|
466
640
|
stale: false,
|
|
467
|
-
keys:
|
|
641
|
+
keys: metadataKeys(events, secretScopedKeys),
|
|
468
642
|
source: sourceSummary(events),
|
|
469
643
|
};
|
|
470
644
|
},
|
|
@@ -478,4 +652,5 @@ export function resetProviderCacheForTests(): void {
|
|
|
478
652
|
backend.redis?.disconnect();
|
|
479
653
|
}
|
|
480
654
|
sharedBackends.clear();
|
|
655
|
+
warnedAboutUnpepperedSecretKeys = false;
|
|
481
656
|
}
|
package/src/runtime/http.ts
CHANGED
|
@@ -517,7 +517,6 @@ async function resolveNativeProxy(
|
|
|
517
517
|
const resolvedProxy = await resolveProxyConfigAsync({
|
|
518
518
|
proxy: options.proxy ?? clientOptions.proxy,
|
|
519
519
|
upstream: clientOptions.upstream,
|
|
520
|
-
apifuseConfig: clientOptions.apifuseConfig,
|
|
521
520
|
proxyPolicy: clientOptions.proxyPolicy,
|
|
522
521
|
affinityKey: clientOptions.affinityKey,
|
|
523
522
|
proxyAttempt: computeProxyAttemptIndex({
|