@apifuse/provider-sdk 2.2.0-beta.24 → 2.2.0-beta.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/AUTHORING.md +7 -6
  2. package/CHANGELOG.md +9 -1
  3. package/README.md +3 -3
  4. package/bin/apifuse-check.ts +62 -3
  5. package/bin/apifuse-pack-check.ts +8 -2
  6. package/bin/apifuse-pack-smoke.ts +43 -2
  7. package/bin/apifuse-pack-types.ts +58 -0
  8. package/bin/apifuse-submit-check.ts +15 -2
  9. package/dist/auth.js +29 -0
  10. package/dist/cli/templates/provider/README.md.tpl +4 -4
  11. package/dist/contract-serialization.d.ts +20 -1
  12. package/dist/contract-serialization.js +583 -8
  13. package/dist/contract.d.ts +2 -0
  14. package/dist/contract.js +9 -5
  15. package/dist/declaration-validation.d.ts +23 -0
  16. package/dist/declaration-validation.js +159 -0
  17. package/dist/define.d.ts +1 -1
  18. package/dist/define.js +13 -2
  19. package/dist/index.d.ts +4 -3
  20. package/dist/index.js +3 -3
  21. package/dist/lint.js +85 -3
  22. package/dist/provider.d.ts +1 -1
  23. package/dist/provider.js +1 -1
  24. package/dist/runtime/cache.d.ts +1 -0
  25. package/dist/runtime/cache.js +169 -15
  26. package/dist/runtime/resolver-vendors/bindings.d.ts +42 -2
  27. package/dist/runtime/resolver-vendors/bindings.js +31 -6
  28. package/dist/runtime/resolver-vendors/browser.d.ts +3 -7
  29. package/dist/runtime/resolver-vendors/browser.js +7 -22
  30. package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
  31. package/dist/runtime/resolver-vendors/hosts.js +33 -0
  32. package/dist/runtime/resolver-vendors/twocaptcha.d.ts +23 -0
  33. package/dist/runtime/resolver-vendors/twocaptcha.js +264 -0
  34. package/dist/runtime/resolver-vendors/types.d.ts +44 -3
  35. package/dist/runtime/resolver-vendors/types.js +10 -0
  36. package/dist/runtime/resolver.d.ts +17 -2
  37. package/dist/runtime/resolver.js +237 -15
  38. package/dist/runtime/stealth.d.ts +26 -4
  39. package/dist/runtime/stealth.js +224 -114
  40. package/dist/schema.d.ts +63 -0
  41. package/dist/schema.js +808 -8
  42. package/dist/server/serve.js +8 -0
  43. package/dist/stealth/profiles.js +16 -7
  44. package/dist/types.d.ts +37 -4
  45. package/package.json +2 -2
  46. package/src/auth.ts +40 -0
  47. package/src/cli/templates/provider/README.md.tpl +4 -4
  48. package/src/contract-serialization.ts +857 -8
  49. package/src/contract.ts +16 -5
  50. package/src/declaration-validation.ts +202 -0
  51. package/src/define.ts +23 -2
  52. package/src/index.ts +13 -0
  53. package/src/lint.ts +98 -3
  54. package/src/provider.ts +10 -0
  55. package/src/runtime/cache.ts +189 -14
  56. package/src/runtime/resolver-vendors/bindings.ts +40 -15
  57. package/src/runtime/resolver-vendors/browser.ts +9 -31
  58. package/src/runtime/resolver-vendors/hosts.ts +38 -0
  59. package/src/runtime/resolver-vendors/twocaptcha.ts +366 -0
  60. package/src/runtime/resolver-vendors/types.ts +54 -0
  61. package/src/runtime/resolver.ts +304 -24
  62. package/src/runtime/stealth.ts +317 -136
  63. package/src/schema.ts +1060 -9
  64. package/src/server/serve.ts +8 -0
  65. package/src/stealth/profiles.ts +17 -7
  66. package/src/types.ts +39 -6
@@ -1,9 +1,12 @@
1
- import { createHash } from "node:crypto";
1
+ import { createHash, createHmac } from "node:crypto";
2
2
  import { providerCacheRedisUrlFromEnv } from "../config/loader.js";
3
+ import { ProviderError } from "../errors.js";
3
4
  import { createProviderRedisClient, ensureRedisReady, withRedisTimeout, } from "./redis.js";
5
+ export const APIFUSE__CACHE__KEY_PEPPER_ENV = "APIFUSE__CACHE__KEY_PEPPER";
4
6
  const DEFAULT_PREFIX = "apifuse:provider-cache:v1";
5
7
  const DEFAULT_MEMORY_MAX_ENTRIES = 1_000;
6
8
  const DEFAULT_REDIS_TIMEOUT_MS = 150;
9
+ const SECRET_SCOPED_KEY_MARKER = "[secret-scoped";
7
10
  const SECRET_FIELD_NAMES = new Set([
8
11
  "authorization",
9
12
  "cookie",
@@ -18,6 +21,7 @@ const SECRET_FIELD_NAMES = new Set([
18
21
  "refresh_token",
19
22
  ]);
20
23
  const sharedBackends = new Map();
24
+ let warnedAboutUnpepperedSecretKeys = false;
21
25
  function backendKey(redisUrl) {
22
26
  return redisUrl ?? "memory";
23
27
  }
@@ -55,20 +59,157 @@ function shouldRedactField(name, extra) {
55
59
  normalized.includes("password") ||
56
60
  normalized.includes("secret"));
57
61
  }
58
- function normalizeKeyPart(value, extra) {
62
+ function unsupportedSecretValue(path, reason) {
63
+ throw new ProviderError(`Secret cache-key values must be JSON-safe; ${reason} at ${path}.`, {
64
+ code: "CACHE_KEY_SECRET_VALUE_UNSUPPORTED",
65
+ fix: "Convert the secret cache-key selector to JSON-safe primitives, arrays, or plain objects.",
66
+ });
67
+ }
68
+ function assertJsonSafeSecretValue(value, reportedPath, ancestors = new Set()) {
69
+ if (value === undefined)
70
+ return;
71
+ if (value === null || typeof value === "string" || typeof value === "boolean")
72
+ return;
73
+ if (typeof value === "number") {
74
+ if (!Number.isFinite(value))
75
+ unsupportedSecretValue(reportedPath, "non-finite numbers are unsupported");
76
+ if (Object.is(value, -0))
77
+ unsupportedSecretValue(reportedPath, "negative zero is unsupported");
78
+ return;
79
+ }
80
+ if (typeof value !== "object") {
81
+ unsupportedSecretValue(reportedPath, `${typeof value} values are unsupported`);
82
+ }
83
+ if (ancestors.has(value))
84
+ unsupportedSecretValue(reportedPath, "cyclic values are unsupported");
85
+ ancestors.add(value);
86
+ try {
87
+ if (Array.isArray(value)) {
88
+ if (Object.getOwnPropertySymbols(value).length > 0) {
89
+ unsupportedSecretValue(reportedPath, "symbol-keyed array properties are unsupported");
90
+ }
91
+ const expectedNames = new Set(["length"]);
92
+ for (let index = 0; index < value.length; index += 1) {
93
+ const key = String(index);
94
+ expectedNames.add(key);
95
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
96
+ if (!descriptor)
97
+ unsupportedSecretValue(reportedPath, "sparse arrays are unsupported");
98
+ if (!descriptor.enumerable || !("value" in descriptor)) {
99
+ unsupportedSecretValue(reportedPath, "array accessors are unsupported");
100
+ }
101
+ assertJsonSafeSecretValue(descriptor.value, reportedPath, ancestors);
102
+ }
103
+ if (Object.getOwnPropertyNames(value).some((name) => !expectedNames.has(name))) {
104
+ unsupportedSecretValue(reportedPath, "custom array properties are unsupported");
105
+ }
106
+ return;
107
+ }
108
+ const prototype = Object.getPrototypeOf(value);
109
+ if (prototype !== Object.prototype && prototype !== null) {
110
+ unsupportedSecretValue(reportedPath, "non-plain objects are unsupported");
111
+ }
112
+ if (Object.getOwnPropertySymbols(value).length > 0) {
113
+ unsupportedSecretValue(reportedPath, "symbol-keyed properties are unsupported");
114
+ }
115
+ for (const key of Object.getOwnPropertyNames(value)) {
116
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
117
+ if (!descriptor?.enumerable || !("value" in descriptor)) {
118
+ unsupportedSecretValue(reportedPath, "non-enumerable properties and accessors are unsupported");
119
+ }
120
+ assertJsonSafeSecretValue(descriptor.value, reportedPath, ancestors);
121
+ }
122
+ }
123
+ finally {
124
+ ancestors.delete(value);
125
+ }
126
+ }
127
+ function containsUndefined(value) {
128
+ if (value === undefined)
129
+ return true;
130
+ if (Array.isArray(value))
131
+ return value.some(containsUndefined);
132
+ if (isRecord(value))
133
+ return Object.values(value).some(containsUndefined);
134
+ return false;
135
+ }
136
+ function tagSecretValue(value) {
137
+ if (value === undefined)
138
+ return ["undefined"];
139
+ if (value === null)
140
+ return ["null"];
141
+ if (typeof value === "string")
142
+ return ["string", value];
143
+ if (typeof value === "number")
144
+ return ["number", value];
145
+ if (typeof value === "boolean")
146
+ return ["boolean", value];
147
+ if (Array.isArray(value))
148
+ return ["array", value.map(tagSecretValue)];
149
+ return [
150
+ "object",
151
+ Object.entries(value).map(([key, entry]) => [
152
+ key,
153
+ tagSecretValue(entry),
154
+ ]),
155
+ ];
156
+ }
157
+ function serializeSecretValue(value) {
158
+ if (!containsUndefined(value))
159
+ return JSON.stringify([value]);
160
+ return `undefined-v1:${JSON.stringify(tagSecretValue(value))}`;
161
+ }
162
+ function warnAboutUnpepperedSecretKey() {
163
+ if (warnedAboutUnpepperedSecretKeys)
164
+ return;
165
+ warnedAboutUnpepperedSecretKeys = true;
166
+ console.warn(JSON.stringify({
167
+ level: "warn",
168
+ event: "provider_cache_secret_key_unpeppered",
169
+ message: `Secret-bearing cache keys are using unkeyed SHA-256 because ${APIFUSE__CACHE__KEY_PEPPER_ENV} is not configured.`,
170
+ }));
171
+ }
172
+ function normalizeKeyPart(value, extra, pepper) {
59
173
  if (Array.isArray(value)) {
60
- return value.map((entry) => normalizeKeyPart(entry, extra));
174
+ const entries = value.map((entry) => normalizeKeyPart(entry, extra, pepper));
175
+ return {
176
+ value: entries.map((entry) => entry.value),
177
+ secretScoped: entries.some((entry) => entry.secretScoped),
178
+ };
61
179
  }
62
180
  if (isRecord(value)) {
63
- const normalized = {};
181
+ const normalized = Object.create(null);
182
+ let secretScoped = false;
64
183
  for (const key of Object.keys(value).sort()) {
65
- if (shouldRedactField(key, extra))
66
- continue;
67
- normalized[key] = normalizeKeyPart(value[key], extra);
184
+ const part = shouldRedactField(key, extra)
185
+ ? hashSecretValue(value[key], key, extra, pepper)
186
+ : normalizeKeyPart(value[key], extra, pepper);
187
+ normalized[key] = part.value;
188
+ secretScoped ||= part.secretScoped;
68
189
  }
69
- return normalized;
190
+ return { value: normalized, secretScoped };
70
191
  }
71
- return value;
192
+ return { value, secretScoped: false };
193
+ }
194
+ function hashSecretValue(value, fieldName, extra, pepper) {
195
+ assertJsonSafeSecretValue(value, `${fieldName} (inside secret value)`);
196
+ const canonical = serializeSecretValue(normalizeKeyPart(value, extra, pepper).value);
197
+ if (pepper === undefined) {
198
+ warnAboutUnpepperedSecretKey();
199
+ const digest = createHash("sha256").update(canonical).digest("hex");
200
+ return { value: `sha256:${digest}`, secretScoped: true };
201
+ }
202
+ const digest = createHmac("sha256", pepper).update(canonical).digest("hex");
203
+ return { value: `hmac-sha256:${digest}`, secretScoped: true };
204
+ }
205
+ function metadataKeys(events, secretScopedKeys) {
206
+ let secretScopedIndex = 0;
207
+ return Array.from(new Set(events.map((event) => event.key))).map((key) => {
208
+ if (!secretScopedKeys.has(key))
209
+ return key;
210
+ secretScopedIndex += 1;
211
+ return `${SECRET_SCOPED_KEY_MARKER}#${secretScopedIndex}]`;
212
+ });
72
213
  }
73
214
  function stableHash(value) {
74
215
  return createHash("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 32);
@@ -138,10 +279,13 @@ async function withRedisFallback(operation) {
138
279
  }
139
280
  export function createProviderCache(options) {
140
281
  const redisUrl = options.redisUrl ?? providerCacheRedisUrlFromEnv();
282
+ const configuredPepper = process.env[APIFUSE__CACHE__KEY_PEPPER_ENV];
283
+ const pepper = configuredPepper && configuredPepper.length > 0 ? configuredPepper : undefined;
141
284
  const backend = getSharedBackend(redisUrl);
142
285
  const memoryMaxEntries = Math.max(1, options.memoryMaxEntries ?? DEFAULT_MEMORY_MAX_ENTRIES);
143
286
  const now = options.now ?? Date.now;
144
287
  const events = [];
288
+ const secretScopedKeys = new Set();
145
289
  function record(meta) {
146
290
  events.push(meta);
147
291
  }
@@ -266,8 +410,11 @@ export function createProviderCache(options) {
266
410
  return {
267
411
  key(namespace, parts, keyOptions) {
268
412
  const extra = new Set((keyOptions?.redactFields ?? []).map((field) => field.toLowerCase()));
269
- const normalized = normalizeKeyPart(parts, extra);
270
- return `${DEFAULT_PREFIX}:${options.providerId}:${namespace}:${stableHash(normalized)}`;
413
+ const normalized = normalizeKeyPart(parts, extra, pepper);
414
+ const key = `${DEFAULT_PREFIX}:${options.providerId}:${namespace}:${stableHash(normalized.value)}`;
415
+ if (normalized.secretScoped)
416
+ secretScopedKeys.add(key);
417
+ return key;
271
418
  },
272
419
  async get(key) {
273
420
  const result = await read(key);
@@ -312,19 +459,25 @@ export function createProviderCache(options) {
312
459
  return {
313
460
  hit: events.some((event) => event.hit),
314
461
  stale: events.some((event) => event.stale),
315
- keys: Array.from(new Set(events.map((event) => event.key))),
462
+ keys: metadataKeys(events, secretScopedKeys),
316
463
  source: sourceSummary(events),
317
464
  };
318
465
  },
319
466
  };
320
467
  }
321
468
  export function createBypassProviderCache(options) {
469
+ const configuredPepper = process.env[APIFUSE__CACHE__KEY_PEPPER_ENV];
470
+ const pepper = configuredPepper && configuredPepper.length > 0 ? configuredPepper : undefined;
322
471
  const events = [];
472
+ const secretScopedKeys = new Set();
323
473
  return {
324
474
  key(namespace, parts, keyOptions) {
325
475
  const extra = new Set((keyOptions?.redactFields ?? []).map((field) => field.toLowerCase()));
326
- const normalized = normalizeKeyPart(parts, extra);
327
- return `${DEFAULT_PREFIX}:${options.providerId}:${namespace}:${stableHash(normalized)}`;
476
+ const normalized = normalizeKeyPart(parts, extra, pepper);
477
+ const key = `${DEFAULT_PREFIX}:${options.providerId}:${namespace}:${stableHash(normalized.value)}`;
478
+ if (normalized.secretScoped)
479
+ secretScopedKeys.add(key);
480
+ return key;
328
481
  },
329
482
  async get(_key) {
330
483
  return null;
@@ -352,7 +505,7 @@ export function createBypassProviderCache(options) {
352
505
  return {
353
506
  hit: false,
354
507
  stale: false,
355
- keys: Array.from(new Set(events.map((event) => event.key))),
508
+ keys: metadataKeys(events, secretScopedKeys),
356
509
  source: sourceSummary(events),
357
510
  };
358
511
  },
@@ -365,4 +518,5 @@ export function resetProviderCacheForTests() {
365
518
  backend.redis?.disconnect();
366
519
  }
367
520
  sharedBackends.clear();
521
+ warnedAboutUnpepperedSecretKeys = false;
368
522
  }
@@ -1,8 +1,48 @@
1
1
  import type { ProviderChallenge } from "../../types.js";
2
2
  import type { ResolverIssuingIdentity } from "./types.js";
3
3
  export declare const RESOLVER_CHALLENGE_BINDINGS: {
4
- readonly aws_waf: "portable";
5
- readonly cloudflare_interstitial: "identity_scoped";
4
+ readonly turnstile: {
5
+ readonly cacheable: false;
6
+ readonly identityBinding: "none";
7
+ readonly directCacheable: false;
8
+ };
9
+ readonly recaptcha_v2: {
10
+ readonly cacheable: false;
11
+ readonly identityBinding: "none";
12
+ readonly directCacheable: false;
13
+ };
14
+ readonly recaptcha_v3: {
15
+ readonly cacheable: false;
16
+ readonly identityBinding: "none";
17
+ readonly directCacheable: false;
18
+ };
19
+ readonly hcaptcha: {
20
+ readonly cacheable: false;
21
+ readonly identityBinding: "none";
22
+ readonly directCacheable: false;
23
+ };
24
+ readonly cloudflare_interstitial: {
25
+ readonly cacheable: true;
26
+ readonly identityBinding: "identity_scoped";
27
+ readonly directCacheable: true;
28
+ };
29
+ readonly aws_waf: {
30
+ readonly cacheable: true;
31
+ readonly identityBinding: "portable";
32
+ readonly directCacheable: true;
33
+ };
34
+ readonly akamai_sec_cpt: {
35
+ readonly cacheable: true;
36
+ readonly identityBinding: "identity_scoped";
37
+ readonly directCacheable: false;
38
+ };
39
+ readonly akamai_sensor: {
40
+ readonly cacheable: true;
41
+ readonly identityBinding: "identity_scoped";
42
+ readonly directCacheable: false;
43
+ };
6
44
  };
45
+ export declare function resolverChallengeIsCacheable(challenge: ProviderChallenge): boolean;
46
+ export declare function resolverChallengeAllowsDirectCache(challenge: ProviderChallenge): boolean;
7
47
  export declare function resolverChallengeIsIdentityScoped(challenge: ProviderChallenge): boolean;
8
48
  export declare function resolverChallengeIssuingIdentity(challenge: ProviderChallenge, identity: ResolverIssuingIdentity): ResolverIssuingIdentity;
@@ -1,14 +1,39 @@
1
+ // An IP-bound artifact minted without any recorded egress identity is unsafe to
2
+ // share. The Akamai kinds therefore reject direct caching, while Cloudflare
3
+ // keeps its pre-existing direct-cache behavior pending measurement.
1
4
  export const RESOLVER_CHALLENGE_BINDINGS = {
2
- aws_waf: "portable",
3
- cloudflare_interstitial: "identity_scoped",
5
+ turnstile: { cacheable: false, identityBinding: "none", directCacheable: false },
6
+ recaptcha_v2: { cacheable: false, identityBinding: "none", directCacheable: false },
7
+ recaptcha_v3: { cacheable: false, identityBinding: "none", directCacheable: false },
8
+ hcaptcha: { cacheable: false, identityBinding: "none", directCacheable: false },
9
+ cloudflare_interstitial: {
10
+ cacheable: true,
11
+ identityBinding: "identity_scoped",
12
+ directCacheable: true,
13
+ },
14
+ aws_waf: { cacheable: true, identityBinding: "portable", directCacheable: true },
15
+ akamai_sec_cpt: {
16
+ cacheable: true,
17
+ identityBinding: "identity_scoped",
18
+ directCacheable: false,
19
+ },
20
+ akamai_sensor: {
21
+ cacheable: true,
22
+ identityBinding: "identity_scoped",
23
+ directCacheable: false,
24
+ },
4
25
  };
26
+ export function resolverChallengeIsCacheable(challenge) {
27
+ return RESOLVER_CHALLENGE_BINDINGS[challenge.kind].cacheable;
28
+ }
29
+ export function resolverChallengeAllowsDirectCache(challenge) {
30
+ return RESOLVER_CHALLENGE_BINDINGS[challenge.kind].directCacheable;
31
+ }
5
32
  export function resolverChallengeIsIdentityScoped(challenge) {
6
- return (RESOLVER_CHALLENGE_BINDINGS[challenge.kind] ===
7
- "identity_scoped");
33
+ return RESOLVER_CHALLENGE_BINDINGS[challenge.kind].identityBinding === "identity_scoped";
8
34
  }
9
35
  export function resolverChallengeIssuingIdentity(challenge, identity) {
10
- const binding = RESOLVER_CHALLENGE_BINDINGS[challenge.kind];
11
- if (binding === "portable") {
36
+ if (RESOLVER_CHALLENGE_BINDINGS[challenge.kind].identityBinding === "portable") {
12
37
  return { userAgent: identity.userAgent };
13
38
  }
14
39
  return identity;
@@ -10,15 +10,11 @@ export interface BrowserResolverVendorOptions {
10
10
  readonly allowedHosts: readonly string[];
11
11
  readonly createClient?: BrowserClientFactory;
12
12
  }
13
- export type BrowserResolverSolution = Extract<ChallengeSolution, {
14
- readonly form: "cookies";
15
- }> & {
16
- /** Unix seconds from the cookie that proved the challenge cleared. */
17
- readonly expires?: number;
18
- };
19
13
  export interface BrowserResolverVendorAdapter extends ResolverVendorAdapter {
20
14
  readonly id: "browser";
21
- solve(challenge: ProviderChallenge, identity: ResolverIdentity | undefined, signal: AbortSignal, traceRecorder?: TraceRecorder): Promise<BrowserResolverSolution>;
15
+ solve(challenge: ProviderChallenge, identity: ResolverIdentity | undefined, signal: AbortSignal, traceRecorder?: TraceRecorder): Promise<Extract<ChallengeSolution, {
16
+ readonly form: "cookies";
17
+ }>>;
22
18
  }
23
19
  export declare function createBrowserResolverVendorAdapter(options: BrowserResolverVendorOptions): BrowserResolverVendorAdapter;
24
20
  export {};
@@ -1,6 +1,7 @@
1
- import { isProviderError, ProviderError } from "../../errors.js";
1
+ import { isProviderError } from "../../errors.js";
2
2
  import { createBrowserClient } from "../browser.js";
3
3
  import { resolverChallengeIssuingIdentity } from "./bindings.js";
4
+ import { assertResolverHostAllowed, normalizedResolverHostname } from "./hosts.js";
4
5
  import { ResolverVendorUnavailableError, } from "./types.js";
5
6
  const BROWSER_VENDOR_ID = "browser";
6
7
  const DEFAULT_COOKIE_POLL_INTERVAL_MS = 100;
@@ -94,32 +95,16 @@ async function abortableDelay(ms, signal) {
94
95
  function isSupportedKind(kind) {
95
96
  return Object.hasOwn(SUCCESS_COOKIE_NAMES, kind);
96
97
  }
97
- function normalizedHostname(hostname) {
98
- return hostname.trim().toLowerCase().replace(/\.$/, "");
99
- }
100
- function assertChallengeHostAllowed(pageUrl, allowedHosts) {
101
- const challengeHost = normalizedHostname(new URL(pageUrl).hostname);
102
- const isAllowed = allowedHosts.some((host) => {
103
- const declaredHost = normalizedHostname(host);
104
- return declaredHost.length > 0 && !declaredHost.includes("*") && declaredHost === challengeHost;
105
- });
106
- if (isAllowed)
107
- return;
108
- throw new ProviderError(`Resolver challenge host "${challengeHost}" is not declared`, {
109
- code: "RESOLVER_HOST_NOT_ALLOWED",
110
- fix: "Add the exact challenge hostname to the provider's allowedHosts declaration.",
111
- });
112
- }
113
98
  function cookieDomainSpecificity(cookie) {
114
- return normalizedHostname(cookie.domain.replace(/^\./, "")).length;
99
+ return normalizedResolverHostname(cookie.domain.replace(/^\./, "")).length;
115
100
  }
116
101
  function isHostOnlyCookieFor(cookie, hostname) {
117
102
  return (!cookie.domain.startsWith(".") &&
118
- normalizedHostname(cookie.domain) === normalizedHostname(hostname));
103
+ normalizedResolverHostname(cookie.domain) === normalizedResolverHostname(hostname));
119
104
  }
120
105
  function cookieAppliesToUrl(cookie, url) {
121
- const cookieDomain = normalizedHostname(cookie.domain.replace(/^\./, ""));
122
- const requestHostname = normalizedHostname(url.hostname);
106
+ const cookieDomain = normalizedResolverHostname(cookie.domain.replace(/^\./, ""));
107
+ const requestHostname = normalizedResolverHostname(url.hostname);
123
108
  const domainMatches = cookieDomain.length > 0 &&
124
109
  (requestHostname === cookieDomain ||
125
110
  (cookie.domain.startsWith(".") && requestHostname.endsWith(`.${cookieDomain}`)));
@@ -226,7 +211,7 @@ export function createBrowserResolverVendorAdapter(options) {
226
211
  if (!isSupportedKind(challenge.kind)) {
227
212
  throw new TypeError(`Browser resolver does not support ${challenge.kind}`);
228
213
  }
229
- assertChallengeHostAllowed(challenge.pageUrl, options.allowedHosts);
214
+ assertResolverHostAllowed(challenge.pageUrl, options.allowedHosts);
230
215
  const challengeKind = challenge.kind;
231
216
  callerSignal.throwIfAborted();
232
217
  const solveController = new AbortController();
@@ -0,0 +1,2 @@
1
+ export declare function normalizedResolverHostname(hostname: string): string;
2
+ export declare function assertResolverHostAllowed(targetUrl: string, allowedHosts: readonly string[]): void;
@@ -0,0 +1,33 @@
1
+ import { ProviderError } from "../../errors.js";
2
+ export function normalizedResolverHostname(hostname) {
3
+ return hostname.trim().toLowerCase().replace(/\.$/, "");
4
+ }
5
+ export function assertResolverHostAllowed(targetUrl, allowedHosts) {
6
+ let targetUrlObject;
7
+ try {
8
+ targetUrlObject = new URL(targetUrl);
9
+ }
10
+ catch {
11
+ throw new ProviderError("Resolver target URL is invalid", {
12
+ code: "RESOLVER_HOST_NOT_ALLOWED",
13
+ fix: "Use a valid URL whose exact hostname appears in the provider's allowedHosts declaration.",
14
+ });
15
+ }
16
+ if (targetUrlObject.protocol !== "http:" && targetUrlObject.protocol !== "https:") {
17
+ throw new ProviderError(`Resolver target URL scheme "${targetUrlObject.protocol}" is not allowed`, {
18
+ code: "RESOLVER_HOST_NOT_ALLOWED",
19
+ fix: "Use an http or https URL whose exact hostname appears in the provider's allowedHosts declaration.",
20
+ });
21
+ }
22
+ const targetHost = normalizedResolverHostname(targetUrlObject.hostname);
23
+ const isAllowed = allowedHosts.some((host) => {
24
+ const declaredHost = normalizedResolverHostname(host);
25
+ return declaredHost.length > 0 && !declaredHost.includes("*") && declaredHost === targetHost;
26
+ });
27
+ if (isAllowed)
28
+ return;
29
+ throw new ProviderError(`Resolver target host "${targetHost}" is not declared`, {
30
+ code: "RESOLVER_HOST_NOT_ALLOWED",
31
+ fix: "Add the exact target hostname to the provider's allowedHosts declaration.",
32
+ });
33
+ }
@@ -0,0 +1,23 @@
1
+ import type { ChallengeSolution, ProviderChallenge } from "../../types.js";
2
+ import { type ResolverIdentity, type ResolverVendorAdapter } from "./types.js";
3
+ type Delay = (ms: number, signal: AbortSignal) => Promise<void>;
4
+ export interface TwoCaptchaResolverVendorOptions {
5
+ readonly apiKey?: string;
6
+ readonly timeoutMs?: number;
7
+ readonly pollIntervalMs?: number;
8
+ readonly allowedHosts: readonly string[];
9
+ readonly fetchImpl?: typeof fetch;
10
+ readonly baseUrl?: string;
11
+ /** Test-only clock override; supplying it disables the real-time deadline timer. */
12
+ readonly now?: () => number;
13
+ /** Test-only delay override used with `now` to exercise polling without sleeping. */
14
+ readonly delay?: Delay;
15
+ }
16
+ export interface TwoCaptchaResolverVendorAdapter extends ResolverVendorAdapter {
17
+ readonly id: "2captcha";
18
+ solve(challenge: ProviderChallenge, identity: ResolverIdentity | undefined, signal: AbortSignal): Promise<Extract<ChallengeSolution, {
19
+ readonly form: "token";
20
+ }>>;
21
+ }
22
+ export declare function createTwoCaptchaResolverVendorAdapter(options: TwoCaptchaResolverVendorOptions): TwoCaptchaResolverVendorAdapter;
23
+ export {};