@apifuse/provider-sdk 2.2.0-beta.67 → 2.2.0-beta.69
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 +34 -2
- package/CHANGELOG.md +8 -0
- package/README.md +18 -7
- package/bin/apifuse-record.ts +81 -21
- package/bin/apifuse-submit-check.ts +170 -28
- package/dist/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl +138 -23
- package/dist/cli/templates/provider/README.md.tpl +17 -9
- package/dist/config/loader.d.ts +16 -0
- package/dist/config/loader.js +25 -8
- package/dist/error-resolution.d.ts +13 -0
- package/dist/error-resolution.js +72 -35
- package/dist/fixture-sanitization.js +159 -15
- package/dist/lint.d.ts +2 -0
- package/dist/lint.js +549 -11
- package/dist/runtime/diagnostic-redactor.d.ts +6 -1
- package/dist/runtime/diagnostic-redactor.js +16 -14
- package/dist/runtime/executor.js +3 -0
- package/dist/runtime/http.d.ts +2 -0
- package/dist/runtime/http.js +17 -103
- package/dist/runtime/instrumentation.js +4 -1
- package/dist/runtime/provider.d.ts +7 -0
- package/dist/runtime/provider.js +14 -0
- package/dist/runtime/redirect-hop-headers.d.ts +19 -0
- package/dist/runtime/redirect-hop-headers.js +58 -0
- package/dist/runtime/redirects.d.ts +33 -1
- package/dist/runtime/redirects.js +113 -0
- package/dist/runtime/resolver-span-attributes.d.ts +34 -0
- package/dist/runtime/resolver-span-attributes.js +93 -0
- package/dist/runtime/resolver-usage.d.ts +8 -0
- package/dist/runtime/resolver-usage.js +5 -11
- package/dist/runtime/resolver-vendors/browser.js +5 -4
- package/dist/runtime/resolver-vendors/capsolver.js +11 -10
- package/dist/runtime/resolver-vendors/twocaptcha.js +9 -8
- package/dist/runtime/resolver.js +7 -6
- package/dist/runtime/stealth-owned-headers.d.ts +21 -8
- package/dist/runtime/stealth-owned-headers.js +28 -8
- package/dist/runtime/stealth-telemetry.d.ts +1 -1
- package/dist/runtime/stealth.js +158 -46
- package/dist/runtime/trace.js +2 -2
- package/dist/server/index.d.ts +1 -0
- package/dist/server/serve-implementation.d.ts +2 -1
- package/dist/server/serve-implementation.js +45 -30
- package/dist/server/state-backend-report.d.ts +70 -0
- package/dist/server/state-backend-report.js +113 -0
- package/dist/testing/run.js +14 -7
- package/dist/trace-sanitization.js +1 -1
- package/dist/types.d.ts +30 -6
- package/package.json +2 -2
- package/src/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl +138 -23
- package/src/cli/templates/provider/README.md.tpl +17 -9
- package/src/config/loader.ts +49 -12
- package/src/error-resolution.ts +89 -35
- package/src/fixture-sanitization.ts +175 -17
- package/src/lint.ts +595 -10
- package/src/runtime/diagnostic-redactor.ts +19 -13
- package/src/runtime/executor.ts +3 -0
- package/src/runtime/http.ts +24 -112
- package/src/runtime/instrumentation.ts +4 -1
- package/src/runtime/provider.ts +15 -0
- package/src/runtime/redirect-hop-headers.ts +71 -0
- package/src/runtime/redirects.ts +150 -1
- package/src/runtime/resolver-span-attributes.ts +139 -0
- package/src/runtime/resolver-usage.ts +8 -5
- package/src/runtime/resolver-vendors/browser.ts +5 -4
- package/src/runtime/resolver-vendors/capsolver.ts +11 -10
- package/src/runtime/resolver-vendors/twocaptcha.ts +9 -8
- package/src/runtime/resolver.ts +7 -6
- package/src/runtime/stealth-owned-headers.ts +34 -8
- package/src/runtime/stealth-telemetry.ts +6 -1
- package/src/runtime/stealth.ts +180 -58
- package/src/runtime/trace.ts +6 -2
- package/src/server/index.ts +4 -0
- package/src/server/serve-implementation.ts +58 -31
- package/src/server/state-backend-report.ts +185 -0
- package/src/testing/run.ts +15 -2
- package/src/trace-sanitization.ts +1 -1
- package/src/types.ts +30 -6
package/src/runtime/stealth.ts
CHANGED
|
@@ -38,7 +38,7 @@ import type {
|
|
|
38
38
|
} from "../types.js";
|
|
39
39
|
import { chrome149HeaderOrder } from "./chrome149-header-order.js";
|
|
40
40
|
import { registerDiagnosticValue } from "./diagnostic-env.js";
|
|
41
|
-
import {
|
|
41
|
+
import { assertCallerHeadersSupported } from "./stealth-owned-headers.js";
|
|
42
42
|
import {
|
|
43
43
|
ENGINE_CEREMONY_EGRESS_LEASE,
|
|
44
44
|
type CeremonyEgressBinding,
|
|
@@ -68,10 +68,15 @@ import {
|
|
|
68
68
|
shouldRetryProxyTransportAttempt,
|
|
69
69
|
validateUnsafeProxyTransportRetryMethods,
|
|
70
70
|
} from "./proxy-retry-policy.js";
|
|
71
|
+
import { redirectHopHeaders } from "./redirect-hop-headers.js";
|
|
71
72
|
import {
|
|
72
73
|
evaluateRedirectHop,
|
|
74
|
+
invalidRedirectPolicy,
|
|
73
75
|
isRedirectStatus,
|
|
74
76
|
nextRedirectMethod,
|
|
77
|
+
nextSameOriginRedirectHop,
|
|
78
|
+
normalizeRedirectPolicy,
|
|
79
|
+
type RedirectPolicyTransport,
|
|
75
80
|
resolveRedirectUrl,
|
|
76
81
|
} from "./redirects.js";
|
|
77
82
|
import {
|
|
@@ -139,12 +144,10 @@ function isBoundEgressTransportFailure(error: TransportError): boolean {
|
|
|
139
144
|
);
|
|
140
145
|
}
|
|
141
146
|
const MAX_STEALTH_REDIRECT_HOPS = 10;
|
|
142
|
-
const
|
|
143
|
-
"
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
"content-type",
|
|
147
|
-
]);
|
|
147
|
+
const STEALTH_REDIRECT_POLICY_TRANSPORT: RedirectPolicyTransport = {
|
|
148
|
+
label: "ctx.stealth",
|
|
149
|
+
maxHops: MAX_STEALTH_REDIRECT_HOPS,
|
|
150
|
+
};
|
|
148
151
|
|
|
149
152
|
function isRecord(value: unknown): value is Record<PropertyKey, unknown> {
|
|
150
153
|
return typeof value === "object" && value !== null;
|
|
@@ -399,6 +402,8 @@ function normalizedHeaderEntries(
|
|
|
399
402
|
// installs Accept-Language via DevTools next to User-Agent; real Chrome only
|
|
400
403
|
// receives it from //net (URLRequestHttpJob::AddExtraHeaders), after
|
|
401
404
|
// Accept-Encoding and before Cookie, as al-placement-capture.json B/C show.
|
|
405
|
+
// chrome-form-post-capture.json was taken without that option and needs no
|
|
406
|
+
// adjustment.
|
|
402
407
|
const CHROME_HEADER_ORDERS = {
|
|
403
408
|
navigation: [
|
|
404
409
|
"sec-ch-ua",
|
|
@@ -435,7 +440,28 @@ const CHROME_HEADER_ORDERS = {
|
|
|
435
440
|
"cookie",
|
|
436
441
|
"priority",
|
|
437
442
|
],
|
|
438
|
-
|
|
443
|
+
"form-post": [
|
|
444
|
+
"content-length",
|
|
445
|
+
"cache-control",
|
|
446
|
+
"sec-ch-ua",
|
|
447
|
+
"sec-ch-ua-mobile",
|
|
448
|
+
"sec-ch-ua-platform",
|
|
449
|
+
"upgrade-insecure-requests",
|
|
450
|
+
"content-type",
|
|
451
|
+
"user-agent",
|
|
452
|
+
"origin",
|
|
453
|
+
"accept",
|
|
454
|
+
"sec-fetch-site",
|
|
455
|
+
"sec-fetch-mode",
|
|
456
|
+
"sec-fetch-user",
|
|
457
|
+
"sec-fetch-dest",
|
|
458
|
+
"referer",
|
|
459
|
+
"accept-encoding",
|
|
460
|
+
"accept-language",
|
|
461
|
+
"cookie",
|
|
462
|
+
"priority",
|
|
463
|
+
],
|
|
464
|
+
} as const satisfies Record<Exclude<ChromeRequestClass, "xhr">, readonly string[]>;
|
|
439
465
|
|
|
440
466
|
const CHROME_H1_HEADER_ORDERS = {
|
|
441
467
|
navigation: [
|
|
@@ -492,7 +518,29 @@ const CHROME_H1_HEADER_ORDERS = {
|
|
|
492
518
|
"accept-language",
|
|
493
519
|
"cookie",
|
|
494
520
|
],
|
|
495
|
-
|
|
521
|
+
"form-post": [
|
|
522
|
+
"host",
|
|
523
|
+
"connection",
|
|
524
|
+
"content-length",
|
|
525
|
+
"cache-control",
|
|
526
|
+
"sec-ch-ua",
|
|
527
|
+
"sec-ch-ua-mobile",
|
|
528
|
+
"sec-ch-ua-platform",
|
|
529
|
+
"upgrade-insecure-requests",
|
|
530
|
+
"content-type",
|
|
531
|
+
"user-agent",
|
|
532
|
+
"origin",
|
|
533
|
+
"accept",
|
|
534
|
+
"sec-fetch-site",
|
|
535
|
+
"sec-fetch-mode",
|
|
536
|
+
"sec-fetch-user",
|
|
537
|
+
"sec-fetch-dest",
|
|
538
|
+
"referer",
|
|
539
|
+
"accept-encoding",
|
|
540
|
+
"accept-language",
|
|
541
|
+
"cookie",
|
|
542
|
+
],
|
|
543
|
+
} as const satisfies Record<ChromeRequestClass, readonly string[]>;
|
|
496
544
|
|
|
497
545
|
const CHROME_H1_HEADER_NAMES: Record<string, string> = {
|
|
498
546
|
host: "Host",
|
|
@@ -516,7 +564,30 @@ const CHROME_H1_HEADER_NAMES: Record<string, string> = {
|
|
|
516
564
|
range: "Range",
|
|
517
565
|
};
|
|
518
566
|
|
|
519
|
-
type ChromeRequestClass =
|
|
567
|
+
type ChromeRequestClass = NonNullable<NonNullable<StealthFetchOptions["stealth"]>["requestClass"]>;
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* What each request class sends besides its header order, one row per public
|
|
571
|
+
* `stealth.requestClass`:
|
|
572
|
+
*
|
|
573
|
+
* - `fetchMetadata: "navigate"` is a document navigation: `Sec-Fetch-Mode:
|
|
574
|
+
* navigate`, `Sec-Fetch-Dest: document`, `Sec-Fetch-User: ?1` unless
|
|
575
|
+
* `userActivation` is false, `Upgrade-Insecure-Requests: 1`, and the
|
|
576
|
+
* profile's `Accept` and `Priority`. `"cors"` is a script fetch: `cors` /
|
|
577
|
+
* `empty`, `Accept: *\/*`, `Priority: u=1, i`.
|
|
578
|
+
* - `bodyful` sends `Content-Length`.
|
|
579
|
+
* - `validateCache` sends `Cache-Control: max-age=0`, which Chrome adds to a
|
|
580
|
+
* form submission (chrome-form-post-capture.json).
|
|
581
|
+
*/
|
|
582
|
+
const CHROME_REQUEST_CLASSES = {
|
|
583
|
+
navigation: { fetchMetadata: "navigate", bodyful: false, validateCache: false },
|
|
584
|
+
"form-post": { fetchMetadata: "navigate", bodyful: true, validateCache: true },
|
|
585
|
+
xhr: { fetchMetadata: "cors", bodyful: false, validateCache: false },
|
|
586
|
+
post: { fetchMetadata: "cors", bodyful: true, validateCache: false },
|
|
587
|
+
} as const satisfies Record<
|
|
588
|
+
ChromeRequestClass,
|
|
589
|
+
{ fetchMetadata: "navigate" | "cors"; bodyful: boolean; validateCache: boolean }
|
|
590
|
+
>;
|
|
520
591
|
|
|
521
592
|
function chromeHeaderOrder(
|
|
522
593
|
requestClass: ChromeRequestClass,
|
|
@@ -547,33 +618,11 @@ function chromeHeaderOrder(
|
|
|
547
618
|
return order;
|
|
548
619
|
}
|
|
549
620
|
|
|
550
|
-
function normalizedCallerHeaderEntries(
|
|
551
|
-
headers: Record<string, string | string[] | undefined>,
|
|
552
|
-
): HeaderTuple[] {
|
|
553
|
-
const entries = normalizedHeaderEntries(headers);
|
|
554
|
-
assertCallerHeadersSupported(entries);
|
|
555
|
-
return entries;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
621
|
function normalizedCallerHeaderEntriesFromRecord(headers: Record<string, string>): HeaderTuple[] {
|
|
559
622
|
// The record has been through normalizeHeaders (lowercase, merged names); the
|
|
560
623
|
// lowercase here only guards later insertions such as the cookie jar's header.
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
value,
|
|
564
|
-
]);
|
|
565
|
-
assertCallerHeadersSupported(entries);
|
|
566
|
-
return entries;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
function assertCallerHeadersSupported(entries: readonly HeaderTuple[]): void {
|
|
570
|
-
for (const [name] of entries) {
|
|
571
|
-
if (isStealthOwnedHeaderName(name)) {
|
|
572
|
-
throw new SDKError(`Stealth transport owns the "${name}" header; remove it from headers.`, {
|
|
573
|
-
code: "STEALTH_HEADER_OVERRIDE_UNSUPPORTED",
|
|
574
|
-
});
|
|
575
|
-
}
|
|
576
|
-
}
|
|
624
|
+
assertCallerHeadersSupported(headers);
|
|
625
|
+
return Object.entries(headers).map(([name, value]) => [name.toLowerCase(), value]);
|
|
577
626
|
}
|
|
578
627
|
|
|
579
628
|
function chromeRequestClass(
|
|
@@ -621,9 +670,10 @@ function buildChromeHeaderTuples(options: {
|
|
|
621
670
|
),
|
|
622
671
|
);
|
|
623
672
|
const requestClass = chromeRequestClass(options.method, options.requestClass);
|
|
673
|
+
const shape = CHROME_REQUEST_CLASSES[requestClass];
|
|
624
674
|
const referer = caller.get("referer");
|
|
625
675
|
const fetchSite = secFetchSite(options.requestUrl, referer);
|
|
626
|
-
const isNavigation =
|
|
676
|
+
const isNavigation = shape.fetchMetadata === "navigate";
|
|
627
677
|
const values = new Map<string, string>([
|
|
628
678
|
["sec-ch-ua", requiredEmulationHeader(emulation, "sec-ch-ua")],
|
|
629
679
|
["sec-ch-ua-mobile", requiredEmulationHeader(emulation, "sec-ch-ua-mobile")],
|
|
@@ -649,12 +699,16 @@ function buildChromeHeaderTuples(options: {
|
|
|
649
699
|
values.set("upgrade-insecure-requests", "1");
|
|
650
700
|
if (options.userActivation !== false) values.set("sec-fetch-user", "?1");
|
|
651
701
|
}
|
|
702
|
+
if (shape.validateCache) values.set("cache-control", "max-age=0");
|
|
652
703
|
for (const [name, value] of callerEntries) values.set(name, value);
|
|
653
704
|
for (const name of ["content-type", "origin", "referer"] as const) {
|
|
654
705
|
const value = caller.get(name);
|
|
655
706
|
if (value !== undefined) values.set(name, value);
|
|
656
707
|
}
|
|
657
|
-
|
|
708
|
+
// A bodyful class keeps its class across a 301/302/303 that rewrites the method
|
|
709
|
+
// to GET (a form submission's redirect stays a validated navigation), but the
|
|
710
|
+
// rewritten request has no body and Chrome sends no Content-Length for it.
|
|
711
|
+
if (shape.bodyful && options.method !== "GET" && options.method !== "HEAD") {
|
|
658
712
|
values.set(
|
|
659
713
|
"content-length",
|
|
660
714
|
caller.get("content-length") ?? String(Buffer.byteLength(options.body ?? "")),
|
|
@@ -1194,10 +1248,30 @@ function locationHeader(headers: Record<string, string>): string | undefined {
|
|
|
1194
1248
|
return undefined;
|
|
1195
1249
|
}
|
|
1196
1250
|
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1251
|
+
/**
|
|
1252
|
+
* Snapshot a caller's `redirectPolicy` before the first send. The policy acts
|
|
1253
|
+
* on redirects the transport follows, so it requires the default
|
|
1254
|
+
* `redirect: "follow"`.
|
|
1255
|
+
*/
|
|
1256
|
+
function snapshotStealthRedirectPolicy(options: StealthFetchOptions) {
|
|
1257
|
+
let policy: ReturnType<typeof normalizeRedirectPolicy>;
|
|
1258
|
+
try {
|
|
1259
|
+
policy = normalizeRedirectPolicy(options.redirectPolicy, STEALTH_REDIRECT_POLICY_TRANSPORT);
|
|
1260
|
+
} catch (error) {
|
|
1261
|
+
if (error instanceof TransportError) throw error;
|
|
1262
|
+
throw invalidRedirectPolicy(
|
|
1263
|
+
STEALTH_REDIRECT_POLICY_TRANSPORT,
|
|
1264
|
+
"could not be read safely",
|
|
1265
|
+
error instanceof Error ? error : undefined,
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
if (policy && options.redirect !== undefined && options.redirect !== "follow") {
|
|
1269
|
+
throw invalidRedirectPolicy(
|
|
1270
|
+
STEALTH_REDIRECT_POLICY_TRANSPORT,
|
|
1271
|
+
`it applies to followed redirects and cannot be combined with redirect: "${options.redirect}"`,
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
return policy;
|
|
1201
1275
|
}
|
|
1202
1276
|
|
|
1203
1277
|
function assertStealthRedirectUrl(url: string): void {
|
|
@@ -1318,7 +1392,9 @@ function startStealthExchange(
|
|
|
1318
1392
|
requestClass:
|
|
1319
1393
|
requestClass === "navigation" && options.stealth?.userActivation === false
|
|
1320
1394
|
? "script_navigation"
|
|
1321
|
-
: requestClass
|
|
1395
|
+
: requestClass === "form-post"
|
|
1396
|
+
? "form_post"
|
|
1397
|
+
: requestClass,
|
|
1322
1398
|
...(status === undefined ? {} : { status }),
|
|
1323
1399
|
...(errorCode ? { errorCode } : {}),
|
|
1324
1400
|
...(error === undefined
|
|
@@ -1359,6 +1435,10 @@ async function fetchStealthRedirectChain(
|
|
|
1359
1435
|
let followedHops = 0;
|
|
1360
1436
|
let response: StealthTransportResponse;
|
|
1361
1437
|
const deadline = options.timeout ? performance.now() + options.timeout : undefined;
|
|
1438
|
+
// Snapshotted and validated by session.fetch before the first send.
|
|
1439
|
+
const redirectPolicy = options.redirectPolicy;
|
|
1440
|
+
const initialOrigin = new URL(requestUrl).origin;
|
|
1441
|
+
const visitedRequests = new Set([`${method} ${requestUrl}`]);
|
|
1362
1442
|
|
|
1363
1443
|
let finishExchange: ReturnType<typeof startStealthExchange> | undefined;
|
|
1364
1444
|
let status: number | undefined;
|
|
@@ -1415,28 +1495,48 @@ async function fetchStealthRedirectChain(
|
|
|
1415
1495
|
});
|
|
1416
1496
|
}
|
|
1417
1497
|
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
);
|
|
1422
|
-
if (!nextUrl) break;
|
|
1423
|
-
if (followedHops >= MAX_STEALTH_REDIRECT_HOPS) {
|
|
1498
|
+
let nextUrl: string;
|
|
1499
|
+
let nextMethod: StealthMethod;
|
|
1500
|
+
if (redirectPolicy) {
|
|
1424
1501
|
discardStealthRedirectBody(response);
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1502
|
+
({ nextUrl, nextMethod } = nextSameOriginRedirectHop({
|
|
1503
|
+
policy: redirectPolicy,
|
|
1504
|
+
initialOrigin,
|
|
1505
|
+
currentUrl,
|
|
1506
|
+
responseUrl: response.url ?? currentUrl,
|
|
1507
|
+
status: response.status,
|
|
1508
|
+
method: currentMethod,
|
|
1509
|
+
location: response.headers.get("location") ?? undefined,
|
|
1510
|
+
followedHops,
|
|
1511
|
+
visitedRequests,
|
|
1512
|
+
}));
|
|
1513
|
+
} else {
|
|
1514
|
+
const location = resolveRedirectUrl(
|
|
1515
|
+
response.headers.get("location") ?? undefined,
|
|
1516
|
+
response.url ?? currentUrl,
|
|
1428
1517
|
);
|
|
1518
|
+
if (!location) break;
|
|
1519
|
+
if (followedHops >= MAX_STEALTH_REDIRECT_HOPS) {
|
|
1520
|
+
discardStealthRedirectBody(response);
|
|
1521
|
+
throw new TransportError(
|
|
1522
|
+
`Stealth request exceeded the ${MAX_STEALTH_REDIRECT_HOPS}-redirect limit`,
|
|
1523
|
+
{ code: "transport_network_error", status: 0 },
|
|
1524
|
+
);
|
|
1525
|
+
}
|
|
1526
|
+
discardStealthRedirectBody(response);
|
|
1527
|
+
nextUrl = location;
|
|
1528
|
+
nextMethod = nextRedirectMethod(response.status, currentMethod);
|
|
1429
1529
|
}
|
|
1430
1530
|
assertStealthRedirectUrl(nextUrl);
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
}
|
|
1531
|
+
currentHeaders = redirectHopHeaders(
|
|
1532
|
+
{ from: currentUrl, to: nextUrl, methodChanged: nextMethod !== currentMethod },
|
|
1533
|
+
currentHeaders,
|
|
1534
|
+
);
|
|
1535
|
+
if (nextMethod !== currentMethod) currentBody = undefined;
|
|
1437
1536
|
currentMethod = nextMethod;
|
|
1438
1537
|
currentUrl = nextUrl;
|
|
1439
1538
|
followedHops += 1;
|
|
1539
|
+
visitedRequests.add(`${currentMethod} ${currentUrl}`);
|
|
1440
1540
|
emitStealthTelemetry(telemetry?.sink, (sink) => sink.recordRedirectHop());
|
|
1441
1541
|
finishExchange(status);
|
|
1442
1542
|
finishExchange = undefined;
|
|
@@ -1558,10 +1658,15 @@ function createSessionFetcher(
|
|
|
1558
1658
|
let entry = clients.get(cacheKey);
|
|
1559
1659
|
if (!entry) {
|
|
1560
1660
|
entry = {
|
|
1661
|
+
// The header shape only partitions sessions; it is never handed to wreq as
|
|
1662
|
+
// session `defaultHeaders`. wreq merges those into every request that lacks
|
|
1663
|
+
// one of their names, so a header the SDK left off on purpose — a credential
|
|
1664
|
+
// dropped on a cross-origin redirect hop, the body headers after POST -> GET,
|
|
1665
|
+
// a cookie for another host — would come back with the first request's value.
|
|
1666
|
+
// Every request already sends its complete ordered header list.
|
|
1561
1667
|
session: wreq.createSession({
|
|
1562
1668
|
browser,
|
|
1563
1669
|
os,
|
|
1564
|
-
...(defaultHeaders ? { defaultHeaders } : {}),
|
|
1565
1670
|
...(proxyUrl ? { proxy: proxyUrl } : {}),
|
|
1566
1671
|
...(ignoreTlsErrors ? { insecure: true } : {}),
|
|
1567
1672
|
timeout: 30_000,
|
|
@@ -1761,7 +1866,11 @@ function createSessionFetcher(
|
|
|
1761
1866
|
const { hasExplicitRetryPolicy, method, stealthRetryOptions } = (() => {
|
|
1762
1867
|
try {
|
|
1763
1868
|
const method = normalizeMethod(options.method ?? "GET");
|
|
1764
|
-
|
|
1869
|
+
assertCallerHeadersSupported(options.headers ?? {});
|
|
1870
|
+
// Replace the caller's object with the validated copy: the walker, a
|
|
1871
|
+
// retry and an explicit replay all read this snapshot.
|
|
1872
|
+
const redirectPolicy = snapshotStealthRedirectPolicy(options);
|
|
1873
|
+
if (redirectPolicy) options.redirectPolicy = redirectPolicy;
|
|
1765
1874
|
const hasExplicitRetryPolicy = options.retry !== undefined;
|
|
1766
1875
|
const stealthRetryOptions =
|
|
1767
1876
|
normalizeProxyTransportRetryOptions(options.retry, {
|
|
@@ -2527,11 +2636,13 @@ function createSessionFetcher(
|
|
|
2527
2636
|
stopWhen,
|
|
2528
2637
|
params,
|
|
2529
2638
|
sensitiveParams,
|
|
2639
|
+
headers: initialHeaders,
|
|
2530
2640
|
...fetchOptions
|
|
2531
2641
|
} = options;
|
|
2532
2642
|
const hops: StealthRedirectHop[] = [];
|
|
2533
2643
|
let method = normalizeMethod(options.method ?? "GET");
|
|
2534
2644
|
let body = options.body;
|
|
2645
|
+
let headers = initialHeaders;
|
|
2535
2646
|
let response: StealthResponse | undefined;
|
|
2536
2647
|
const visitedRequests = new Set<string>();
|
|
2537
2648
|
const initialParams = params
|
|
@@ -2593,6 +2704,7 @@ function createSessionFetcher(
|
|
|
2593
2704
|
try {
|
|
2594
2705
|
response = await session.fetch(currentUrl, {
|
|
2595
2706
|
...fetchOptions,
|
|
2707
|
+
...(headers ? { headers } : {}),
|
|
2596
2708
|
body,
|
|
2597
2709
|
method,
|
|
2598
2710
|
...(hopIndex === 0 && initialParams ? { params: initialParams } : {}),
|
|
@@ -2689,6 +2801,16 @@ function createSessionFetcher(
|
|
|
2689
2801
|
cookieStore: cookieJar.serialize(),
|
|
2690
2802
|
};
|
|
2691
2803
|
}
|
|
2804
|
+
if (headers) {
|
|
2805
|
+
headers = redirectHopHeaders(
|
|
2806
|
+
{
|
|
2807
|
+
from: currentUrl,
|
|
2808
|
+
to: decision.nextUrl,
|
|
2809
|
+
methodChanged: decision.nextMethod !== method,
|
|
2810
|
+
},
|
|
2811
|
+
headers,
|
|
2812
|
+
);
|
|
2813
|
+
}
|
|
2692
2814
|
if (decision.nextMethod !== method) {
|
|
2693
2815
|
body = undefined;
|
|
2694
2816
|
}
|
package/src/runtime/trace.ts
CHANGED
|
@@ -191,7 +191,11 @@ function normalizeAttributes(
|
|
|
191
191
|
? redactedKey === REDACTION_FAILED
|
|
192
192
|
? REDACTION_FAILED
|
|
193
193
|
: REDACTED_QUERY_VALUE
|
|
194
|
-
: normalizeAttributeValue(
|
|
194
|
+
: normalizeAttributeValue(
|
|
195
|
+
value,
|
|
196
|
+
diagnosticAttributeRedactor(key, redact, value),
|
|
197
|
+
retainBigints,
|
|
198
|
+
);
|
|
195
199
|
return [normalizedKey, normalizedValue] as const;
|
|
196
200
|
})
|
|
197
201
|
.filter(
|
|
@@ -226,7 +230,7 @@ function prepareResourceAttributesForExport(
|
|
|
226
230
|
? redactedKey === REDACTION_FAILED
|
|
227
231
|
? REDACTION_FAILED
|
|
228
232
|
: REDACTED_QUERY_VALUE
|
|
229
|
-
: sanitizeSpanNameForOutput(value, diagnosticAttributeRedactor(key, redact)),
|
|
233
|
+
: sanitizeSpanNameForOutput(value, diagnosticAttributeRedactor(key, redact, value)),
|
|
230
234
|
];
|
|
231
235
|
}),
|
|
232
236
|
);
|
package/src/server/index.ts
CHANGED
|
@@ -115,6 +115,10 @@ export {
|
|
|
115
115
|
type ServeOptions,
|
|
116
116
|
serve,
|
|
117
117
|
} from "./serve.js";
|
|
118
|
+
export type {
|
|
119
|
+
ProviderStateBackendLogEvent,
|
|
120
|
+
ProviderStoreBackendReport,
|
|
121
|
+
} from "./state-backend-report.js";
|
|
118
122
|
export type { ProviderErrorObservability } from "../errors.js";
|
|
119
123
|
export {
|
|
120
124
|
computeSelfTestPlanDigest,
|
|
@@ -29,6 +29,7 @@ import { SDK_VERSION } from "./sdk-version.js";
|
|
|
29
29
|
import { safeProviderErrorObservability } from "../error-observability.js";
|
|
30
30
|
import { providerErrorStackFrames } from "./error-stack-frames.js";
|
|
31
31
|
import {
|
|
32
|
+
registeredUpstreamErrorCategory,
|
|
32
33
|
SDK_OWNED_PROVIDER_ERROR_CODES,
|
|
33
34
|
SDK_RUNTIME_OWNED_ERROR_CODES,
|
|
34
35
|
SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES,
|
|
@@ -106,7 +107,7 @@ import { wrapWithInstrumentation } from "../runtime/instrumentation.js";
|
|
|
106
107
|
import type * as NativeNetworkRuntimeModule from "../runtime/native-network.js";
|
|
107
108
|
import { createOcrClientFromEnv } from "../runtime/ocr.js";
|
|
108
109
|
import { bindOcrTelemetry, OcrTelemetryCollector } from "../runtime/ocr-telemetry.js";
|
|
109
|
-
import { getProviderBaseUrl } from "../runtime/provider.js";
|
|
110
|
+
import { getProviderBaseUrl, getProviderStealthBaseUrl } from "../runtime/provider.js";
|
|
110
111
|
import {
|
|
111
112
|
PROXY_AUTH_IP_DENIED_CODE,
|
|
112
113
|
PROXY_EDGE_AUTH_REJECTED_CODE,
|
|
@@ -126,6 +127,7 @@ import {
|
|
|
126
127
|
tenantOpaqueKeys,
|
|
127
128
|
} from "../runtime/request-telemetry.js";
|
|
128
129
|
import type * as ResolverRuntimeModule from "../runtime/resolver.js";
|
|
130
|
+
import { resolverSpanAttributes } from "../runtime/resolver-span-attributes.js";
|
|
129
131
|
import {
|
|
130
132
|
bindResolverTelemetry,
|
|
131
133
|
createUnsupportedResolverClient,
|
|
@@ -216,6 +218,10 @@ import type {
|
|
|
216
218
|
} from "../types.js";
|
|
217
219
|
import { VALID_OPERATION_ERROR_STATUSES } from "../types.js";
|
|
218
220
|
import type { SelfTestCancellationLogEvent } from "./self-test.js";
|
|
221
|
+
import {
|
|
222
|
+
type ProviderStateBackendLogEvent,
|
|
223
|
+
providerStateBackendLogEvent,
|
|
224
|
+
} from "./state-backend-report.js";
|
|
219
225
|
import { resolveSelfTestMasterSecrets } from "./self-test-token.js";
|
|
220
226
|
import {
|
|
221
227
|
collectStaticDiagnosticSensitiveValues,
|
|
@@ -635,15 +641,6 @@ function bindResolverSignalWithoutRuntime(
|
|
|
635
641
|
return boundResolver;
|
|
636
642
|
}
|
|
637
643
|
|
|
638
|
-
function getProviderStealthBaseUrl(provider: ProviderDefinition): string | undefined {
|
|
639
|
-
const baseUrl = getProviderBaseUrl(provider);
|
|
640
|
-
if (baseUrl) {
|
|
641
|
-
return baseUrl;
|
|
642
|
-
}
|
|
643
|
-
const firstHost = provider.allowedHosts?.[0];
|
|
644
|
-
return firstHost ? `https://${firstHost}` : undefined;
|
|
645
|
-
}
|
|
646
|
-
|
|
647
644
|
function getProviderStealthProfile(provider: ProviderDefinition) {
|
|
648
645
|
return provider.stealth ? getStealthProfile(provider.stealth) : undefined;
|
|
649
646
|
}
|
|
@@ -706,7 +703,7 @@ function createStealthChallengeDetection(
|
|
|
706
703
|
const solve = () => resolver.solve(challenge, solveSignal, recorder ?? undefined);
|
|
707
704
|
return recorder
|
|
708
705
|
? recorder.runSpan("resolver.solve", solve, {
|
|
709
|
-
attributes: { challenge_kind: challenge.kind },
|
|
706
|
+
attributes: resolverSpanAttributes({ challenge_kind: challenge.kind }),
|
|
710
707
|
})
|
|
711
708
|
: solve();
|
|
712
709
|
},
|
|
@@ -1476,6 +1473,7 @@ export type ProviderServerLogEvent =
|
|
|
1476
1473
|
*/
|
|
1477
1474
|
warnings?: readonly string[];
|
|
1478
1475
|
}
|
|
1476
|
+
| ProviderStateBackendLogEvent
|
|
1479
1477
|
| SelfTestCancellationLogEvent;
|
|
1480
1478
|
|
|
1481
1479
|
export type ProviderServerLogger = (event: ProviderServerLogEvent) => void;
|
|
@@ -1607,8 +1605,9 @@ function zodDetails(
|
|
|
1607
1605
|
// (credential_unavailable) usually means a caller credential problem, an
|
|
1608
1606
|
// internal stateful-routing deadline is APIFuse-owned despite its timeout
|
|
1609
1607
|
// category, and the built-in upstream failure families keep their upstream
|
|
1610
|
-
// attribution even
|
|
1611
|
-
//
|
|
1608
|
+
// attribution even under an explicit non-upstream category. An unset category
|
|
1609
|
+
// needs no override: a registered upstream code already carries its category
|
|
1610
|
+
// (SDK_UPSTREAM_ERROR_CODE_REGISTRY), and the source follows from it.
|
|
1612
1611
|
function publicErrorSource(error: unknown, category: ProviderErrorCategory): ProviderErrorSource {
|
|
1613
1612
|
if (error instanceof StatefulRoutingDeadlineError) return "apifuse";
|
|
1614
1613
|
if (isProviderError(error)) {
|
|
@@ -1931,6 +1930,24 @@ function providerObservabilityDetails(
|
|
|
1931
1930
|
};
|
|
1932
1931
|
}
|
|
1933
1932
|
|
|
1933
|
+
// The category a `ProviderError` (validation errors included) gets when its
|
|
1934
|
+
// author set none, ahead of the branch's own default: an operation-declared
|
|
1935
|
+
// rejection status (409/410/422) → upstream_rejected, then the registered
|
|
1936
|
+
// upstream code's category (UPSTREAM_SCHEMA_ERROR → upstream_schema_drift,
|
|
1937
|
+
// NOT_FOUND → upstream_http, ...). `undefined` leaves the branch default.
|
|
1938
|
+
function derivedErrorCategory(
|
|
1939
|
+
error: unknown,
|
|
1940
|
+
declaredStatus: unknown,
|
|
1941
|
+
): ProviderErrorCategory | undefined {
|
|
1942
|
+
if (
|
|
1943
|
+
isEmittableErrorStatus(declaredStatus) &&
|
|
1944
|
+
categoryForStatus(declaredStatus) === "upstream_rejected"
|
|
1945
|
+
) {
|
|
1946
|
+
return "upstream_rejected";
|
|
1947
|
+
}
|
|
1948
|
+
return registeredUpstreamErrorCategory(providerErrorCode(error));
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1934
1951
|
function classifiedErrorObservabilityDetails(
|
|
1935
1952
|
error: unknown,
|
|
1936
1953
|
declaredErrorCode?: OperationErrorCode,
|
|
@@ -1945,12 +1962,10 @@ function classifiedErrorObservabilityDetails(
|
|
|
1945
1962
|
return {
|
|
1946
1963
|
category: providerCategory
|
|
1947
1964
|
? providerCategory
|
|
1948
|
-
:
|
|
1949
|
-
|
|
1950
|
-
? "upstream_rejected"
|
|
1951
|
-
: isEmittableErrorStatus(declaredStatus) && declaredStatus >= 500
|
|
1965
|
+
: (derivedErrorCategory(error, declaredStatus) ??
|
|
1966
|
+
(isEmittableErrorStatus(declaredStatus) && declaredStatus >= 500
|
|
1952
1967
|
? "provider_error"
|
|
1953
|
-
: "input_validation",
|
|
1968
|
+
: "input_validation")),
|
|
1954
1969
|
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
1955
1970
|
retryable: isProviderError(error)
|
|
1956
1971
|
? (providerErrorOption(error, "retryable") ?? effectiveDeclaration?.retryable ?? false)
|
|
@@ -1967,19 +1982,13 @@ function classifiedErrorObservabilityDetails(
|
|
|
1967
1982
|
}
|
|
1968
1983
|
|
|
1969
1984
|
if (isProviderError(error)) {
|
|
1970
|
-
//
|
|
1971
|
-
//
|
|
1972
|
-
// status (409/410/422) classify as upstream_rejected unless the
|
|
1973
|
-
// author set an explicit category.
|
|
1974
|
-
const declaredStatus = effectiveDeclaration?.status;
|
|
1975
|
-
const rejectionDefault =
|
|
1976
|
-
providerErrorCode(error) === "UPSTREAM_REJECTED" ||
|
|
1977
|
-
(isEmittableErrorStatus(declaredStatus) &&
|
|
1978
|
-
categoryForStatus(declaredStatus) === "upstream_rejected")
|
|
1979
|
-
? ("upstream_rejected" as const)
|
|
1980
|
-
: ("provider_error" as const);
|
|
1985
|
+
// Retryability does not follow the derived category: it stays
|
|
1986
|
+
// instance ?? declared ?? false below.
|
|
1981
1987
|
return {
|
|
1982
|
-
category:
|
|
1988
|
+
category:
|
|
1989
|
+
providerErrorOption(error, "category") ??
|
|
1990
|
+
derivedErrorCategory(error, effectiveDeclaration?.status) ??
|
|
1991
|
+
"provider_error",
|
|
1983
1992
|
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
1984
1993
|
retryable:
|
|
1985
1994
|
providerErrorOption(error, "retryable") ?? effectiveDeclaration?.retryable ?? false,
|
|
@@ -3755,7 +3764,13 @@ function createServerAppWithCapabilityModules(
|
|
|
3755
3764
|
}
|
|
3756
3765
|
|
|
3757
3766
|
app.notFound((c) => {
|
|
3758
|
-
|
|
3767
|
+
// An unknown route is not an upstream answer: the explicit category keeps
|
|
3768
|
+
// the registered not_found code from attributing it to an upstream.
|
|
3769
|
+
const error = new ProviderError("Not found", {
|
|
3770
|
+
code: "not_found",
|
|
3771
|
+
category: "provider_error",
|
|
3772
|
+
retryable: false,
|
|
3773
|
+
});
|
|
3759
3774
|
const observabilityDetails = errorObservabilityDetails(error);
|
|
3760
3775
|
return responseWithErrorObservability(
|
|
3761
3776
|
c.json(
|
|
@@ -4354,6 +4369,18 @@ export async function serve<TContext extends Partial<ProviderContext> = Provider
|
|
|
4354
4369
|
!isUnavailableProviderEngine(engineSelection.engine),
|
|
4355
4370
|
),
|
|
4356
4371
|
);
|
|
4372
|
+
// Same contract for the stores: one line per process boot naming the state
|
|
4373
|
+
// and cache backends this pod resolved, so "which Redis is this provider's
|
|
4374
|
+
// state actually on" is a log grep instead of an inference from the
|
|
4375
|
+
// Deployment spec (apifuse#2302).
|
|
4376
|
+
logger(
|
|
4377
|
+
providerStateBackendLogEvent({
|
|
4378
|
+
providerId: provider.id,
|
|
4379
|
+
sdkVersion: SDK_VERSION,
|
|
4380
|
+
injectedState: options.state !== undefined,
|
|
4381
|
+
allowMemoryFallback: options.allowMemoryStateFallback === true,
|
|
4382
|
+
}),
|
|
4383
|
+
);
|
|
4357
4384
|
|
|
4358
4385
|
const servers: BunServerHandle[] = [];
|
|
4359
4386
|
try {
|