@apifuse/provider-sdk 2.2.0-beta.1 → 2.2.0-beta.3
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 +9 -0
- package/bin/apifuse-submit-check.ts +48 -10
- package/bin/submit-check-xml-semantics.ts +204 -0
- package/bin/submit-check-xml.ts +134 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.js +50 -0
- package/dist/provider.d.ts +1 -1
- package/dist/provider.js +1 -1
- package/dist/runtime/executor.js +7 -2
- package/dist/runtime/http.js +28 -338
- package/dist/runtime/proxy-retry-policy.d.ts +40 -0
- package/dist/runtime/proxy-retry-policy.js +326 -0
- package/dist/runtime/stealth.js +57 -203
- package/dist/server/serve.js +20 -12
- package/dist/server/types.d.ts +1 -0
- package/dist/server/types.js +1 -0
- package/package.json +2 -1
- package/src/errors.ts +60 -0
- package/src/provider.ts +3 -0
- package/src/runtime/executor.ts +7 -2
- package/src/runtime/http.ts +60 -547
- package/src/runtime/proxy-retry-policy.ts +469 -0
- package/src/runtime/stealth.ts +100 -361
- package/src/server/serve.ts +26 -14
- package/src/server/types.ts +1 -0
package/dist/runtime/stealth.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { Impit } from "impit";
|
|
3
3
|
import { DEFAULT_SMARTPROXY_POOL_SIZE, invalidateProxyResolutionCacheAsync, ProxyResolutionError, resolveProxyConfigAsync, SMARTPROXY_MAX_POOL_SIZE, } from "../config/loader";
|
|
4
|
-
import {
|
|
4
|
+
import { SDKError, TransportError } from "../errors";
|
|
5
5
|
import { getStealthProfile } from "../stealth/profiles";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { createProxyAuthIpDeniedError, createProxyEdgeAuthRejectedError, createProxyEdgeTlsRejectedError, createProxyPoolExhaustedError, createProxyPoolStaleError, isProxyAuthIpDeniedMessage, isProxyEdgeAuthRejectedMessage, isProxyEdgeTlsRejectedResponse, isProxyPoolRefreshableError, isProxyPoolStaleMessage, isProxyPoolStaleStatus, PROXY_EDGE_AUTH_REJECTED_CODE, PROXY_POOL_STALE_CODE, } from "./proxy-errors";
|
|
7
|
+
import { computeProxyAttemptIndex, computeProxyTransportRetryDelayMs, createDefaultProxyTransportRetryOptions, normalizeProxyTransportRetryOptions, shouldRetryProxyTransportAttempt, validateUnsafeProxyTransportRetryMethods, } from "./proxy-retry-policy";
|
|
8
8
|
import { appendQueryParams } from "./request-options";
|
|
9
9
|
const DEFAULT_PROFILE = "chrome-146";
|
|
10
10
|
const MISSING_PROXY_WARNING = "[provider-sdk] Provider requested proxy routing, but no proxy URL was configured. Continuing without proxy.";
|
|
@@ -14,31 +14,7 @@ const PROXY_CONNECT_FAILURE_CODE = "proxy_connect_failed";
|
|
|
14
14
|
const PROXY_CONNECT_FAILURE_BODY_PATTERN = /\bproxy\b.*\b(non[\s-]?200|connect|tunnel)|\bconnect\b.*\bproxy\b|\btunnel\b/i;
|
|
15
15
|
const PROXY_AUTH_DIAGNOSTIC_URL = "http://example.com/";
|
|
16
16
|
const PROXY_AUTH_DIAGNOSTIC_TIMEOUT_MS = 5_000;
|
|
17
|
-
const
|
|
18
|
-
const DEFAULT_STEALTH_RETRY_ERROR_CODES = [
|
|
19
|
-
PROXY_CONNECT_FAILURE_CODE,
|
|
20
|
-
"transport_network_error",
|
|
21
|
-
"transport_timeout",
|
|
22
|
-
];
|
|
23
|
-
const RATE_LIMIT_STEALTH_RETRY_ERROR_CODES = ["transport_timeout"];
|
|
24
|
-
const KNOWN_STEALTH_RETRY_METHODS = new Set([
|
|
25
|
-
"GET",
|
|
26
|
-
"HEAD",
|
|
27
|
-
"POST",
|
|
28
|
-
"PUT",
|
|
29
|
-
"DELETE",
|
|
30
|
-
"OPTIONS",
|
|
31
|
-
"TRACE",
|
|
32
|
-
"PATCH",
|
|
33
|
-
]);
|
|
34
|
-
const UNSAFE_STEALTH_RETRY_METHODS = new Set([
|
|
35
|
-
"POST",
|
|
36
|
-
"PUT",
|
|
37
|
-
"PATCH",
|
|
38
|
-
"DELETE",
|
|
39
|
-
"TRACE",
|
|
40
|
-
]);
|
|
41
|
-
const MAX_STEALTH_RETRY_ATTEMPTS = 8;
|
|
17
|
+
const STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES = [PROXY_CONNECT_FAILURE_CODE];
|
|
42
18
|
const REMOVED_CHROME_PROFILE_NAMES = new Set([
|
|
43
19
|
"chrome-120",
|
|
44
20
|
"chrome-124",
|
|
@@ -303,130 +279,8 @@ function isPolicyManagedProxy(options) {
|
|
|
303
279
|
const policy = options.proxyPolicy ?? options.upstream?.proxy;
|
|
304
280
|
return Boolean(policy && typeof policy === "object");
|
|
305
281
|
}
|
|
306
|
-
function isRetrySafeStealthMethod(method) {
|
|
307
|
-
return method === "GET" || method === "HEAD" || method === "OPTIONS";
|
|
308
|
-
}
|
|
309
|
-
function createStealthRetryOptions(preset) {
|
|
310
|
-
switch (preset) {
|
|
311
|
-
case HttpRetryPreset.Off:
|
|
312
|
-
return {
|
|
313
|
-
attempts: 1,
|
|
314
|
-
methods: DEFAULT_STEALTH_RETRY_METHODS,
|
|
315
|
-
errorCodes: DEFAULT_STEALTH_RETRY_ERROR_CODES,
|
|
316
|
-
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
317
|
-
};
|
|
318
|
-
case HttpRetryPreset.AggressiveRead:
|
|
319
|
-
return {
|
|
320
|
-
attempts: 4,
|
|
321
|
-
methods: DEFAULT_STEALTH_RETRY_METHODS,
|
|
322
|
-
errorCodes: DEFAULT_STEALTH_RETRY_ERROR_CODES,
|
|
323
|
-
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
324
|
-
};
|
|
325
|
-
case HttpRetryPreset.RateLimitAware:
|
|
326
|
-
return {
|
|
327
|
-
attempts: 3,
|
|
328
|
-
methods: DEFAULT_STEALTH_RETRY_METHODS,
|
|
329
|
-
errorCodes: RATE_LIMIT_STEALTH_RETRY_ERROR_CODES,
|
|
330
|
-
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
331
|
-
};
|
|
332
|
-
case HttpRetryPreset.SafeRead:
|
|
333
|
-
case HttpRetryPreset.TransportTransient:
|
|
334
|
-
return {
|
|
335
|
-
attempts: 3,
|
|
336
|
-
methods: DEFAULT_STEALTH_RETRY_METHODS,
|
|
337
|
-
errorCodes: DEFAULT_STEALTH_RETRY_ERROR_CODES,
|
|
338
|
-
unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
throw new ProviderError(`Unknown stealth retry preset: ${preset}`, {
|
|
342
|
-
code: "retry_invalid_policy",
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
function normalizeStealthRetryOptions(retry) {
|
|
346
|
-
if (retry === undefined)
|
|
347
|
-
return undefined;
|
|
348
|
-
if (retry === false)
|
|
349
|
-
return createStealthRetryOptions(HttpRetryPreset.Off);
|
|
350
|
-
if (retry === true)
|
|
351
|
-
return createStealthRetryOptions(HttpRetryPreset.TransportTransient);
|
|
352
|
-
if (typeof retry === "string") {
|
|
353
|
-
if (!Object.values(HttpRetryPreset).includes(retry)) {
|
|
354
|
-
throw new ProviderError(`Unknown stealth retry preset: ${retry}`, {
|
|
355
|
-
code: "retry_invalid_policy",
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
return createStealthRetryOptions(retry);
|
|
359
|
-
}
|
|
360
|
-
if (typeof retry !== "object" || retry === null || Array.isArray(retry)) {
|
|
361
|
-
throw new ProviderError("Stealth retry policy must be a plain object", {
|
|
362
|
-
code: "retry_invalid_policy",
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
if (retry.unsafeMethodPolicy !== undefined &&
|
|
366
|
-
!Object.values(HttpRetryUnsafeMethodPolicy).includes(retry.unsafeMethodPolicy)) {
|
|
367
|
-
throw new ProviderError(`Unknown stealth retry unsafe method policy: ${String(retry.unsafeMethodPolicy)}`, { code: "retry_invalid_policy" });
|
|
368
|
-
}
|
|
369
|
-
if (retry.methods !== undefined) {
|
|
370
|
-
if (!Array.isArray(retry.methods)) {
|
|
371
|
-
throw new ProviderError("Stealth retry methods must be an array", {
|
|
372
|
-
code: "retry_invalid_policy",
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
const unknownMethods = retry.methods
|
|
376
|
-
.map((method) => (typeof method === "string" ? method.toUpperCase() : ""))
|
|
377
|
-
.filter((method) => !KNOWN_STEALTH_RETRY_METHODS.has(method));
|
|
378
|
-
if (unknownMethods.length > 0) {
|
|
379
|
-
throw new ProviderError(`Unknown stealth retry method(s): ${unknownMethods.join(", ")}`, { code: "retry_invalid_policy" });
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
if (retry.errorCodes !== undefined) {
|
|
383
|
-
if (!Array.isArray(retry.errorCodes) ||
|
|
384
|
-
retry.errorCodes.some((errorCode) => typeof errorCode !== "string")) {
|
|
385
|
-
throw new ProviderError("Stealth retry errorCodes must contain only strings", { code: "retry_invalid_policy" });
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
const base = createStealthRetryOptions(retry.preset ?? HttpRetryPreset.TransportTransient);
|
|
389
|
-
const attempts = retry.attempts === undefined || !Number.isFinite(retry.attempts)
|
|
390
|
-
? base.attempts
|
|
391
|
-
: Math.max(1, Math.min(MAX_STEALTH_RETRY_ATTEMPTS, Math.floor(retry.attempts)));
|
|
392
|
-
const normalized = {
|
|
393
|
-
attempts,
|
|
394
|
-
methods: retry.methods?.map((method) => method.toUpperCase()) ?? base.methods,
|
|
395
|
-
errorCodes: retry.errorCodes ?? base.errorCodes,
|
|
396
|
-
unsafeMethodPolicy: retry.unsafeMethodPolicy ?? base.unsafeMethodPolicy,
|
|
397
|
-
};
|
|
398
|
-
if (normalized.unsafeMethodPolicy !==
|
|
399
|
-
HttpRetryUnsafeMethodPolicy.AllowExplicitUnsafe) {
|
|
400
|
-
const unsafeMethods = normalized.methods.filter((method) => UNSAFE_STEALTH_RETRY_METHODS.has(method.toUpperCase()));
|
|
401
|
-
if (unsafeMethods.length > 0) {
|
|
402
|
-
throw new ProviderError(`Stealth retry methods include unsafe method(s): ${unsafeMethods.join(", ")}`, { code: "retry_unsafe_method" });
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
return normalized;
|
|
406
|
-
}
|
|
407
|
-
function isExplicitStealthRetryAllowed(method, error, retryOptions) {
|
|
408
|
-
if (!retryOptions || retryOptions.attempts <= 1)
|
|
409
|
-
return false;
|
|
410
|
-
return (retryOptions.methods.includes(method.toUpperCase()) &&
|
|
411
|
-
retryOptions.errorCodes.includes(proxyAttemptErrorCode(error)));
|
|
412
|
-
}
|
|
413
|
-
function isRetryableProxyTransportError(error) {
|
|
414
|
-
if (error instanceof TransportError) {
|
|
415
|
-
if (error.code === PROXY_AUTH_IP_DENIED_CODE) {
|
|
416
|
-
return false;
|
|
417
|
-
}
|
|
418
|
-
return (error.code === PROXY_CONNECT_FAILURE_CODE ||
|
|
419
|
-
error.code === "transport_network_error" ||
|
|
420
|
-
error.code === "transport_timeout");
|
|
421
|
-
}
|
|
422
|
-
if (error instanceof SDKError) {
|
|
423
|
-
return false;
|
|
424
|
-
}
|
|
425
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
426
|
-
return /\bproxy\b|\bnon[\s-]?200\b|\bconnect\b|\btunnel\b/i.test(message);
|
|
427
|
-
}
|
|
428
282
|
function isProxyConnectFailureResponse(response, body) {
|
|
429
|
-
return
|
|
283
|
+
return response.status === 0 && PROXY_CONNECT_FAILURE_BODY_PATTERN.test(body ?? "");
|
|
430
284
|
}
|
|
431
285
|
function createProxyConnectFailureError(body, cause) {
|
|
432
286
|
const bodyExcerpt = (body ?? "").trim().slice(0, 1_000);
|
|
@@ -529,8 +383,7 @@ function normalizeStealthTransportError(error) {
|
|
|
529
383
|
return createProxyEdgeAuthRejectedError(error instanceof Error ? error : undefined);
|
|
530
384
|
}
|
|
531
385
|
const proxyTunnelStatus = getProxyTunnelStatus(error);
|
|
532
|
-
if (proxyTunnelStatus !== undefined &&
|
|
533
|
-
isProxyPoolStaleStatus(proxyTunnelStatus)) {
|
|
386
|
+
if (proxyTunnelStatus !== undefined && isProxyPoolStaleStatus(proxyTunnelStatus)) {
|
|
534
387
|
return createProxyPoolStaleError(proxyTunnelStatus, error instanceof Error ? error : undefined);
|
|
535
388
|
}
|
|
536
389
|
if (PROXY_CONNECT_FAILURE_BODY_PATTERN.test(message)) {
|
|
@@ -542,6 +395,9 @@ function normalizeStealthTransportError(error) {
|
|
|
542
395
|
cause: error instanceof Error ? error : undefined,
|
|
543
396
|
});
|
|
544
397
|
}
|
|
398
|
+
function sleep(ms) {
|
|
399
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
400
|
+
}
|
|
545
401
|
function normalizeMethod(method) {
|
|
546
402
|
switch (method.toUpperCase()) {
|
|
547
403
|
case "HEAD":
|
|
@@ -608,18 +464,16 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
608
464
|
return client;
|
|
609
465
|
}
|
|
610
466
|
async function resolveRequestProxy(options, proxyAttempt) {
|
|
611
|
-
const rawProxyAttemptOffset = options?.proxyAttemptOffset ?? 0;
|
|
612
|
-
const proxyAttemptOffset = Number.isFinite(rawProxyAttemptOffset)
|
|
613
|
-
? Math.max(0, Math.floor(rawProxyAttemptOffset))
|
|
614
|
-
: 0;
|
|
615
467
|
const resolvedProxy = await resolveProxyConfigAsync({
|
|
616
468
|
proxy: options?.proxy ?? clientOptions.proxy,
|
|
617
469
|
upstream: clientOptions.upstream,
|
|
618
470
|
apifuseConfig: clientOptions.apifuseConfig,
|
|
619
471
|
affinityKey: clientOptions.affinityKey,
|
|
620
|
-
proxyAttempt:
|
|
621
|
-
|
|
622
|
-
: proxyAttemptOffset
|
|
472
|
+
proxyAttempt: computeProxyAttemptIndex({
|
|
473
|
+
baseProxyAttempt: clientOptions.proxyAttempt,
|
|
474
|
+
proxyAttemptOffset: options?.proxyAttemptOffset,
|
|
475
|
+
retryAttemptOffset: proxyAttempt,
|
|
476
|
+
}),
|
|
623
477
|
telemetry: clientOptions.telemetry,
|
|
624
478
|
});
|
|
625
479
|
if (resolvedProxy.shouldWarn && !hasWarnedMissingProxy) {
|
|
@@ -636,16 +490,28 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
636
490
|
async fetch(url, options = {}) {
|
|
637
491
|
const method = normalizeMethod(options.method ?? "GET");
|
|
638
492
|
const hasExplicitRetryPolicy = options.retry !== undefined;
|
|
639
|
-
const stealthRetryOptions =
|
|
493
|
+
const stealthRetryOptions = normalizeProxyTransportRetryOptions(options.retry, {
|
|
494
|
+
extraErrorCodes: STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES,
|
|
495
|
+
label: "Stealth",
|
|
496
|
+
}) ??
|
|
497
|
+
(hasExplicitRetryPolicy
|
|
498
|
+
? undefined
|
|
499
|
+
: createDefaultProxyTransportRetryOptions({
|
|
500
|
+
extraErrorCodes: STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES,
|
|
501
|
+
label: "Stealth",
|
|
502
|
+
}));
|
|
503
|
+
if (stealthRetryOptions) {
|
|
504
|
+
validateUnsafeProxyTransportRetryMethods(stealthRetryOptions, "Stealth");
|
|
505
|
+
}
|
|
640
506
|
const hasPolicyProxy = isPolicyManagedProxy(clientOptions);
|
|
641
507
|
const usesPolicyAllocator = hasPolicyProxy && !options.proxy && !clientOptions.proxy;
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
508
|
+
const retryAttemptCap = Math.max(1, stealthRetryOptions?.attempts ?? 1);
|
|
509
|
+
const policyProxyAttemptCap = Math.max(1, Math.min(MAX_POLICY_PROXY_RETRY_ATTEMPTS, clientOptions.proxyPolicy?.session?.poolSize ??
|
|
510
|
+
(typeof clientOptions.upstream?.proxy === "object"
|
|
511
|
+
? clientOptions.upstream.proxy.session?.poolSize
|
|
512
|
+
: undefined) ??
|
|
513
|
+
DEFAULT_SMARTPROXY_POOL_SIZE));
|
|
514
|
+
const maxAttempts = usesPolicyAllocator ? policyProxyAttemptCap : retryAttemptCap;
|
|
649
515
|
let lastError;
|
|
650
516
|
for (let refreshAttempt = 0; refreshAttempt <= MAX_POLICY_PROXY_POOL_REFRESHES; refreshAttempt += 1) {
|
|
651
517
|
let stalePoolError;
|
|
@@ -666,9 +532,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
666
532
|
...(attemptProxy?.poolIndex === undefined
|
|
667
533
|
? {}
|
|
668
534
|
: { poolIndex: attemptProxy.poolIndex }),
|
|
669
|
-
...(attemptProxy?.proxyHash
|
|
670
|
-
? { proxyHash: attemptProxy.proxyHash }
|
|
671
|
-
: {}),
|
|
535
|
+
...(attemptProxy?.proxyHash ? { proxyHash: attemptProxy.proxyHash } : {}),
|
|
672
536
|
outcome,
|
|
673
537
|
...(errorCode ? { errorCode } : {}),
|
|
674
538
|
...(status === undefined ? {} : { status }),
|
|
@@ -679,16 +543,14 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
679
543
|
assertNoUnsupportedFingerprintOverrides(options);
|
|
680
544
|
attemptProxy = await resolveRequestProxy(options, attempt);
|
|
681
545
|
proxy = attemptProxy.url;
|
|
682
|
-
if (proxy) {
|
|
546
|
+
if (proxy && usesPolicyAllocator) {
|
|
683
547
|
if (attemptedProxies.has(proxy)) {
|
|
684
548
|
break;
|
|
685
549
|
}
|
|
686
550
|
attemptedProxies.add(proxy);
|
|
687
551
|
}
|
|
688
552
|
const ignoreTlsErrors = Boolean(options.stealth?.insecureSkipVerify ??
|
|
689
|
-
(!hasPolicyProxy &&
|
|
690
|
-
proxy &&
|
|
691
|
-
clientOptions.proxyStealth?.insecureSkipVerify));
|
|
553
|
+
(!hasPolicyProxy && proxy && clientOptions.proxyStealth?.insecureSkipVerify));
|
|
692
554
|
const profileName = options.profile ?? defaultProfile;
|
|
693
555
|
const requestUrl = appendQueryParams(resolveUrl(baseUrl, url), options.params);
|
|
694
556
|
const headers = { ...(options.headers ?? {}) };
|
|
@@ -709,17 +571,13 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
709
571
|
const response = await getClient(profileName, proxy, ignoreTlsErrors).fetch(requestUrl, requestInit);
|
|
710
572
|
const normalized = await normalizeResponse(response, requestUrl);
|
|
711
573
|
cookieJar.setFromCookieStrings(setCookieHeadersFromResponse(response.headers));
|
|
712
|
-
if (proxy &&
|
|
713
|
-
isProxyConnectFailureResponse(response, normalized.body)) {
|
|
574
|
+
if (proxy && isProxyConnectFailureResponse(response, normalized.body)) {
|
|
714
575
|
throw createProxyConnectFailureError(normalized.body);
|
|
715
576
|
}
|
|
716
577
|
if (response.status >= 400) {
|
|
717
578
|
if (proxy &&
|
|
718
579
|
usesPolicyAllocator &&
|
|
719
|
-
isProxyEdgeTlsRejectedResponse(response.status, [
|
|
720
|
-
JSON.stringify(responseHeadersToRecord(response.headers)),
|
|
721
|
-
normalized.body,
|
|
722
|
-
].join("\n"))) {
|
|
580
|
+
isProxyEdgeTlsRejectedResponse(response.status, [JSON.stringify(responseHeadersToRecord(response.headers)), normalized.body].join("\n"))) {
|
|
723
581
|
throw createProxyEdgeTlsRejectedError(response.status);
|
|
724
582
|
}
|
|
725
583
|
if (proxy && isProxyAuthIpDeniedMessage(normalized.body)) {
|
|
@@ -747,9 +605,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
747
605
|
const normalizedError = normalizeStealthTransportError(error);
|
|
748
606
|
recordProxyAttempt("error", proxyAttemptErrorCode(normalizedError), proxyAttemptStatus(normalizedError));
|
|
749
607
|
lastError = normalizedError;
|
|
750
|
-
if (proxy &&
|
|
751
|
-
usesPolicyAllocator &&
|
|
752
|
-
isProxyPoolRefreshableError(normalizedError)) {
|
|
608
|
+
if (proxy && usesPolicyAllocator && isProxyPoolRefreshableError(normalizedError)) {
|
|
753
609
|
stalePoolError = normalizedError;
|
|
754
610
|
if (shouldRunProxyAuthDiagnostic(normalizedError)) {
|
|
755
611
|
stalePoolDiagnosticProxy = proxy;
|
|
@@ -759,15 +615,20 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
759
615
|
}
|
|
760
616
|
break;
|
|
761
617
|
}
|
|
762
|
-
if (
|
|
763
|
-
|
|
764
|
-
(stealthRetryOptions
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
618
|
+
if (attempt + 1 <
|
|
619
|
+
(stealthRetryOptions
|
|
620
|
+
? Math.min(maxAttempts, stealthRetryOptions.attempts)
|
|
621
|
+
: maxAttempts) &&
|
|
622
|
+
shouldRetryProxyTransportAttempt({
|
|
623
|
+
error: normalizedError,
|
|
624
|
+
explicitRetry: hasExplicitRetryPolicy,
|
|
625
|
+
method,
|
|
626
|
+
options: stealthRetryOptions,
|
|
627
|
+
proxyUsed: Boolean(proxy),
|
|
628
|
+
})) {
|
|
629
|
+
if (stealthRetryOptions) {
|
|
630
|
+
await sleep(computeProxyTransportRetryDelayMs(stealthRetryOptions, attempt + 1));
|
|
631
|
+
}
|
|
771
632
|
continue;
|
|
772
633
|
}
|
|
773
634
|
throw normalizedError;
|
|
@@ -920,10 +781,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
920
781
|
}
|
|
921
782
|
catch (error) {
|
|
922
783
|
const message = error instanceof Error
|
|
923
|
-
? [
|
|
924
|
-
error.message,
|
|
925
|
-
error.cause instanceof Error ? error.cause.message : "",
|
|
926
|
-
]
|
|
784
|
+
? [error.message, error.cause instanceof Error ? error.cause.message : ""]
|
|
927
785
|
.filter(Boolean)
|
|
928
786
|
.join(" ")
|
|
929
787
|
: String(error);
|
|
@@ -951,12 +809,8 @@ function hasHeader(headers, name) {
|
|
|
951
809
|
return Object.keys(headers).some((key) => key.toLowerCase() === needle);
|
|
952
810
|
}
|
|
953
811
|
export function createStealthClient(baseUrl, defaultProfileOrOptions = DEFAULT_PROFILE, clientOptions = {}) {
|
|
954
|
-
const defaultProfile = typeof defaultProfileOrOptions === "string"
|
|
955
|
-
|
|
956
|
-
: DEFAULT_PROFILE;
|
|
957
|
-
const resolvedClientOptions = typeof defaultProfileOrOptions === "string"
|
|
958
|
-
? clientOptions
|
|
959
|
-
: defaultProfileOrOptions;
|
|
812
|
+
const defaultProfile = typeof defaultProfileOrOptions === "string" ? defaultProfileOrOptions : DEFAULT_PROFILE;
|
|
813
|
+
const resolvedClientOptions = typeof defaultProfileOrOptions === "string" ? clientOptions : defaultProfileOrOptions;
|
|
960
814
|
let sharedSession = null;
|
|
961
815
|
function getSharedSession() {
|
|
962
816
|
if (!sharedSession) {
|
package/dist/server/serve.js
CHANGED
|
@@ -3,7 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { Hono } from "hono";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { AuthAbortError, createAuthFlowHelpers } from "../auth";
|
|
6
|
-
import { AuthError,
|
|
6
|
+
import { AuthError, isProviderError, isSessionExpiredError, isTransportError, ProviderError, } from "../errors";
|
|
7
7
|
import { loadProviderLocaleCatalogs, localizeAuthTurn, } from "../i18n/catalog";
|
|
8
8
|
import { categoryForStatus, isRetryableCategory, PROVIDER_OBSERVABILITY_TAXONOMY_VERSION, } from "../observability";
|
|
9
9
|
import { createScratchpad } from "../runtime/auth-flow";
|
|
@@ -107,7 +107,7 @@ function isProductionProviderBrowserMode(provider, env = process.env) {
|
|
|
107
107
|
return (env.NODE_ENV === "production" && env.APIFUSE__PROVIDER__ID === provider.id);
|
|
108
108
|
}
|
|
109
109
|
export function resolveProviderProxyAffinityKey(provider, request, operationId) {
|
|
110
|
-
const connectionKey = request
|
|
110
|
+
const connectionKey = resolveOperationConnectionId(request) ?? request.connection?.externalRef;
|
|
111
111
|
const affinity = typeof provider.proxy === "object"
|
|
112
112
|
? provider.proxy.session?.affinity
|
|
113
113
|
: undefined;
|
|
@@ -116,6 +116,9 @@ export function resolveProviderProxyAffinityKey(provider, request, operationId)
|
|
|
116
116
|
}
|
|
117
117
|
return connectionKey ?? provider.id;
|
|
118
118
|
}
|
|
119
|
+
function resolveOperationConnectionId(request) {
|
|
120
|
+
return request.connection?.id ?? request.connectionId;
|
|
121
|
+
}
|
|
119
122
|
function createProviderContext(provider, request, operationId, options = {}, state = createUnsupportedProviderRuntimeState(), proxyTelemetry) {
|
|
120
123
|
const baseUrl = getProviderBaseUrl(provider);
|
|
121
124
|
const stealthBaseUrl = getProviderStealthBaseUrl(provider);
|
|
@@ -142,7 +145,7 @@ function createProviderContext(provider, request, operationId, options = {}, sta
|
|
|
142
145
|
values: request.connection?.secrets,
|
|
143
146
|
});
|
|
144
147
|
const requestContext = {
|
|
145
|
-
connectionId: request
|
|
148
|
+
connectionId: resolveOperationConnectionId(request),
|
|
146
149
|
headers: request.headers ?? {},
|
|
147
150
|
};
|
|
148
151
|
const context = wrapWithInstrumentation({
|
|
@@ -290,7 +293,7 @@ function zodDetails(error) {
|
|
|
290
293
|
}));
|
|
291
294
|
}
|
|
292
295
|
function toErrorResponse(error, requestId) {
|
|
293
|
-
if (error
|
|
296
|
+
if (isProviderError(error)) {
|
|
294
297
|
const details = publicProviderErrorDetails(error);
|
|
295
298
|
return {
|
|
296
299
|
error: {
|
|
@@ -340,20 +343,25 @@ function publicProviderErrorDetails(error) {
|
|
|
340
343
|
function isPlainRecord(value) {
|
|
341
344
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
342
345
|
}
|
|
346
|
+
// Accepts `unknown` so the branded guards narrow cleanly from the top: the
|
|
347
|
+
// subtype error classes are structurally compatible with ProviderError, so
|
|
348
|
+
// narrowing from a ProviderError-typed value would collapse the negative branch
|
|
349
|
+
// to `never`. Narrowing from unknown avoids that while still recognizing errors
|
|
350
|
+
// from a duplicate SDK module instance.
|
|
343
351
|
function providerObservabilityDetails(error) {
|
|
344
352
|
// Session-expiry surfaces the credential_expired category + the opt-in
|
|
345
353
|
// retryable signal so Gateway/Credential Service can refresh and re-drive the
|
|
346
354
|
// operation (see design.md §4.3 D3). Without this branch the auth error would
|
|
347
355
|
// serialize as a bare 401 with no retryable/category, losing the refresh
|
|
348
356
|
// signal for exactly the retryOnAuthRefresh operations it is meant to enable.
|
|
349
|
-
if (error
|
|
357
|
+
if (isSessionExpiredError(error)) {
|
|
350
358
|
return {
|
|
351
359
|
category: error.options?.category ?? "credential_expired",
|
|
352
360
|
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
353
361
|
retryable: error.options?.retryable ?? false,
|
|
354
362
|
};
|
|
355
363
|
}
|
|
356
|
-
if (!(error
|
|
364
|
+
if (!isTransportError(error)) {
|
|
357
365
|
return undefined;
|
|
358
366
|
}
|
|
359
367
|
const isProxyPoolCode = error.code === PROXY_POOL_EXHAUSTED_CODE ||
|
|
@@ -382,7 +390,7 @@ function providerObservabilityDetails(error) {
|
|
|
382
390
|
};
|
|
383
391
|
}
|
|
384
392
|
function publicProviderErrorMessage(error) {
|
|
385
|
-
if (error
|
|
393
|
+
if (isTransportError(error)) {
|
|
386
394
|
if (error.code === PROXY_AUTH_IP_DENIED_CODE) {
|
|
387
395
|
return error.message;
|
|
388
396
|
}
|
|
@@ -410,10 +418,10 @@ function toStatusCode(error) {
|
|
|
410
418
|
if (error instanceof z.ZodError) {
|
|
411
419
|
return 400;
|
|
412
420
|
}
|
|
413
|
-
if (error
|
|
421
|
+
if (isTransportError(error)) {
|
|
414
422
|
return error.code === "transport_timeout" ? 504 : 502;
|
|
415
423
|
}
|
|
416
|
-
if (error
|
|
424
|
+
if (isProviderError(error)) {
|
|
417
425
|
switch (error.code) {
|
|
418
426
|
case "AUTH_REQUIRED":
|
|
419
427
|
case "reauth_required":
|
|
@@ -445,14 +453,14 @@ function extractRequestId(raw) {
|
|
|
445
453
|
return typeof value === "string" ? value : undefined;
|
|
446
454
|
}
|
|
447
455
|
function logProviderError(logger, provider, kind, route, requestId, error, status, cost) {
|
|
448
|
-
const code = error
|
|
456
|
+
const code = isProviderError(error)
|
|
449
457
|
? (error.code ?? "provider_error")
|
|
450
458
|
: error instanceof z.ZodError
|
|
451
459
|
? "invalid_request"
|
|
452
460
|
: "internal_error";
|
|
453
461
|
const errorClass = error instanceof Error ? error.name : typeof error;
|
|
454
462
|
const message = error instanceof Error ? error.message : String(error);
|
|
455
|
-
const details = error
|
|
463
|
+
const details = isProviderError(error)
|
|
456
464
|
? providerObservabilityDetails(error)
|
|
457
465
|
: undefined;
|
|
458
466
|
const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
|
|
@@ -468,7 +476,7 @@ function logProviderError(logger, provider, kind, route, requestId, error, statu
|
|
|
468
476
|
code,
|
|
469
477
|
errorClass,
|
|
470
478
|
message,
|
|
471
|
-
...(error
|
|
479
|
+
...(isTransportError(error) && error.upstreamStatus
|
|
472
480
|
? { upstreamStatus: error.upstreamStatus }
|
|
473
481
|
: {}),
|
|
474
482
|
...(details
|
package/dist/server/types.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export declare const OperationConnectionSchema: z.ZodObject<{
|
|
|
21
21
|
export declare const OperationRequestSchema: z.ZodObject<{
|
|
22
22
|
requestId: z.ZodString;
|
|
23
23
|
input: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
24
|
+
connectionId: z.ZodOptional<z.ZodString>;
|
|
24
25
|
connection: z.ZodOptional<z.ZodObject<{
|
|
25
26
|
id: z.ZodString;
|
|
26
27
|
mode: z.ZodEnum<{
|
package/dist/server/types.js
CHANGED
|
@@ -17,6 +17,7 @@ export const OperationConnectionSchema = z.object({
|
|
|
17
17
|
export const OperationRequestSchema = z.object({
|
|
18
18
|
requestId: z.string(),
|
|
19
19
|
input: z.record(z.string(), z.unknown()),
|
|
20
|
+
connectionId: z.string().optional(),
|
|
20
21
|
connection: OperationConnectionSchema.optional(),
|
|
21
22
|
headers: z.record(z.string(), z.string()).optional(),
|
|
22
23
|
trace: z.record(z.string(), z.string()).optional(),
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.2.0-beta.
|
|
2
|
+
"version": "2.2.0-beta.3",
|
|
3
3
|
"name": "@apifuse/provider-sdk",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
@@ -93,6 +93,7 @@
|
|
|
93
93
|
},
|
|
94
94
|
"dependencies": {
|
|
95
95
|
"@clack/prompts": "^1.5.1",
|
|
96
|
+
"@rgrove/parse-xml": "4.2.2",
|
|
96
97
|
"@types/ms": "^2.1.0",
|
|
97
98
|
"acorn": "^8.17.0",
|
|
98
99
|
"ajv": "^8.17",
|
package/src/errors.ts
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
import type { ProviderErrorCategory } from "./observability";
|
|
2
2
|
|
|
3
|
+
// Versioned, cross-realm brands. `Symbol.for` resolves to the same symbol in
|
|
4
|
+
// any copy/entrypoint of this SDK major version, so an error created by a
|
|
5
|
+
// duplicate module instance (e.g. the packaged CLI's src/* server vs a
|
|
6
|
+
// provider's dist/* import) still carries a brand the server can recognize even
|
|
7
|
+
// though `instanceof` splits across the two constructors. The `@1` suffix lets a
|
|
8
|
+
// future breaking change to this contract mint a distinct key.
|
|
9
|
+
const PROVIDER_ERROR_BRAND = Symbol.for("@apifuse/provider-sdk/error-brand@1");
|
|
10
|
+
const PROVIDER_ERROR_BRAND_VALUE = 1;
|
|
11
|
+
const SESSION_EXPIRED_BRAND = Symbol.for(
|
|
12
|
+
"@apifuse/provider-sdk/error-kind/session-expired@1",
|
|
13
|
+
);
|
|
14
|
+
const TRANSPORT_BRAND = Symbol.for("@apifuse/provider-sdk/error-kind/transport@1");
|
|
15
|
+
|
|
16
|
+
// Defines a non-enumerable, non-writable, non-configurable own data property.
|
|
17
|
+
// Immutable + own means a guard can trust it via a single descriptor read
|
|
18
|
+
// without invoking attacker-controlled getters or accepting inherited brands.
|
|
19
|
+
function defineErrorBrand(target: object, brand: symbol, value: number | true): void {
|
|
20
|
+
Object.defineProperty(target, brand, {
|
|
21
|
+
value,
|
|
22
|
+
enumerable: false,
|
|
23
|
+
writable: false,
|
|
24
|
+
configurable: false,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Recognizes an own data-property brand with the expected value. Rejects
|
|
29
|
+
// missing brands (unbranded lookalikes), accessor brands (no own `value`
|
|
30
|
+
// slot — the getter is never called), and inherited brands (own-descriptor
|
|
31
|
+
// lookup returns undefined on the child).
|
|
32
|
+
function hasOwnBrand(value: unknown, brand: symbol, expected: number | true): boolean {
|
|
33
|
+
if (value === null || (typeof value !== "object" && typeof value !== "function")) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, brand);
|
|
37
|
+
return (
|
|
38
|
+
descriptor !== undefined &&
|
|
39
|
+
Object.hasOwn(descriptor, "value") &&
|
|
40
|
+
descriptor.value === expected
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
3
44
|
export type ProviderErrorOptions = {
|
|
4
45
|
fix?: string;
|
|
5
46
|
code?: string;
|
|
@@ -19,6 +60,7 @@ export class ProviderError extends Error {
|
|
|
19
60
|
if (options?.cause) {
|
|
20
61
|
this.cause = options.cause;
|
|
21
62
|
}
|
|
63
|
+
defineErrorBrand(this, PROVIDER_ERROR_BRAND, PROVIDER_ERROR_BRAND_VALUE);
|
|
22
64
|
}
|
|
23
65
|
|
|
24
66
|
get fix(): string | undefined {
|
|
@@ -60,6 +102,7 @@ export class SessionExpiredError extends AuthError {
|
|
|
60
102
|
...options,
|
|
61
103
|
});
|
|
62
104
|
this.name = "SessionExpiredError";
|
|
105
|
+
defineErrorBrand(this, SESSION_EXPIRED_BRAND, true);
|
|
63
106
|
}
|
|
64
107
|
}
|
|
65
108
|
|
|
@@ -91,9 +134,26 @@ export class TransportError extends ProviderError {
|
|
|
91
134
|
this.name = "TransportError";
|
|
92
135
|
this.status = options?.status;
|
|
93
136
|
this.upstreamStatus = options?.upstreamStatus ?? options?.status;
|
|
137
|
+
defineErrorBrand(this, TRANSPORT_BRAND, true);
|
|
94
138
|
}
|
|
95
139
|
}
|
|
96
140
|
|
|
141
|
+
// Cross-module type guards. Prefer these over `instanceof` at any boundary that
|
|
142
|
+
// may receive an error from a different copy/entrypoint of the SDK (see the HTTP
|
|
143
|
+
// server error boundary). They recognize branded errors regardless of which
|
|
144
|
+
// module instance constructed them, while rejecting unbranded lookalikes.
|
|
145
|
+
export function isProviderError(value: unknown): value is ProviderError {
|
|
146
|
+
return hasOwnBrand(value, PROVIDER_ERROR_BRAND, PROVIDER_ERROR_BRAND_VALUE);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function isSessionExpiredError(value: unknown): value is SessionExpiredError {
|
|
150
|
+
return isProviderError(value) && hasOwnBrand(value, SESSION_EXPIRED_BRAND, true);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function isTransportError(value: unknown): value is TransportError {
|
|
154
|
+
return isProviderError(value) && hasOwnBrand(value, TRANSPORT_BRAND, true);
|
|
155
|
+
}
|
|
156
|
+
|
|
97
157
|
export class ProviderSecretError extends ProviderError {
|
|
98
158
|
constructor(message: string, options?: ProviderErrorOptions) {
|
|
99
159
|
super(message, { code: "provider_secret_error", ...options });
|
package/src/provider.ts
CHANGED
package/src/runtime/executor.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ProviderError, SessionExpiredError } from "../errors";
|
|
1
|
+
import { isSessionExpiredError, ProviderError, SessionExpiredError } from "../errors";
|
|
2
2
|
import { parseSchema } from "../schema";
|
|
3
3
|
import type { ProviderContext, ProviderDefinition } from "../types";
|
|
4
4
|
|
|
@@ -64,7 +64,12 @@ export async function executeOperation(
|
|
|
64
64
|
// operation is safe to re-drive after refresh, which we signal by marking
|
|
65
65
|
// the surfaced error retryable; non-idempotent operations (the default)
|
|
66
66
|
// stay non-retryable so they are not auto-re-driven. See design.md §4.3 D3.
|
|
67
|
-
|
|
67
|
+
// Use the branded guard, not `instanceof`: a handler loaded through a
|
|
68
|
+
// duplicate/published SDK module can throw a correctly branded
|
|
69
|
+
// SessionExpiredError whose constructor identity differs from this
|
|
70
|
+
// executor's, which `instanceof` would miss — dropping the retryable
|
|
71
|
+
// upgrade and stranding an operation that opted into auth refresh.
|
|
72
|
+
if (isSessionExpiredError(error) && operation.retryOnAuthRefresh) {
|
|
68
73
|
throw new SessionExpiredError(error.message, { retryable: true });
|
|
69
74
|
}
|
|
70
75
|
throw error;
|