@apifuse/provider-sdk 2.2.0-beta.5 → 2.2.0-beta.8
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 +53 -0
- package/CHANGELOG.md +12 -0
- package/README.md +5 -1
- package/SUBMISSION.md +1 -1
- package/bin/apifuse-check.ts +26 -1
- package/bin/apifuse-pack-check.ts +14 -0
- package/bin/apifuse-submit-check.ts +193 -2
- package/bin/apifuse-sync-assets.ts +117 -0
- package/dist/auth-turn/index.d.ts +2 -2
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +8 -0
- package/dist/cli/create.d.ts +3 -0
- package/dist/cli/create.js +34 -35
- package/dist/cli/prompt-assets.d.ts +80 -0
- package/dist/cli/prompt-assets.js +743 -0
- package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/dist/config/loader.d.ts +79 -6
- package/dist/config/loader.js +272 -48
- package/dist/define.js +27 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/runtime/executor.js +7 -0
- package/dist/runtime/http.js +3 -0
- package/dist/runtime/proxy-errors.js +6 -2
- package/dist/runtime/proxy-nodemaven.d.ts +34 -0
- package/dist/runtime/proxy-nodemaven.js +128 -0
- package/dist/runtime/proxy-telemetry.d.ts +2 -1
- package/dist/runtime/proxy-telemetry.js +39 -4
- package/dist/runtime/secrets.d.ts +27 -0
- package/dist/runtime/secrets.js +51 -0
- package/dist/runtime/stealth.js +20 -9
- package/dist/server/serve.d.ts +5 -0
- package/dist/server/serve.js +39 -0
- package/dist/server/types.d.ts +9 -9
- package/dist/types.d.ts +30 -1
- package/package.json +4 -3
- package/src/cli/commands.ts +10 -0
- package/src/cli/create.ts +42 -35
- package/src/cli/prompt-assets.ts +865 -0
- package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/src/config/loader.ts +405 -61
- package/src/define.ts +35 -3
- package/src/index.ts +5 -0
- package/src/runtime/executor.ts +8 -0
- package/src/runtime/http.ts +3 -0
- package/src/runtime/proxy-errors.ts +12 -4
- package/src/runtime/proxy-nodemaven.ts +178 -0
- package/src/runtime/proxy-telemetry.ts +56 -5
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/stealth.ts +26 -10
- package/src/server/serve.ts +53 -0
- package/src/types.ts +30 -1
- package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
- package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
- /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
package/dist/config/loader.js
CHANGED
|
@@ -2,6 +2,11 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { Redis } from "ioredis";
|
|
5
|
+
import { NODEMAVEN_DEFAULT_PROTOCOL, hasNodemavenCredentials, nodemavenPoolSize, synthesizeNodemavenProxy, } from "../runtime/proxy-nodemaven.js";
|
|
6
|
+
// "smartproxy" here is api.smartproxy.org — a residential proxy with an IP
|
|
7
|
+
// extraction API (app_key → raw ip:port pool). It is NOT the company formerly
|
|
8
|
+
// named Smartproxy (smartproxy.com), which rebranded to Decodo in 2025 and is
|
|
9
|
+
// modelled separately as the `decodo` gateway vendor. Do not conflate them.
|
|
5
10
|
export const SMARTPROXY_APP_KEY_ENV = "APIFUSE__PROXY__SMARTPROXY_APP_KEY";
|
|
6
11
|
export const SMARTPROXY_MAX_LIFETIME_MINUTES = 2000;
|
|
7
12
|
export const DEFAULT_SMARTPROXY_POOL_SIZE = 20;
|
|
@@ -15,11 +20,17 @@ export const REDIS_URL_ENV = "APIFUSE__REDIS__URL";
|
|
|
15
20
|
export class ProxyResolutionError extends Error {
|
|
16
21
|
code;
|
|
17
22
|
telemetry;
|
|
23
|
+
vendor;
|
|
24
|
+
vendorChain;
|
|
25
|
+
protocol;
|
|
18
26
|
constructor(code, message, options) {
|
|
19
27
|
super(message, options);
|
|
20
28
|
this.name = "ProxyResolutionError";
|
|
21
29
|
this.code = code;
|
|
22
30
|
this.telemetry = options?.telemetry;
|
|
31
|
+
this.vendor = options?.vendor;
|
|
32
|
+
this.vendorChain = options?.vendorChain;
|
|
33
|
+
this.protocol = options?.protocol;
|
|
23
34
|
}
|
|
24
35
|
}
|
|
25
36
|
const proxyCache = new Map();
|
|
@@ -232,6 +243,11 @@ function applyStickyProxySession(proxyUrl) {
|
|
|
232
243
|
if (!parsed.hostname || !parsed.username || !parsed.password) {
|
|
233
244
|
return proxyUrl;
|
|
234
245
|
}
|
|
246
|
+
// This rewrites sticky-session usernames for a bring-your-own *gateway* URL
|
|
247
|
+
// (APIFUSE__PROXY__URL). The `smartproxy` host here means a smartproxy.com /
|
|
248
|
+
// Decodo-family gateway that authenticates by username — NOT the
|
|
249
|
+
// api.smartproxy.org allocation vendor, whose endpoints are raw ip:port with
|
|
250
|
+
// no credentials and therefore return early above.
|
|
235
251
|
const host = parsed.hostname.toLowerCase();
|
|
236
252
|
if (!host.includes("smartproxy") && !host.includes("decodo")) {
|
|
237
253
|
return proxyUrl;
|
|
@@ -299,47 +315,173 @@ export async function resolveProxyConfigAsync(options = {}) {
|
|
|
299
315
|
if (policy.mode === "disabled") {
|
|
300
316
|
return { shouldWarn: false };
|
|
301
317
|
}
|
|
302
|
-
const
|
|
303
|
-
if (
|
|
318
|
+
const chain = resolveVendorChain(policy);
|
|
319
|
+
if (chain.length === 0) {
|
|
320
|
+
// decodo/custom/env-static providers keep the legacy static-URL path.
|
|
304
321
|
return resolveProxyConfig({
|
|
305
322
|
...options,
|
|
306
323
|
upstream: { proxy: true },
|
|
307
324
|
});
|
|
308
325
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
326
|
+
// Protocol is chosen per vendor (each vendor's benchmarked-best), with an
|
|
327
|
+
// optional explicit override for the harness/tests. Both are tunnelling
|
|
328
|
+
// schemes. transportProtocols is what the calling transport can actually use.
|
|
329
|
+
const transportProtocols = options.transportProtocols ?? ["http", "socks5"];
|
|
330
|
+
const sizes = chain.map((vendor) => vendorPoolSize(vendor, policy));
|
|
331
|
+
const total = sizes.reduce((sum, size) => sum + size, 0);
|
|
332
|
+
const normalizedAttempt = normalizeAttemptIndex(options.proxyAttempt);
|
|
333
|
+
const { vendorIndex: startVendorIndex, poolIndex: startPoolIndex } = mapFlatAttempt(total > 0 ? normalizedAttempt % total : 0, sizes);
|
|
334
|
+
const refreshEpoch = normalizeAttemptIndex(options.proxyRefreshEpoch);
|
|
335
|
+
let lastError;
|
|
336
|
+
let blockedProtocol;
|
|
337
|
+
for (let vendorIndex = startVendorIndex; vendorIndex < chain.length; vendorIndex++) {
|
|
338
|
+
const vendor = chain[vendorIndex];
|
|
339
|
+
const nextVendor = chain[vendorIndex + 1];
|
|
340
|
+
const poolIndex = vendorIndex === startVendorIndex ? startPoolIndex : 0;
|
|
341
|
+
const protocol = options.protocol ?? VENDOR_DEFAULT_PROTOCOL[vendor];
|
|
342
|
+
if (!vendorHasCredentials(vendor)) {
|
|
343
|
+
options.telemetry?.recordProxyVendorFailover?.({
|
|
344
|
+
vendor,
|
|
345
|
+
nextVendor,
|
|
346
|
+
phase: "resolution",
|
|
347
|
+
reason: "no_credentials",
|
|
348
|
+
});
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
// The calling transport must be able to use this vendor's protocol; if not,
|
|
352
|
+
// fail over to the next vendor rather than silently downgrading.
|
|
353
|
+
if (!transportProtocols.includes(protocol)) {
|
|
354
|
+
blockedProtocol = protocol;
|
|
355
|
+
options.telemetry?.recordProxyVendorFailover?.({
|
|
356
|
+
vendor,
|
|
357
|
+
nextVendor,
|
|
358
|
+
phase: "resolution",
|
|
359
|
+
reason: "protocol_unsupported",
|
|
360
|
+
});
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
try {
|
|
364
|
+
return await resolveWithVendor(vendor, policy, options, {
|
|
365
|
+
protocol,
|
|
366
|
+
poolIndex,
|
|
367
|
+
refreshEpoch,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
// Config/programming errors (invalid filter, etc.) are not vendor
|
|
372
|
+
// outages — propagate them rather than failing over.
|
|
373
|
+
if (!(error instanceof ProxyResolutionError)) {
|
|
374
|
+
throw error;
|
|
375
|
+
}
|
|
376
|
+
if (error.telemetry) {
|
|
377
|
+
options.telemetry?.recordProxyResolution(error.telemetry);
|
|
378
|
+
}
|
|
379
|
+
lastError = error;
|
|
380
|
+
options.telemetry?.recordProxyVendorFailover?.({
|
|
381
|
+
vendor,
|
|
382
|
+
nextVendor,
|
|
383
|
+
phase: "resolution",
|
|
384
|
+
reason: "allocation_failed",
|
|
385
|
+
});
|
|
313
386
|
}
|
|
314
|
-
return { shouldWarn: true };
|
|
315
387
|
}
|
|
316
|
-
|
|
388
|
+
if (policy.mode === "required") {
|
|
389
|
+
if (lastError) {
|
|
390
|
+
throw lastError instanceof ProxyResolutionError
|
|
391
|
+
? lastError
|
|
392
|
+
: new ProxyResolutionError("PROXY_ALLOCATION_FAILED", `All proxy vendors [${chain.join(", ")}] failed for required proxy egress.`, { cause: lastError, vendorChain: chain });
|
|
393
|
+
}
|
|
394
|
+
if (blockedProtocol) {
|
|
395
|
+
throw new ProxyResolutionError("PROXY_PROTOCOL_UNSUPPORTED", `No proxy vendor in [${chain.join(", ")}] could serve a protocol supported by this transport (supports: ${transportProtocols.join(", ")}; vendor wanted "${blockedProtocol}"). Route this provider through the stealth transport.`, { protocol: blockedProtocol, vendorChain: chain });
|
|
396
|
+
}
|
|
397
|
+
throw new ProxyResolutionError("PROXY_REQUIRED", `Proxy egress is required but no vendor credentials are configured. Missing: ${chain
|
|
398
|
+
.map((vendor) => `${missingCredentialEnv(vendor)} (${vendor})`)
|
|
399
|
+
.join(", ")}.`, { vendorChain: chain });
|
|
400
|
+
}
|
|
401
|
+
return { shouldWarn: true };
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Each vendor's default egress protocol, chosen from live KR benchmarks. HTTP
|
|
405
|
+
* CONNECT wins for nodemaven (socks5 adds ~500ms through the gateway) and ties
|
|
406
|
+
* for smartproxy, and is the only protocol ctx.http (Bun native fetch) supports.
|
|
407
|
+
* Override per call via ProxyResolutionOptions.protocol (harness/tests).
|
|
408
|
+
*/
|
|
409
|
+
const VENDOR_DEFAULT_PROTOCOL = {
|
|
410
|
+
smartproxy: "http",
|
|
411
|
+
nodemaven: NODEMAVEN_DEFAULT_PROTOCOL,
|
|
412
|
+
};
|
|
413
|
+
/**
|
|
414
|
+
* Guard the No-MITM invariant: a resolved proxy URL must use a tunnelling scheme
|
|
415
|
+
* (http CONNECT or socks5) so the client TLS handshake reaches the origin
|
|
416
|
+
* end-to-end. Anything else would intercept TLS and break fingerprinting.
|
|
417
|
+
*/
|
|
418
|
+
export function assertTunnelingScheme(url) {
|
|
419
|
+
let scheme;
|
|
317
420
|
try {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
421
|
+
scheme = new URL(url).protocol.replace(/:$/, "").toLowerCase();
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
throw new ProxyResolutionError("PROXY_ALLOCATION_FAILED", `Malformed proxy URL: ${url}`);
|
|
425
|
+
}
|
|
426
|
+
if (scheme !== "http" && scheme !== "socks5") {
|
|
427
|
+
throw new ProxyResolutionError("PROXY_ALLOCATION_FAILED", `Resolved proxy scheme "${scheme}" is not a tunnelling scheme (expected http or socks5). Refusing to route TLS through a non-tunnelling proxy.`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
async function resolveWithVendor(vendor, policy, options, context) {
|
|
431
|
+
if (vendor === "nodemaven") {
|
|
432
|
+
const startedAt = Date.now();
|
|
433
|
+
const synthesized = synthesizeNodemavenProxy({
|
|
434
|
+
policy,
|
|
435
|
+
affinityKey: options.affinityKey,
|
|
436
|
+
protocol: context.protocol,
|
|
437
|
+
poolIndex: context.poolIndex,
|
|
438
|
+
refreshEpoch: context.refreshEpoch,
|
|
439
|
+
country: resolveSmartproxyCountry(policy),
|
|
440
|
+
});
|
|
441
|
+
options.telemetry?.recordProxyResolution({
|
|
442
|
+
provider: "nodemaven",
|
|
443
|
+
protocol: synthesized.protocol,
|
|
444
|
+
cacheStatus: "disabled",
|
|
445
|
+
cacheHit: false,
|
|
446
|
+
resolutionMs: Math.max(0, Date.now() - startedAt),
|
|
447
|
+
attempts: 1,
|
|
448
|
+
});
|
|
449
|
+
assertTunnelingScheme(synthesized.url);
|
|
321
450
|
return {
|
|
322
451
|
shouldWarn: false,
|
|
323
|
-
url:
|
|
324
|
-
source: "
|
|
452
|
+
url: synthesized.url,
|
|
453
|
+
source: "nodemaven-gateway",
|
|
454
|
+
protocol: synthesized.protocol,
|
|
325
455
|
diagnostics: {
|
|
326
|
-
...
|
|
327
|
-
|
|
328
|
-
poolIndex,
|
|
456
|
+
...synthesized.diagnostics,
|
|
457
|
+
poolIndex: context.poolIndex,
|
|
329
458
|
},
|
|
330
459
|
};
|
|
331
460
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
throw error instanceof ProxyResolutionError
|
|
338
|
-
? error
|
|
339
|
-
: new ProxyResolutionError("PROXY_ALLOCATION_FAILED", "Smartproxy allocator failed for required proxy egress.", { cause: error });
|
|
340
|
-
}
|
|
341
|
-
return { shouldWarn: true };
|
|
461
|
+
// smartproxy allocation-style vendor.
|
|
462
|
+
const appKey = process.env[SMARTPROXY_APP_KEY_ENV]?.trim();
|
|
463
|
+
if (!appKey) {
|
|
464
|
+
// Guarded by vendorHasCredentials; treated as a vendor-internal failure.
|
|
465
|
+
throw new ProxyResolutionError("PROXY_ALLOCATION_FAILED", `${SMARTPROXY_APP_KEY_ENV} is not configured.`, { vendor: "smartproxy" });
|
|
342
466
|
}
|
|
467
|
+
const lifetimeMinutes = resolveSmartproxyLifetime(policy);
|
|
468
|
+
const allocated = await allocateSmartproxy(policy, appKey, lifetimeMinutes, options.affinityKey, context.protocol);
|
|
469
|
+
options.telemetry?.recordProxyResolution({ ...allocated.telemetry, protocol: context.protocol });
|
|
470
|
+
const poolIndex = selectProxyPoolIndex(allocated.pool.urls.length, context.poolIndex);
|
|
471
|
+
const url = allocated.pool.urls[poolIndex];
|
|
472
|
+
if (url)
|
|
473
|
+
assertTunnelingScheme(url);
|
|
474
|
+
return {
|
|
475
|
+
shouldWarn: false,
|
|
476
|
+
url,
|
|
477
|
+
source: "smartproxy-allocator",
|
|
478
|
+
protocol: context.protocol,
|
|
479
|
+
diagnostics: {
|
|
480
|
+
...allocated.pool.diagnostics,
|
|
481
|
+
poolSize: allocated.pool.urls.length,
|
|
482
|
+
poolIndex,
|
|
483
|
+
},
|
|
484
|
+
};
|
|
343
485
|
}
|
|
344
486
|
function resolvePolicy(options) {
|
|
345
487
|
if (options.proxyPolicy) {
|
|
@@ -351,8 +493,80 @@ function resolvePolicy(options) {
|
|
|
351
493
|
}
|
|
352
494
|
return undefined;
|
|
353
495
|
}
|
|
354
|
-
function
|
|
355
|
-
return
|
|
496
|
+
function isRegistryVendor(name) {
|
|
497
|
+
return name === "smartproxy" || name === "nodemaven";
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Ordered list of SDK-native proxy vendors declared by the policy. `providers`
|
|
501
|
+
* takes precedence over the legacy singular `provider`; the platform default
|
|
502
|
+
* env is the final fallback. Non-registry names (decodo/custom) are dropped so
|
|
503
|
+
* an all-static chain falls through to the legacy env-URL path unchanged.
|
|
504
|
+
*/
|
|
505
|
+
export function resolveVendorChain(policy) {
|
|
506
|
+
const declared = policy.providers?.length
|
|
507
|
+
? policy.providers
|
|
508
|
+
: [policy.provider ?? envDefaultProvider()];
|
|
509
|
+
const chain = [];
|
|
510
|
+
for (const name of declared) {
|
|
511
|
+
if (isRegistryVendor(name) && !chain.includes(name)) {
|
|
512
|
+
chain.push(name);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return chain;
|
|
516
|
+
}
|
|
517
|
+
function envDefaultProvider() {
|
|
518
|
+
const raw = process.env[DEFAULT_PROXY_PROVIDER_ENV]?.trim().toLowerCase();
|
|
519
|
+
return raw ?? undefined;
|
|
520
|
+
}
|
|
521
|
+
function vendorHasCredentials(vendor) {
|
|
522
|
+
if (vendor === "nodemaven")
|
|
523
|
+
return hasNodemavenCredentials();
|
|
524
|
+
return Boolean(process.env[SMARTPROXY_APP_KEY_ENV]?.trim());
|
|
525
|
+
}
|
|
526
|
+
function missingCredentialEnv(vendor) {
|
|
527
|
+
return vendor === "nodemaven" ? "APIFUSE__PROXY__NODEMAVEN_USERNAME" : SMARTPROXY_APP_KEY_ENV;
|
|
528
|
+
}
|
|
529
|
+
function vendorPoolSize(vendor, policy) {
|
|
530
|
+
return vendor === "nodemaven" ? nodemavenPoolSize(policy) : resolveSmartproxyPoolSize(policy);
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Total attempt span across a policy's vendor chain — the sum of each vendor's
|
|
534
|
+
* pool size. Transports use this so successive attempts rotate a vendor's pool
|
|
535
|
+
* and then fail over to the next vendor via the flat attempt index. With one
|
|
536
|
+
* vendor this equals that vendor's pool size (today's behaviour).
|
|
537
|
+
*/
|
|
538
|
+
export function resolvePolicyProxyPoolSpan(policy) {
|
|
539
|
+
const chain = resolveVendorChain(policy);
|
|
540
|
+
if (chain.length === 0)
|
|
541
|
+
return resolveSmartproxyPoolSize(policy);
|
|
542
|
+
return chain.reduce((sum, vendor) => sum + vendorPoolSize(vendor, policy), 0);
|
|
543
|
+
}
|
|
544
|
+
/** Map a resolved proxy source label to the vendor that served it. */
|
|
545
|
+
export function vendorFromResolvedSource(source) {
|
|
546
|
+
if (source === "nodemaven-gateway")
|
|
547
|
+
return "nodemaven";
|
|
548
|
+
if (source === "smartproxy-allocator")
|
|
549
|
+
return "smartproxy";
|
|
550
|
+
return undefined;
|
|
551
|
+
}
|
|
552
|
+
function normalizeAttemptIndex(attempt) {
|
|
553
|
+
return Number.isFinite(attempt) ? Math.max(0, Math.floor(attempt)) : 0;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Map a flat attempt index into (vendorIndex, poolIndex) by concatenating each
|
|
557
|
+
* vendor's pool space in chain order. With a single vendor this reduces to
|
|
558
|
+
* `attempt % poolSize`, preserving today's behaviour exactly.
|
|
559
|
+
*/
|
|
560
|
+
export function mapFlatAttempt(flat, sizes) {
|
|
561
|
+
let cursor = flat;
|
|
562
|
+
for (let vendorIndex = 0; vendorIndex < sizes.length; vendorIndex++) {
|
|
563
|
+
const size = Math.max(1, sizes[vendorIndex] ?? 1);
|
|
564
|
+
if (cursor < size) {
|
|
565
|
+
return { vendorIndex, poolIndex: cursor };
|
|
566
|
+
}
|
|
567
|
+
cursor -= size;
|
|
568
|
+
}
|
|
569
|
+
return { vendorIndex: 0, poolIndex: 0 };
|
|
356
570
|
}
|
|
357
571
|
function resolveSmartproxyCountry(policy) {
|
|
358
572
|
return (policy.geo?.country ?? process.env[DEFAULT_PROXY_COUNTRY_ENV]?.trim().toUpperCase() ?? undefined);
|
|
@@ -381,10 +595,11 @@ function selectProxyPoolIndex(poolSize, attempt = 0) {
|
|
|
381
595
|
const normalizedAttempt = Number.isFinite(attempt) ? Math.max(0, Math.floor(attempt)) : 0;
|
|
382
596
|
return normalizedAttempt % poolSize;
|
|
383
597
|
}
|
|
384
|
-
function buildSmartproxyCacheKey(policy, affinityKey, lifetimeMinutes) {
|
|
598
|
+
function buildSmartproxyCacheKey(policy, affinityKey, lifetimeMinutes, protocol) {
|
|
385
599
|
const poolSize = resolveSmartproxyPoolSize(policy);
|
|
386
600
|
return JSON.stringify({
|
|
387
601
|
provider: "smartproxy",
|
|
602
|
+
protocol,
|
|
388
603
|
country: resolveSmartproxyCountry(policy),
|
|
389
604
|
affinity: policy.session?.affinity ?? "request",
|
|
390
605
|
affinityKey: (policy.session?.affinity ?? "request") === "request" ? undefined : affinityKey,
|
|
@@ -392,8 +607,8 @@ function buildSmartproxyCacheKey(policy, affinityKey, lifetimeMinutes) {
|
|
|
392
607
|
poolSize,
|
|
393
608
|
});
|
|
394
609
|
}
|
|
395
|
-
async function allocateSmartproxy(policy, appKey, lifetimeMinutes, affinityKey) {
|
|
396
|
-
const cacheKey = buildSmartproxyCacheKey(policy, affinityKey, lifetimeMinutes);
|
|
610
|
+
async function allocateSmartproxy(policy, appKey, lifetimeMinutes, affinityKey, protocol) {
|
|
611
|
+
const cacheKey = buildSmartproxyCacheKey(policy, affinityKey, lifetimeMinutes, protocol);
|
|
397
612
|
const startedAt = Date.now();
|
|
398
613
|
const now = startedAt;
|
|
399
614
|
const invalidatedUntil = invalidatedProxyKeys.get(cacheKey) ?? 0;
|
|
@@ -401,7 +616,7 @@ async function allocateSmartproxy(policy, appKey, lifetimeMinutes, affinityKey)
|
|
|
401
616
|
const cached = proxyCache.get(cacheKey);
|
|
402
617
|
if (!skipCached && cached && isFresh(cached, now)) {
|
|
403
618
|
if (shouldSoftRefresh(cached, now)) {
|
|
404
|
-
void refreshSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes);
|
|
619
|
+
void refreshSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes, protocol);
|
|
405
620
|
return {
|
|
406
621
|
pool: cached,
|
|
407
622
|
telemetry: telemetryForPool(cached, "soft_stale_refresh", startedAt, {
|
|
@@ -429,7 +644,7 @@ async function allocateSmartproxy(policy, appKey, lifetimeMinutes, affinityKey)
|
|
|
429
644
|
}),
|
|
430
645
|
};
|
|
431
646
|
}
|
|
432
|
-
const promise = allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinutes, startedAt).finally(() => {
|
|
647
|
+
const promise = allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinutes, startedAt, protocol).finally(() => {
|
|
433
648
|
proxyInflight.delete(cacheKey);
|
|
434
649
|
});
|
|
435
650
|
proxyInflight.set(cacheKey, promise);
|
|
@@ -460,9 +675,9 @@ async function readSmartproxyRedisPool(cacheKey, startedAt) {
|
|
|
460
675
|
telemetry: telemetryForPool(pool, "redis_hit", startedAt, { redisReadMs }),
|
|
461
676
|
};
|
|
462
677
|
}
|
|
463
|
-
async function refreshSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes) {
|
|
678
|
+
async function refreshSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes, protocol) {
|
|
464
679
|
try {
|
|
465
|
-
await allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinutes, Date.now(), {
|
|
680
|
+
await allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinutes, Date.now(), protocol, {
|
|
466
681
|
background: true,
|
|
467
682
|
});
|
|
468
683
|
}
|
|
@@ -470,10 +685,10 @@ async function refreshSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes)
|
|
|
470
685
|
// Soft refresh is opportunistic; current fresh pool remains usable.
|
|
471
686
|
}
|
|
472
687
|
}
|
|
473
|
-
async function allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinutes, startedAt, options = {}) {
|
|
688
|
+
async function allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinutes, startedAt, protocol, options = {}) {
|
|
474
689
|
const redis = getProxyRedis();
|
|
475
690
|
if (!redis || !(await ensureRedisReady(redis))) {
|
|
476
|
-
return await allocateAndStoreSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes, startedAt, { cacheStatus: "allocator" });
|
|
691
|
+
return await allocateAndStoreSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes, startedAt, { cacheStatus: "allocator", protocol });
|
|
477
692
|
}
|
|
478
693
|
const poolKey = smartproxyRedisPoolKey(cacheKey);
|
|
479
694
|
const lockKey = smartproxyRedisLockKey(cacheKey);
|
|
@@ -487,6 +702,7 @@ async function allocateSmartproxyShared(cacheKey, policy, appKey, lifetimeMinute
|
|
|
487
702
|
cacheStatus: options.background ? "soft_stale_refresh" : "allocator",
|
|
488
703
|
redis,
|
|
489
704
|
poolKey,
|
|
705
|
+
protocol,
|
|
490
706
|
});
|
|
491
707
|
}
|
|
492
708
|
finally {
|
|
@@ -599,7 +815,7 @@ async function readSmartproxyAllocatorBodyWithDeadline(response, signal) {
|
|
|
599
815
|
}
|
|
600
816
|
async function allocateAndStoreSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes, startedAt, options) {
|
|
601
817
|
const poolSize = resolveSmartproxyPoolSize(policy);
|
|
602
|
-
const allocatorUrl = buildSmartproxyAllocatorUrl(policy, appKey, lifetimeMinutes, poolSize);
|
|
818
|
+
const allocatorUrl = buildSmartproxyAllocatorUrl(policy, appKey, lifetimeMinutes, poolSize, options.protocol);
|
|
603
819
|
const allocatorStartedAt = Date.now();
|
|
604
820
|
const allocatorDeadlineAt = allocatorStartedAt + smartproxyAllocatorDeadlineMs();
|
|
605
821
|
let allocation;
|
|
@@ -609,7 +825,7 @@ async function allocateAndStoreSmartproxyPool(cacheKey, policy, appKey, lifetime
|
|
|
609
825
|
lastFailure = smartproxyAllocatorDeadlineFailure(attempt);
|
|
610
826
|
break;
|
|
611
827
|
}
|
|
612
|
-
const attemptResult = await fetchSmartproxyAllocatorAttempt(allocatorUrl, attempt, allocatorDeadlineAt);
|
|
828
|
+
const attemptResult = await fetchSmartproxyAllocatorAttempt(allocatorUrl, attempt, allocatorDeadlineAt, options.protocol);
|
|
613
829
|
if (attemptResult.ok) {
|
|
614
830
|
allocation = attemptResult;
|
|
615
831
|
break;
|
|
@@ -692,7 +908,7 @@ async function allocateAndStoreSmartproxyPool(cacheKey, policy, appKey, lifetime
|
|
|
692
908
|
}),
|
|
693
909
|
};
|
|
694
910
|
}
|
|
695
|
-
async function fetchSmartproxyAllocatorAttempt(allocatorUrl, attempt, deadlineAt) {
|
|
911
|
+
async function fetchSmartproxyAllocatorAttempt(allocatorUrl, attempt, deadlineAt, protocol) {
|
|
696
912
|
const { controller, dispose } = createDeadlineAbortController(deadlineAt);
|
|
697
913
|
let response;
|
|
698
914
|
try {
|
|
@@ -735,7 +951,7 @@ async function fetchSmartproxyAllocatorAttempt(allocatorUrl, attempt, deadlineAt
|
|
|
735
951
|
bodyClass: "http_error",
|
|
736
952
|
};
|
|
737
953
|
}
|
|
738
|
-
const urls = parseSmartproxyAllocatorProxies(body);
|
|
954
|
+
const urls = parseSmartproxyAllocatorProxies(body, protocol);
|
|
739
955
|
const bodyClass = classifySmartproxyAllocatorBody(body, urls);
|
|
740
956
|
if (urls.length === 0) {
|
|
741
957
|
return {
|
|
@@ -770,13 +986,20 @@ function smartproxyAllocatorFailureMessage(failure) {
|
|
|
770
986
|
}
|
|
771
987
|
return "Smartproxy allocator response did not contain a usable proxy endpoint.";
|
|
772
988
|
}
|
|
773
|
-
|
|
989
|
+
// Smartproxy get-ip-v3 `protocol` param: 1 = HTTP. The SOCKS5 value ("2") is a
|
|
990
|
+
// best-effort mapping pending live vendor confirmation; http is the default and
|
|
991
|
+
// the only value exercised in production today.
|
|
992
|
+
const SMARTPROXY_PROTOCOL_PARAM = {
|
|
993
|
+
http: "1",
|
|
994
|
+
socks5: "2",
|
|
995
|
+
};
|
|
996
|
+
function buildSmartproxyAllocatorUrl(policy, appKey, lifetimeMinutes, poolSize, protocol) {
|
|
774
997
|
const params = new URLSearchParams({
|
|
775
998
|
app_key: appKey,
|
|
776
999
|
pt: "9",
|
|
777
1000
|
num: String(poolSize),
|
|
778
1001
|
life: String(lifetimeMinutes),
|
|
779
|
-
protocol:
|
|
1002
|
+
protocol: SMARTPROXY_PROTOCOL_PARAM[protocol],
|
|
780
1003
|
format: "txt",
|
|
781
1004
|
lb: "\\n",
|
|
782
1005
|
});
|
|
@@ -788,7 +1011,8 @@ function buildSmartproxyAllocatorUrl(policy, appKey, lifetimeMinutes, poolSize)
|
|
|
788
1011
|
// old path 404s into the marketing site); the API lives on the api host.
|
|
789
1012
|
return `https://api.smartproxy.org/web_v1/ip/get-ip-v3?${params.toString()}`;
|
|
790
1013
|
}
|
|
791
|
-
function parseSmartproxyAllocatorProxies(body) {
|
|
1014
|
+
function parseSmartproxyAllocatorProxies(body, protocol) {
|
|
1015
|
+
const scheme = protocol === "socks5" ? "socks5" : "http";
|
|
792
1016
|
const trimmed = body.trim();
|
|
793
1017
|
if (!trimmed) {
|
|
794
1018
|
return [];
|
|
@@ -807,7 +1031,7 @@ function parseSmartproxyAllocatorProxies(body) {
|
|
|
807
1031
|
const port = "port" in item && (typeof item.port === "string" || typeof item.port === "number")
|
|
808
1032
|
? item.port
|
|
809
1033
|
: "";
|
|
810
|
-
return ip && port ?
|
|
1034
|
+
return ip && port ? `${scheme}://${ip}:${port}` : null;
|
|
811
1035
|
})
|
|
812
1036
|
.filter((url) => url !== null);
|
|
813
1037
|
}
|
|
@@ -819,7 +1043,7 @@ function parseSmartproxyAllocatorProxies(body) {
|
|
|
819
1043
|
.split(/\r?\n/)
|
|
820
1044
|
.map((item) => item.trim())
|
|
821
1045
|
.filter((item) => /^\d{1,3}(?:\.\d{1,3}){3}:\d{2,5}$/.test(item))
|
|
822
|
-
.map((line) =>
|
|
1046
|
+
.map((line) => `${scheme}://${line}`);
|
|
823
1047
|
}
|
|
824
1048
|
function classifySmartproxyAllocatorBody(body, urls) {
|
|
825
1049
|
if (urls.length > 0) {
|
|
@@ -847,11 +1071,11 @@ function markSmartproxyCacheInvalidated(options = {}) {
|
|
|
847
1071
|
if (!policy || policy.mode === "disabled") {
|
|
848
1072
|
return undefined;
|
|
849
1073
|
}
|
|
850
|
-
if (
|
|
1074
|
+
if (!resolveVendorChain(policy).includes("smartproxy")) {
|
|
851
1075
|
return undefined;
|
|
852
1076
|
}
|
|
853
1077
|
const lifetimeMinutes = resolveSmartproxyLifetime(policy);
|
|
854
|
-
const cacheKey = buildSmartproxyCacheKey(policy, options.affinityKey, lifetimeMinutes);
|
|
1078
|
+
const cacheKey = buildSmartproxyCacheKey(policy, options.affinityKey, lifetimeMinutes, options.protocol ?? VENDOR_DEFAULT_PROTOCOL.smartproxy);
|
|
855
1079
|
invalidatedProxyKeys.set(cacheKey, Date.now() + SMARTPROXY_INVALIDATION_SKIP_REDIS_MS);
|
|
856
1080
|
proxyCache.delete(cacheKey);
|
|
857
1081
|
proxyInflight.delete(cacheKey);
|
package/dist/define.js
CHANGED
|
@@ -8,7 +8,7 @@ const VALID_RUNTIMES = ["standard", "shared", "browser"];
|
|
|
8
8
|
const VALID_AUTH_MODES = ["none", "platform-managed", "credentials", "oauth2"];
|
|
9
9
|
const VALID_PROVIDER_ACCESS_VISIBILITIES = ["public", "early_access"];
|
|
10
10
|
const VALID_PROVIDER_PROXY_MODES = ["disabled", "optional", "required"];
|
|
11
|
-
const VALID_PROVIDER_PROXY_PROVIDERS = ["smartproxy", "decodo", "custom"];
|
|
11
|
+
const VALID_PROVIDER_PROXY_PROVIDERS = ["smartproxy", "nodemaven", "decodo", "custom"];
|
|
12
12
|
const VALID_PROVIDER_PROXY_AFFINITIES = [
|
|
13
13
|
"request",
|
|
14
14
|
"operation",
|
|
@@ -137,11 +137,21 @@ function validateProviderProxy(config) {
|
|
|
137
137
|
fix: `Use proxy: { mode: "required", provider: "smartproxy", geo: { country: "KR" }, session: { affinity: "connection", lifetimeMinutes: 30 } }`,
|
|
138
138
|
});
|
|
139
139
|
}
|
|
140
|
-
rejectUnknownFields(proxy, new Set(["mode", "provider", "geo", "session"]), "proxy");
|
|
140
|
+
rejectUnknownFields(proxy, new Set(["mode", "provider", "providers", "geo", "session"]), "proxy");
|
|
141
141
|
assertLiteralField(proxy.mode, "proxy.mode", VALID_PROVIDER_PROXY_MODES, config.id);
|
|
142
142
|
if (proxy.provider !== undefined) {
|
|
143
143
|
assertLiteralField(proxy.provider, "proxy.provider", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
|
|
144
144
|
}
|
|
145
|
+
if (proxy.providers !== undefined) {
|
|
146
|
+
if (!Array.isArray(proxy.providers) || proxy.providers.length === 0) {
|
|
147
|
+
throw new ValidationError(`Provider "${config.id}" has invalid proxy.providers: must be a non-empty array of proxy vendors.`, {
|
|
148
|
+
fix: `Use proxy.providers: ["smartproxy", "nodemaven"] to declare an ordered fallback chain.`,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
for (const vendor of proxy.providers) {
|
|
152
|
+
assertLiteralField(vendor, "proxy.providers[]", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
145
155
|
if (proxy.geo !== undefined) {
|
|
146
156
|
if (!proxy.geo || typeof proxy.geo !== "object" || Array.isArray(proxy.geo)) {
|
|
147
157
|
throw new ValidationError(`Provider "${config.id}" has invalid proxy.geo: must be an object.`, {
|
|
@@ -178,7 +188,15 @@ function validateProviderProxy(config) {
|
|
|
178
188
|
throw new ValidationError(`Provider "${config.id}" has invalid proxy.session.poolSize: must be a positive integer.`);
|
|
179
189
|
}
|
|
180
190
|
}
|
|
181
|
-
|
|
191
|
+
// Smartproxy uses a provider-declared secret; when it is a required-mode
|
|
192
|
+
// vendor (singular or in the chain) the app key must be declared so a missing
|
|
193
|
+
// credential fails at build/validation time, not during a live outage.
|
|
194
|
+
const vendorChain = proxy.providers && proxy.providers.length > 0
|
|
195
|
+
? proxy.providers
|
|
196
|
+
: proxy.provider
|
|
197
|
+
? [proxy.provider]
|
|
198
|
+
: [];
|
|
199
|
+
if (proxy.mode === "required" && vendorChain.includes("smartproxy")) {
|
|
182
200
|
const hasSmartproxySecret = config.secrets?.some((secret) => secret.name === SMARTPROXY_APP_KEY_SECRET && secret.required !== false);
|
|
183
201
|
if (!hasSmartproxySecret) {
|
|
184
202
|
throw new ValidationError(`Provider "${config.id}" requires Smartproxy egress but does not declare ${SMARTPROXY_APP_KEY_SECRET}.`, {
|
|
@@ -186,6 +204,12 @@ function validateProviderProxy(config) {
|
|
|
186
204
|
});
|
|
187
205
|
}
|
|
188
206
|
}
|
|
207
|
+
// `decodo`/`custom` are deprecated vendor values (string-union members, so the
|
|
208
|
+
// @deprecated symbol gate can't catch them — warn at validation time instead).
|
|
209
|
+
const deprecatedVendors = vendorChain.filter((vendor) => vendor === "decodo" || vendor === "custom");
|
|
210
|
+
if (deprecatedVendors.length > 0) {
|
|
211
|
+
console.warn(`[provider-sdk] Provider "${config.id}" uses deprecated proxy vendor(s): ${deprecatedVendors.join(", ")}. Use "smartproxy"/"nodemaven", or the APIFUSE__PROXY__URL bring-your-own escape hatch.`);
|
|
212
|
+
}
|
|
189
213
|
}
|
|
190
214
|
function validateProviderStt(config) {
|
|
191
215
|
const stt = config.stt;
|
package/dist/index.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export { generateInsights } from "./runtime/insights.js";
|
|
|
27
27
|
export { type InstrumentationOptions, type InstrumentedProviderContext, wrapWithInstrumentation, } from "./runtime/instrumentation.js";
|
|
28
28
|
export { type PrevalidateResult, prevalidate } from "./runtime/prevalidate.js";
|
|
29
29
|
export { getProviderBaseUrl } from "./runtime/provider.js";
|
|
30
|
+
export { assertRequiredSecretsPresent, listMissingRequiredSecrets, MISSING_SECRET_CODE, } from "./runtime/secrets.js";
|
|
30
31
|
export { createUnsupportedProviderRuntimeState, UnsupportedProviderStateError, } from "./runtime/state.js";
|
|
31
32
|
export { createStealthClient } from "./runtime/stealth.js";
|
|
32
33
|
export { APIFUSE__STT__BACKEND_ENV, APIFUSE__STT__CLOUDFLARE_API_TOKEN_ENV, APIFUSE__STT__MODEL_ENV, createSttClientFromEnv, createUnsupportedSttClient, extractVerificationCode, resolveSttPrompt, } from "./runtime/stt.js";
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ export { generateInsights } from "./runtime/insights.js";
|
|
|
24
24
|
export { wrapWithInstrumentation, } from "./runtime/instrumentation.js";
|
|
25
25
|
export { prevalidate } from "./runtime/prevalidate.js";
|
|
26
26
|
export { getProviderBaseUrl } from "./runtime/provider.js";
|
|
27
|
+
export { assertRequiredSecretsPresent, listMissingRequiredSecrets, MISSING_SECRET_CODE, } from "./runtime/secrets.js";
|
|
27
28
|
export { createUnsupportedProviderRuntimeState, UnsupportedProviderStateError, } from "./runtime/state.js";
|
|
28
29
|
export { createStealthClient } from "./runtime/stealth.js";
|
|
29
30
|
export { APIFUSE__STT__BACKEND_ENV, APIFUSE__STT__CLOUDFLARE_API_TOKEN_ENV, APIFUSE__STT__MODEL_ENV, createSttClientFromEnv, createUnsupportedSttClient, extractVerificationCode, resolveSttPrompt, } from "./runtime/stt.js";
|
package/dist/runtime/executor.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isSessionExpiredError, ProviderError, SessionExpiredError } from "../errors.js";
|
|
2
2
|
import { parseSchema } from "../schema.js";
|
|
3
|
+
import { assertRequiredSecretsPresent } from "./secrets.js";
|
|
3
4
|
export function isStreamingOperation(provider, operationId) {
|
|
4
5
|
const kind = provider.operations[operationId]?.transport?.kind ?? "json";
|
|
5
6
|
return kind !== "json";
|
|
@@ -23,6 +24,12 @@ export async function executeOperation(provider, operationId, ctx, input, _optio
|
|
|
23
24
|
fix: `Valid operations: ${Object.keys(provider.operations).join(", ")}`,
|
|
24
25
|
});
|
|
25
26
|
}
|
|
27
|
+
// SDK-owned secret presence gate (single source of truth): declared
|
|
28
|
+
// `required: true` secrets are validated here, before input parsing and the
|
|
29
|
+
// handler, so every invocation path (serve /v1, self-test probes, perf,
|
|
30
|
+
// record) fails with the same structured MISSING_SECRET error instead of a
|
|
31
|
+
// handler-specific crash. Providers must not re-check presence locally.
|
|
32
|
+
assertRequiredSecretsPresent(provider, ctx.env);
|
|
26
33
|
const validatedInput = await parseSchema(operation.input, input, `operations.${operationId}.input`);
|
|
27
34
|
const execute = () => ctx.trace.span(`handler:${operationId}`, () => Promise.resolve(operation.handler(ctx, validatedInput)));
|
|
28
35
|
let result;
|
package/dist/runtime/http.js
CHANGED
|
@@ -175,6 +175,9 @@ async function resolveNativeProxy(options, clientOptions, warn, proxyAttemptOffs
|
|
|
175
175
|
baseProxyAttempt: clientOptions.proxyAttempt,
|
|
176
176
|
retryAttemptOffset: proxyAttemptOffset,
|
|
177
177
|
}),
|
|
178
|
+
// Bun's native fetch proxy option tunnels HTTP CONNECT only; SOCKS5 is not
|
|
179
|
+
// supported here, so a socks5 policy fails loudly rather than downgrading.
|
|
180
|
+
transportProtocols: ["http"],
|
|
178
181
|
telemetry: clientOptions.telemetry,
|
|
179
182
|
});
|
|
180
183
|
if (resolvedProxy.shouldWarn) {
|
|
@@ -11,8 +11,12 @@ const PROXY_POOL_STALE_STATUS_CODES = new Set([509, 512]);
|
|
|
11
11
|
const PROXY_EDGE_TLS_REJECTED_STATUS_CODES = new Set([495]);
|
|
12
12
|
const PROXY_AUTH_IP_DENIED_PATTERN = /\b(?:source|egress|client)\s+ip\b.{0,120}\b(?:deny|denied|unauthori[sz]ed|not\s+authori[sz]ed|white\s*list|allow\s*list)\b|\b(?:white\s*list|allow\s*list)\b.{0,120}\b(?:source|egress|client)\s+ip\b/i;
|
|
13
13
|
const PROXY_EDGE_AUTH_REJECTED_PATTERN = /\bauth\s+ip\s+err\b|\bproxy\b.{0,120}\bauth(?:entication)?\b.{0,120}\b(?:reject(?:ed)?|fail(?:ed)?|invalid|den(?:y|ied)|unauthori[sz]ed)\b|\bauth(?:entication)?\b.{0,120}\b(?:reject(?:ed)?|fail(?:ed)?|invalid|den(?:y|ied)|unauthori[sz]ed)\b.{0,120}\bproxy\b/i;
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
// Vendor host tokens that can appear in upstream error strings. Adding a proxy
|
|
15
|
+
// vendor updates every classifier below in one place. `proxy` is the generic
|
|
16
|
+
// fallback so vendor-agnostic messages still classify.
|
|
17
|
+
const PROXY_VENDOR_ALTERNATION = "smartproxy|nodemaven|proxy";
|
|
18
|
+
const PROXY_POOL_STALE_MESSAGE_PATTERN = new RegExp(`\\bproxy\\b.{0,120}\\b(?:pool|lease|expired|unavailable|exhausted|non[\\s-]?200\\s+code:\\s*(?:509|512))\\b|\\bnon[\\s-]?200\\s+code:\\s*(?:509|512)\\b.{0,120}\\bproxy\\b|\\b(?:${PROXY_VENDOR_ALTERNATION})\\b.{0,120}\\b(?:509|512)\\b`, "i");
|
|
19
|
+
const PROXY_EDGE_TLS_REJECTED_MESSAGE_PATTERN = new RegExp(`\\b(?:${PROXY_VENDOR_ALTERNATION})\\b.{0,160}\\b(?:495|ssl|tls|cert(?:ificate)?|handshake|edge|connect|non[\\s-]?200)\\b|\\b(?:495|ssl|tls|cert(?:ificate)?|handshake|edge|connect|non[\\s-]?200)\\b.{0,160}\\b(?:${PROXY_VENDOR_ALTERNATION})\\b`, "i");
|
|
16
20
|
export function isProxyAuthIpDeniedMessage(message) {
|
|
17
21
|
return PROXY_AUTH_IP_DENIED_PATTERN.test(message);
|
|
18
22
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ProviderProxyPolicy } from "../types.js";
|
|
2
|
+
export declare const NODEMAVEN_USERNAME_ENV = "APIFUSE__PROXY__NODEMAVEN_USERNAME";
|
|
3
|
+
export declare const NODEMAVEN_PASSWORD_ENV = "APIFUSE__PROXY__NODEMAVEN_PASSWORD";
|
|
4
|
+
export declare const NODEMAVEN_FILTER_ENV = "APIFUSE__PROXY__NODEMAVEN_FILTER";
|
|
5
|
+
export declare const NODEMAVEN_GATEWAY_HOST = "gate.nodemaven.com";
|
|
6
|
+
/** Both schemes tunnel bytes end-to-end, preserving the client TLS handshake. */
|
|
7
|
+
export type ProxyProtocol = "http" | "socks5";
|
|
8
|
+
/**
|
|
9
|
+
* NodeMaven's fastest protocol: HTTP CONNECT. Benchmarks (KR, cold + warm)
|
|
10
|
+
* showed socks5 through the gateway adds ~500ms per request over http, so
|
|
11
|
+
* NodeMaven never defaults to socks5.
|
|
12
|
+
*/
|
|
13
|
+
export declare const NODEMAVEN_DEFAULT_PROTOCOL: ProxyProtocol;
|
|
14
|
+
export declare function hasNodemavenCredentials(): boolean;
|
|
15
|
+
export declare function nodemavenPoolSize(policy: ProviderProxyPolicy): number;
|
|
16
|
+
export type NodemavenSynthesisInput = {
|
|
17
|
+
policy: ProviderProxyPolicy;
|
|
18
|
+
affinityKey: string | undefined;
|
|
19
|
+
protocol: ProxyProtocol;
|
|
20
|
+
poolIndex: number;
|
|
21
|
+
refreshEpoch: number;
|
|
22
|
+
/** ISO 3166-1 alpha-2, already resolved by the caller (falls back to env). */
|
|
23
|
+
country?: string;
|
|
24
|
+
};
|
|
25
|
+
export type NodemavenSynthesis = {
|
|
26
|
+
url: string;
|
|
27
|
+
protocol: ProxyProtocol;
|
|
28
|
+
diagnostics: Record<string, string | number | boolean>;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Synthesize a NodeMaven gateway proxy URL locally from static credentials.
|
|
32
|
+
* There is no allocation API — geo/session are encoded in the username.
|
|
33
|
+
*/
|
|
34
|
+
export declare function synthesizeNodemavenProxy(input: NodemavenSynthesisInput): NodemavenSynthesis;
|