@apifuse/provider-sdk 2.2.0-beta.44 → 2.2.0-beta.45
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 +4 -0
- package/README.md +2 -1
- package/bin/apifuse-check.ts +14 -5
- package/dist/declaration-validation.d.ts +2 -1
- package/dist/declaration-validation.js +59 -0
- package/dist/define.d.ts +4 -0
- package/dist/lint.d.ts +15 -0
- package/dist/lint.js +171 -2
- package/dist/runtime/insights.js +2 -2
- package/dist/runtime/stealth.d.ts +1 -1
- package/dist/runtime/stealth.js +14 -5
- package/dist/server/serve-implementation.js +4 -4
- package/dist/stealth/profiles.d.ts +4 -0
- package/dist/stealth/profiles.js +118 -15
- package/dist/types.d.ts +4 -0
- package/package.json +1 -1
- package/src/declaration-validation.ts +65 -1
- package/src/define.ts +4 -0
- package/src/lint.ts +240 -2
- package/src/runtime/insights.ts +2 -2
- package/src/runtime/stealth.ts +17 -5
- package/src/server/serve-implementation.ts +4 -4
- package/src/stealth/profiles.ts +135 -16
- package/src/types.ts +4 -0
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -144,7 +144,8 @@ the bad request path; provider/runtime failures include `code`, `message`, and
|
|
|
144
144
|
flows that must inspect intermediate `Location`/`Set-Cookie` headers, create
|
|
145
145
|
a session with `ctx.stealth.createSession()` and use `session.redirects.run()`;
|
|
146
146
|
inspect accumulated cookies through `session.cookies`. Select an SDK stealth
|
|
147
|
-
`profile` such as `chrome-
|
|
147
|
+
intent-based `profile` such as `chrome-desktop`; do not pin a browser version
|
|
148
|
+
or tune JA3, HTTP/2 SETTINGS, or
|
|
148
149
|
pseudo-header order in provider code. Chrome, Firefox, and Safari profiles
|
|
149
150
|
are supported; use `ctx.browser` when the provider needs browser execution.
|
|
150
151
|
- **Query-parameter credentials**: when an upstream requires a credential in
|
package/bin/apifuse-check.ts
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
} from "../src/declaration-validation.js";
|
|
18
18
|
import { isProviderError } from "../src/errors.js";
|
|
19
19
|
import type { ProviderDefinition } from "../src/index.js";
|
|
20
|
-
import {
|
|
20
|
+
import { lintProviderWithInformation, type ProviderLintMode } from "../src/lint.js";
|
|
21
21
|
import { safeParseSchemaSync } from "../src/schema.js";
|
|
22
22
|
|
|
23
23
|
const HELP_TEXT = `Usage: apifuse check [path]
|
|
@@ -379,12 +379,21 @@ function checkAuthoringLint(
|
|
|
379
379
|
};
|
|
380
380
|
}
|
|
381
381
|
|
|
382
|
-
const
|
|
382
|
+
const { diagnostics, information } = lintProviderWithInformation(
|
|
383
|
+
{ ...provider, providerSourceFiles },
|
|
384
|
+
{ mode: lintMode },
|
|
385
|
+
);
|
|
383
386
|
const errors = diagnostics.filter((diagnostic) => diagnostic.level === "error");
|
|
384
|
-
const details =
|
|
385
|
-
const field =
|
|
386
|
-
return
|
|
387
|
+
const details = information.map((entry) => {
|
|
388
|
+
const field = entry.field ? `${entry.field}: ` : "";
|
|
389
|
+
return `INFO ${entry.rule} ${field}${entry.message}`;
|
|
387
390
|
});
|
|
391
|
+
details.push(
|
|
392
|
+
...diagnostics.map((diagnostic) => {
|
|
393
|
+
const field = diagnostic.field ? `${diagnostic.field}: ` : "";
|
|
394
|
+
return `${diagnostic.level.toUpperCase()} ${diagnostic.rule} ${field}${diagnostic.message}`;
|
|
395
|
+
}),
|
|
396
|
+
);
|
|
388
397
|
|
|
389
398
|
return {
|
|
390
399
|
message: "Provider authoring lint has no error-level diagnostics",
|
|
@@ -12,6 +12,7 @@ export declare const DECLARATION_RULE_IDS: {
|
|
|
12
12
|
readonly proxyNoMixedVendors: "proxy-no-mixed-vendors";
|
|
13
13
|
readonly proxySmartproxyGeo: "proxy-smartproxy-country-only";
|
|
14
14
|
readonly operationUpstreamProxy: "operation-upstream-proxy-unsupported";
|
|
15
|
+
readonly pinnedWireFieldPathValid: "public-schema-pinned-wire-field-valid";
|
|
15
16
|
};
|
|
16
17
|
export type DeclarationRuleId = (typeof DECLARATION_RULE_IDS)[keyof typeof DECLARATION_RULE_IDS];
|
|
17
18
|
export type DeclarationViolation = {
|
|
@@ -23,7 +24,7 @@ export type DeclarationViolation = {
|
|
|
23
24
|
export declare function declarationInvalidError(violations: readonly DeclarationViolation[]): ProviderError;
|
|
24
25
|
/** Enforces declaration rules whose runtime behavior would otherwise fail open. */
|
|
25
26
|
export declare function validateFailClosedDeclaration(provider: ProviderDefinition): void;
|
|
26
|
-
type ProviderDeclarationRulesInput = Pick<ProviderDefinition, "healthJourneys" | "proxy">;
|
|
27
|
+
type ProviderDeclarationRulesInput = Pick<ProviderDefinition, "healthJourneys" | "meta" | "proxy">;
|
|
27
28
|
type OperationDeclarationRulesInput = Pick<ProviderDefinition, "operations">;
|
|
28
29
|
/** Enforces fail-closed rules that only depend on the provider declaration. */
|
|
29
30
|
export declare function validateFailClosedProviderDeclaration(provider: ProviderDeclarationRulesInput): void;
|
|
@@ -13,6 +13,7 @@ export const DECLARATION_RULE_IDS = {
|
|
|
13
13
|
proxyNoMixedVendors: "proxy-no-mixed-vendors",
|
|
14
14
|
proxySmartproxyGeo: "proxy-smartproxy-country-only",
|
|
15
15
|
operationUpstreamProxy: "operation-upstream-proxy-unsupported",
|
|
16
|
+
pinnedWireFieldPathValid: "public-schema-pinned-wire-field-valid",
|
|
16
17
|
};
|
|
17
18
|
export function declarationInvalidError(violations) {
|
|
18
19
|
const summary = violations
|
|
@@ -28,6 +29,7 @@ export function declarationInvalidError(violations) {
|
|
|
28
29
|
export function validateFailClosedDeclaration(provider) {
|
|
29
30
|
const violations = [];
|
|
30
31
|
validateHealthDeclaration(provider, violations);
|
|
32
|
+
validatePinnedWireFieldPaths(provider, violations);
|
|
31
33
|
validateSchemaDeclaration(provider, violations);
|
|
32
34
|
validateProxyDeclaration(provider, violations);
|
|
33
35
|
validateOperationDeclaration(provider, violations);
|
|
@@ -50,8 +52,65 @@ export function validateFailClosedOperationDeclaration(provider) {
|
|
|
50
52
|
}
|
|
51
53
|
function collectProviderDeclarationViolations(provider, violations) {
|
|
52
54
|
validateHealthDeclaration(provider, violations);
|
|
55
|
+
validatePinnedWireFieldPaths(provider, violations);
|
|
53
56
|
validateProxyDeclaration(provider, violations);
|
|
54
57
|
}
|
|
58
|
+
function validatePinnedWireFieldPaths(provider, violations) {
|
|
59
|
+
const pins = provider.meta?.contract?.pinnedWireFieldPaths;
|
|
60
|
+
if (pins === undefined)
|
|
61
|
+
return;
|
|
62
|
+
if (!Array.isArray(pins)) {
|
|
63
|
+
violations.push({
|
|
64
|
+
ruleId: DECLARATION_RULE_IDS.pinnedWireFieldPathValid,
|
|
65
|
+
path: "meta.contract.pinnedWireFieldPaths",
|
|
66
|
+
message: "pinned wire field paths must be an array.",
|
|
67
|
+
fix: "Declare a readonly array of { path, reason } entries.",
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const seenPaths = new Set();
|
|
72
|
+
for (const [index, pin] of pins.entries()) {
|
|
73
|
+
const entryPath = `meta.contract.pinnedWireFieldPaths[${index}]`;
|
|
74
|
+
if (!pin || typeof pin !== "object" || Array.isArray(pin)) {
|
|
75
|
+
violations.push({
|
|
76
|
+
ruleId: DECLARATION_RULE_IDS.pinnedWireFieldPathValid,
|
|
77
|
+
path: entryPath,
|
|
78
|
+
message: "each pinned wire field path must be an object.",
|
|
79
|
+
fix: `Replace ${entryPath} with { path: "operations.<operation-id>.<field-path>", reason: "<non-empty reason>" }.`,
|
|
80
|
+
});
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const path = "path" in pin ? pin.path : undefined;
|
|
84
|
+
const reason = "reason" in pin ? pin.reason : undefined;
|
|
85
|
+
if (typeof path !== "string" || path.trim().length === 0) {
|
|
86
|
+
violations.push({
|
|
87
|
+
ruleId: DECLARATION_RULE_IDS.pinnedWireFieldPathValid,
|
|
88
|
+
path: `${entryPath}.path`,
|
|
89
|
+
message: "path must be a non-empty string.",
|
|
90
|
+
fix: `Set ${entryPath}.path to the exact public-schema-upstream-field diagnostic path.`,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
else if (seenPaths.has(path)) {
|
|
94
|
+
violations.push({
|
|
95
|
+
ruleId: DECLARATION_RULE_IDS.pinnedWireFieldPathValid,
|
|
96
|
+
path: `${entryPath}.path`,
|
|
97
|
+
message: `duplicate pinned wire field path ${JSON.stringify(path)}.`,
|
|
98
|
+
fix: `Keep exactly one declaration for ${JSON.stringify(path)}.`,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
seenPaths.add(path);
|
|
103
|
+
}
|
|
104
|
+
if (typeof reason !== "string" || reason.trim().length === 0) {
|
|
105
|
+
violations.push({
|
|
106
|
+
ruleId: DECLARATION_RULE_IDS.pinnedWireFieldPathValid,
|
|
107
|
+
path: `${entryPath}.reason`,
|
|
108
|
+
message: "reason must be a non-empty string.",
|
|
109
|
+
fix: `Document why renaming the exact wire field would break the upstream contract.`,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
55
114
|
function collectOperationDeclarationViolations(provider, violations) {
|
|
56
115
|
validateSchemaDeclaration(provider, violations);
|
|
57
116
|
validateOperationDeclaration(provider, violations);
|
package/dist/define.d.ts
CHANGED
|
@@ -103,6 +103,10 @@ export interface ProviderDeclaration {
|
|
|
103
103
|
implementationProfile?: ProviderImplementationProfile;
|
|
104
104
|
contract?: {
|
|
105
105
|
publicSchemaFieldNames?: "normalized";
|
|
106
|
+
readonly pinnedWireFieldPaths?: readonly {
|
|
107
|
+
readonly path: string;
|
|
108
|
+
readonly reason: string;
|
|
109
|
+
}[];
|
|
106
110
|
};
|
|
107
111
|
};
|
|
108
112
|
healthMonitor?: ProviderHealthMonitorConfig;
|
package/dist/lint.d.ts
CHANGED
|
@@ -12,6 +12,10 @@ type ProviderAuthLike = {
|
|
|
12
12
|
};
|
|
13
13
|
type ProviderContractMetaLike = {
|
|
14
14
|
publicSchemaFieldNames?: "normalized";
|
|
15
|
+
pinnedWireFieldPaths?: readonly {
|
|
16
|
+
readonly path: string;
|
|
17
|
+
readonly reason: string;
|
|
18
|
+
}[];
|
|
15
19
|
};
|
|
16
20
|
export interface LintDiagnostic {
|
|
17
21
|
rule: string;
|
|
@@ -23,6 +27,15 @@ export type ProviderLintMode = "official" | "standalone";
|
|
|
23
27
|
type ProviderLintOptions = {
|
|
24
28
|
mode?: ProviderLintMode;
|
|
25
29
|
};
|
|
30
|
+
export interface ProviderLintInformation {
|
|
31
|
+
rule: string;
|
|
32
|
+
message: string;
|
|
33
|
+
field?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface ProviderLintResult {
|
|
36
|
+
diagnostics: LintDiagnostic[];
|
|
37
|
+
information: ProviderLintInformation[];
|
|
38
|
+
}
|
|
26
39
|
export declare function lintOperation(op: {
|
|
27
40
|
description?: string;
|
|
28
41
|
descriptionKey?: string;
|
|
@@ -76,4 +89,6 @@ export declare function lintProvider(provider: {
|
|
|
76
89
|
};
|
|
77
90
|
reviewed?: string;
|
|
78
91
|
}, options?: ProviderLintOptions): LintDiagnostic[];
|
|
92
|
+
/** Internal detailed result used by the CLI to render suppression audits. */
|
|
93
|
+
export declare function lintProviderWithInformation(provider: Parameters<typeof lintProvider>[0], options?: ProviderLintOptions): ProviderLintResult;
|
|
79
94
|
export {};
|
package/dist/lint.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import { SDK_RUNTIME_OWNED_ERROR_CODES, SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES, } from "./error-resolution.js";
|
|
2
3
|
import { lintPublicSchemaFieldNames } from "./public-schema-field-lint.js";
|
|
3
4
|
import { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_SENSITIVE_META_KEY } from "./schema.js";
|
|
5
|
+
const requireModule = createRequire(import.meta.url);
|
|
6
|
+
// `typeof import(...)` keeps the type without emitting a static import: the
|
|
7
|
+
// typescript package is a CLI-only dependency and src/lint.ts is production
|
|
8
|
+
// runtime, which the typescript-import-boundary test enforces.
|
|
9
|
+
let typeScriptModule;
|
|
10
|
+
function getTypeScript() {
|
|
11
|
+
typeScriptModule ??= requireModule("typescript");
|
|
12
|
+
return typeScriptModule;
|
|
13
|
+
}
|
|
4
14
|
// Operations that perform an auth-lifecycle action belong on the single
|
|
5
15
|
// `auth.flow` interface, never on a provider operation:
|
|
6
16
|
// - entry (login / signin / authenticate) => auth.flow.start/continue
|
|
@@ -667,6 +677,114 @@ function lintSelfHostedBrowserPatterns(provider, options) {
|
|
|
667
677
|
}
|
|
668
678
|
const THROWN_ERROR_CONSTRUCTION_PATTERN = /new\s+(?:ProviderError|ValidationError)\s*\(/g;
|
|
669
679
|
const TEST_SOURCE_FILE_PATTERN = /(?:^|\/)(?:__tests__|__mocks__)\/|\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
680
|
+
const RECORDED_FIXTURE_SOURCE_FILE_PATTERN = /(?:^|\/)__fixtures__(?:\/|$)|(?:^|\/)__tests__\/fixtures(?:\/|$)/;
|
|
681
|
+
const JAVASCRIPT_SOURCE_FILE_PATTERN = /\.[cm]?[jt]sx?$/;
|
|
682
|
+
const VERSIONED_PROFILE_LITERAL_PATTERN = /\b(?:chrome|chromium|firefox|safari|edge|opera|ios[-_]safari)[-_]\d+(?:[._-]\d+)*(?=$|[^A-Za-z0-9])/i;
|
|
683
|
+
const VERSIONED_USER_AGENT_PATTERN = /\b(?:Chrome|CriOS|Firefox|FxiOS|EdgA?|OPR)\/\d+(?:\.\d+)*/i;
|
|
684
|
+
const VERSIONED_SAFARI_USER_AGENT_PATTERN = /\bVersion\/(\d+(?:\.\d+)*)(?=[\s\S]*\bSafari\/\d)/i;
|
|
685
|
+
const VERSIONED_CLIENT_HINT_PATTERN = /(?:^|[;,\s])v\s*=\s*["']?\d+/i;
|
|
686
|
+
function staticStringText(node) {
|
|
687
|
+
if (!node)
|
|
688
|
+
return undefined;
|
|
689
|
+
const ts = getTypeScript();
|
|
690
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))
|
|
691
|
+
return node.text;
|
|
692
|
+
return undefined;
|
|
693
|
+
}
|
|
694
|
+
function staticPropertyName(node) {
|
|
695
|
+
const ts = getTypeScript();
|
|
696
|
+
if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) {
|
|
697
|
+
return node.text;
|
|
698
|
+
}
|
|
699
|
+
if (ts.isComputedPropertyName(node))
|
|
700
|
+
return staticStringText(node.expression);
|
|
701
|
+
return undefined;
|
|
702
|
+
}
|
|
703
|
+
function isSecChUaHeaderName(value) {
|
|
704
|
+
return value?.toLowerCase() === "sec-ch-ua";
|
|
705
|
+
}
|
|
706
|
+
function collectBrowserVersionLiteralFindings(source) {
|
|
707
|
+
const ts = getTypeScript();
|
|
708
|
+
const sourceFile = ts.createSourceFile("provider-source.ts", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
709
|
+
const findings = [];
|
|
710
|
+
const seen = new Set();
|
|
711
|
+
const addFinding = (kind, literal, position) => {
|
|
712
|
+
const key = `${kind}:${position}:${literal}`;
|
|
713
|
+
if (seen.has(key))
|
|
714
|
+
return;
|
|
715
|
+
seen.add(key);
|
|
716
|
+
findings.push({ kind, literal, position });
|
|
717
|
+
};
|
|
718
|
+
const inspectLiteral = (text, position) => {
|
|
719
|
+
const profile = text.match(VERSIONED_PROFILE_LITERAL_PATTERN)?.[0];
|
|
720
|
+
if (profile)
|
|
721
|
+
addFinding("profile", profile, position);
|
|
722
|
+
const userAgent = text.match(VERSIONED_USER_AGENT_PATTERN)?.[0] ??
|
|
723
|
+
text.match(VERSIONED_SAFARI_USER_AGENT_PATTERN)?.[0];
|
|
724
|
+
if (userAgent)
|
|
725
|
+
addFinding("user-agent", userAgent, position);
|
|
726
|
+
};
|
|
727
|
+
const inspectSecChUaValue = (node) => {
|
|
728
|
+
const text = staticStringText(node);
|
|
729
|
+
if (text && VERSIONED_CLIENT_HINT_PATTERN.test(text)) {
|
|
730
|
+
addFinding("sec-ch-ua", "sec-ch-ua", node.getStart(sourceFile));
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
const visit = (node) => {
|
|
734
|
+
const text = staticStringText(node);
|
|
735
|
+
if (text !== undefined)
|
|
736
|
+
inspectLiteral(text, node.getStart(sourceFile));
|
|
737
|
+
if (ts.isPropertyAssignment(node) && isSecChUaHeaderName(staticPropertyName(node.name))) {
|
|
738
|
+
inspectSecChUaValue(node.initializer);
|
|
739
|
+
}
|
|
740
|
+
if (ts.isCallExpression(node) && isSecChUaHeaderName(staticStringText(node.arguments[0]))) {
|
|
741
|
+
inspectSecChUaValue(node.arguments[1]);
|
|
742
|
+
}
|
|
743
|
+
if (ts.isArrayLiteralExpression(node) &&
|
|
744
|
+
isSecChUaHeaderName(staticStringText(node.elements[0]))) {
|
|
745
|
+
inspectSecChUaValue(node.elements[1]);
|
|
746
|
+
}
|
|
747
|
+
ts.forEachChild(node, visit);
|
|
748
|
+
};
|
|
749
|
+
visit(sourceFile);
|
|
750
|
+
return findings.sort((left, right) => left.position - right.position);
|
|
751
|
+
}
|
|
752
|
+
function browserVersionLiteralMessage(finding) {
|
|
753
|
+
switch (finding.kind) {
|
|
754
|
+
case "profile":
|
|
755
|
+
return `Hardcoded stealth profile "${finding.literal}" pins a browser version and will rot. Use the matching intent alias instead: "chrome-desktop", "firefox-desktop", "safari-desktop", or "safari-mobile".`;
|
|
756
|
+
case "user-agent":
|
|
757
|
+
return `Hardcoded User-Agent browser version "${finding.literal}" can disagree with the stealth TLS fingerprint. Remove the literal and derive it from the matching intent profile, for example getStealthProfile("chrome-desktop").userAgent.`;
|
|
758
|
+
case "sec-ch-ua":
|
|
759
|
+
return 'Hardcoded sec-ch-ua versions can disagree with the stealth TLS fingerprint. Remove the literal and let ctx.stealth generate client hints from an intent profile such as "chrome-desktop"; derive any explicit User-Agent with getStealthProfile("chrome-desktop").userAgent.';
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
function lintBrowserVersionLiterals(provider) {
|
|
763
|
+
const sources = [];
|
|
764
|
+
const sourceFiles = Object.entries(provider.providerSourceFiles ?? {}).filter(([filePath]) => JAVASCRIPT_SOURCE_FILE_PATTERN.test(filePath) &&
|
|
765
|
+
!TEST_SOURCE_FILE_PATTERN.test(filePath) &&
|
|
766
|
+
!RECORDED_FIXTURE_SOURCE_FILE_PATTERN.test(filePath));
|
|
767
|
+
if (sourceFiles.length > 0) {
|
|
768
|
+
for (const [filePath, source] of sourceFiles) {
|
|
769
|
+
sources.push({ field: `sourceFiles.${filePath}`, source });
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
else {
|
|
773
|
+
if (provider.authFlowSource)
|
|
774
|
+
sources.push({ field: "auth.flow", source: provider.authFlowSource });
|
|
775
|
+
for (const [operationKey, operation] of Object.entries(provider.operations ?? {})) {
|
|
776
|
+
const source = getOperationSource(operation);
|
|
777
|
+
if (source)
|
|
778
|
+
sources.push({ field: `operations.${operationKey}.handler`, source });
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return sources.flatMap(({ field, source }) => collectBrowserVersionLiteralFindings(source).map((finding) => ({
|
|
782
|
+
rule: "browser-version-literal",
|
|
783
|
+
level: "error",
|
|
784
|
+
field,
|
|
785
|
+
message: browserVersionLiteralMessage(finding),
|
|
786
|
+
})));
|
|
787
|
+
}
|
|
670
788
|
/**
|
|
671
789
|
* Skips a string literal starting at `startIndex` (which must point at the
|
|
672
790
|
* opening quote). Returns the index of the closing quote, or -1 when the
|
|
@@ -1021,7 +1139,57 @@ export function lintOperation(op) {
|
|
|
1021
1139
|
}
|
|
1022
1140
|
return diagnostics;
|
|
1023
1141
|
}
|
|
1142
|
+
function declaredPinnedWireFieldPaths(provider) {
|
|
1143
|
+
const pins = provider.meta?.contract?.pinnedWireFieldPaths;
|
|
1144
|
+
if (!Array.isArray(pins))
|
|
1145
|
+
return [];
|
|
1146
|
+
return pins.filter((pin) => pin !== null &&
|
|
1147
|
+
typeof pin === "object" &&
|
|
1148
|
+
typeof pin.path === "string" &&
|
|
1149
|
+
pin.path.trim().length > 0 &&
|
|
1150
|
+
typeof pin.reason === "string" &&
|
|
1151
|
+
pin.reason.trim().length > 0);
|
|
1152
|
+
}
|
|
1153
|
+
function applyPinnedWireFieldPaths(provider, diagnostics) {
|
|
1154
|
+
const pins = declaredPinnedWireFieldPaths(provider);
|
|
1155
|
+
if (pins.length === 0) {
|
|
1156
|
+
return { diagnostics: [...diagnostics], information: [] };
|
|
1157
|
+
}
|
|
1158
|
+
const pinsByPath = new Map(pins.map((pin) => [pin.path, pin]));
|
|
1159
|
+
const matchedPaths = new Set();
|
|
1160
|
+
const remainingDiagnostics = diagnostics.filter((diagnostic) => {
|
|
1161
|
+
if (diagnostic.rule !== "public-schema-upstream-field" ||
|
|
1162
|
+
diagnostic.field === undefined ||
|
|
1163
|
+
!pinsByPath.has(diagnostic.field)) {
|
|
1164
|
+
return true;
|
|
1165
|
+
}
|
|
1166
|
+
matchedPaths.add(diagnostic.field);
|
|
1167
|
+
return false;
|
|
1168
|
+
});
|
|
1169
|
+
const information = [];
|
|
1170
|
+
for (const pin of pins) {
|
|
1171
|
+
if (matchedPaths.has(pin.path)) {
|
|
1172
|
+
information.push({
|
|
1173
|
+
rule: "public-schema-pinned-wire-field",
|
|
1174
|
+
field: pin.path,
|
|
1175
|
+
message: `Suppressed exact public-schema-upstream-field diagnostic. Reason: ${pin.reason}`,
|
|
1176
|
+
});
|
|
1177
|
+
continue;
|
|
1178
|
+
}
|
|
1179
|
+
remainingDiagnostics.push({
|
|
1180
|
+
rule: "public-schema-pinned-wire-field-stale",
|
|
1181
|
+
level: "error",
|
|
1182
|
+
field: pin.path,
|
|
1183
|
+
message: `Pinned wire field path ${JSON.stringify(pin.path)} matches no current public-schema-upstream-field diagnostic; remove the stale declaration.`,
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
return { diagnostics: remainingDiagnostics, information };
|
|
1187
|
+
}
|
|
1024
1188
|
export function lintProvider(provider, options = {}) {
|
|
1189
|
+
return lintProviderWithInformation(provider, options).diagnostics;
|
|
1190
|
+
}
|
|
1191
|
+
/** Internal detailed result used by the CLI to render suppression audits. */
|
|
1192
|
+
export function lintProviderWithInformation(provider, options = {}) {
|
|
1025
1193
|
const diagnostics = [
|
|
1026
1194
|
...lintAllowedHosts(provider.id, provider.allowedHosts),
|
|
1027
1195
|
...lintReviewed(provider.id, provider.reviewed),
|
|
@@ -1030,6 +1198,7 @@ export function lintProvider(provider, options = {}) {
|
|
|
1030
1198
|
...lintCredentialWriteUsage(provider),
|
|
1031
1199
|
...lintPlaywrightDirectImports(provider),
|
|
1032
1200
|
...lintSelfHostedBrowserPatterns(provider, options),
|
|
1201
|
+
...lintBrowserVersionLiterals(provider),
|
|
1033
1202
|
...lintUndeclaredThrownErrorCodes(provider),
|
|
1034
1203
|
];
|
|
1035
1204
|
if (provider.operations) {
|
|
@@ -1053,7 +1222,7 @@ export function lintProvider(provider, options = {}) {
|
|
|
1053
1222
|
}
|
|
1054
1223
|
}
|
|
1055
1224
|
if (!provider.operations) {
|
|
1056
|
-
return diagnostics;
|
|
1225
|
+
return applyPinnedWireFieldPaths(provider, diagnostics);
|
|
1057
1226
|
}
|
|
1058
1227
|
diagnostics.push(...Object.entries(provider.operations).flatMap(([operationKey, operation]) => [
|
|
1059
1228
|
...lintOperation({
|
|
@@ -1077,5 +1246,5 @@ export function lintProvider(provider, options = {}) {
|
|
|
1077
1246
|
: `operations.${operationKey}`,
|
|
1078
1247
|
message: `[${operationKey}] ${diagnostic.message}`,
|
|
1079
1248
|
}))));
|
|
1080
|
-
return diagnostics;
|
|
1249
|
+
return applyPinnedWireFieldPaths(provider, diagnostics);
|
|
1081
1250
|
}
|
package/dist/runtime/insights.js
CHANGED
|
@@ -4,7 +4,7 @@ const SLOW_TRANSFORM_MS = 10;
|
|
|
4
4
|
const DNS_WARN_MS = 5;
|
|
5
5
|
const BROWSER_IDLE_MS = 5_000;
|
|
6
6
|
const REFRESH_WARN_RATE = 0.1;
|
|
7
|
-
const TLS_REUSE_FIX = `const session = ctx.stealth.createSession({ profile: 'chrome-
|
|
7
|
+
const TLS_REUSE_FIX = `const session = ctx.stealth.createSession({ profile: 'chrome-desktop' });
|
|
8
8
|
const resp = await session.fetch(url, opts);`;
|
|
9
9
|
const TRANSFORM_FIX = `transformResponse: (raw) => {
|
|
10
10
|
return raw.items.map(({ id, name, price }) => ({ id, name, price }));
|
|
@@ -13,7 +13,7 @@ const LARGE_RESPONSE_FIX = `const resp = await ctx.http.get('/items', {
|
|
|
13
13
|
params: { limit: 50, page: 1 },
|
|
14
14
|
});`;
|
|
15
15
|
const DNS_FIX = `// Enable DNS caching or reuse a long-lived session per host.
|
|
16
|
-
const session = ctx.stealth.createSession({ profile: 'chrome-
|
|
16
|
+
const session = ctx.stealth.createSession({ profile: 'chrome-desktop' });
|
|
17
17
|
await session.fetch(url, opts);`;
|
|
18
18
|
const PROXY_FIX = `// Re-check whether this operation really needs a proxy.
|
|
19
19
|
await ctx.stealth.fetch(url, { ...opts, proxy: undefined });`;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { BrowserProfile, EmulationOS } from "wreq-js";
|
|
2
2
|
import type { ProxyResolutionOptions } from "../config/loader.js";
|
|
3
3
|
import type { StealthClient, StealthResponse } from "../types.js";
|
|
4
|
-
export declare const DEFAULT_PROFILE = "chrome-
|
|
4
|
+
export declare const DEFAULT_PROFILE = "chrome-desktop";
|
|
5
5
|
export type StealthClientOptions = ProxyResolutionOptions & {
|
|
6
6
|
warn?: (message: string) => void;
|
|
7
7
|
/**
|
package/dist/runtime/stealth.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { DEFAULT_SMARTPROXY_POOL_SIZE, invalidateProxyResolutionCacheAsync, ProxyResolutionError, policyResolvesRegistryVendorChain, resolvePolicyProxyPoolSpan, resolvePolicyTransportAttemptCap, resolveProxyConfigAsync, vendorFromResolvedSource, } from "../config/loader.js";
|
|
3
3
|
import { SDKError, TransportError } from "../errors.js";
|
|
4
|
-
import { getStealthProfile } from "../stealth/profiles.js";
|
|
4
|
+
import { getStealthProfile, getStealthProfileIntentAlias } from "../stealth/profiles.js";
|
|
5
5
|
import { StealthCookieJar } from "./stealth-cookies.js";
|
|
6
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.js";
|
|
7
7
|
import { computeProxyAttemptIndex, computeProxyTransportRetryDelayMs, createDefaultProxyTransportRetryOptions, normalizeProxyTransportRetryOptions, shouldRetryProxyTransportAttempt, validateUnsafeProxyTransportRetryMethods, } from "./proxy-retry-policy.js";
|
|
8
8
|
import { evaluateRedirectHop, isRedirectStatus, nextRedirectMethod, resolveRedirectUrl, } from "./redirects.js";
|
|
9
9
|
import { isSensitiveKey, normalizeSensitiveParams, redactSensitiveError, redactSensitiveRequestError, redactSensitiveText, redactUrlQueryParams, serializeRequestUrl, } from "./request-options.js";
|
|
10
|
-
export const DEFAULT_PROFILE = "chrome-
|
|
10
|
+
export const DEFAULT_PROFILE = "chrome-desktop";
|
|
11
11
|
const MISSING_PROXY_WARNING = "[provider-sdk] Provider requested proxy routing, but no proxy URL was configured. Continuing without proxy.";
|
|
12
12
|
const MAX_POLICY_PROXY_POOL_REFRESHES = 1;
|
|
13
13
|
const PROXY_CONNECT_FAILURE_CODE = "proxy_connect_failed";
|
|
@@ -108,7 +108,11 @@ function resolveDefaultWreqProfileMapping() {
|
|
|
108
108
|
}
|
|
109
109
|
return { identifier, os: profile.platform };
|
|
110
110
|
}
|
|
111
|
-
|
|
111
|
+
let defaultWreqProfileMapping;
|
|
112
|
+
function getDefaultWreqProfileMapping() {
|
|
113
|
+
defaultWreqProfileMapping ??= resolveDefaultWreqProfileMapping();
|
|
114
|
+
return defaultWreqProfileMapping;
|
|
115
|
+
}
|
|
112
116
|
export function resolveWreqProfile(profileName, wreqProfiles) {
|
|
113
117
|
if (REMOVED_CHROME_PROFILE_NAMES.has(profileName)) {
|
|
114
118
|
throw new SDKError(`Unknown stealth profile: ${profileName}`);
|
|
@@ -125,8 +129,9 @@ export function resolveWreqProfile(profileName, wreqProfiles) {
|
|
|
125
129
|
// profile strings still run with the transport default instead of failing
|
|
126
130
|
// before the request starts. Removed built-in profile aliases above remain
|
|
127
131
|
// explicit errors so callers do not accidentally pin retired fingerprints.
|
|
128
|
-
|
|
129
|
-
|
|
132
|
+
const defaultMapping = getDefaultWreqProfileMapping();
|
|
133
|
+
identifier = defaultMapping.identifier;
|
|
134
|
+
os = defaultMapping.os;
|
|
130
135
|
}
|
|
131
136
|
const browser = closestWreqProfile(identifier, wreqProfiles);
|
|
132
137
|
if (!browser) {
|
|
@@ -546,6 +551,10 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
546
551
|
let closed = false;
|
|
547
552
|
let hasWarnedMissingProxy = false;
|
|
548
553
|
const warn = clientOptions.warn ?? console.warn;
|
|
554
|
+
const intentAlias = getStealthProfileIntentAlias(defaultProfile);
|
|
555
|
+
if (intentAlias) {
|
|
556
|
+
warn(`[provider-sdk] Stealth profile "${defaultProfile}" pins a browser version and is deprecated. Use the intent profile "${intentAlias}" so TLS, headers, and User-Agent stay aligned with SDK updates; derive an explicit User-Agent with getStealthProfile("${intentAlias}").userAgent instead of hardcoding one.`);
|
|
557
|
+
}
|
|
549
558
|
const cookieJar = new StealthCookieJar([], baseUrl);
|
|
550
559
|
async function getClientEntry(profileName, proxyUrl, ignoreTlsErrors) {
|
|
551
560
|
if (closed) {
|
|
@@ -446,10 +446,10 @@ function createProviderContext(provider, request, operationId, options, state =
|
|
|
446
446
|
stealth: stealthBaseUrl
|
|
447
447
|
? capabilityModules.stealth
|
|
448
448
|
? stealthProfile
|
|
449
|
-
? capabilityModules.stealth.createStealthClient(stealthBaseUrl,
|
|
449
|
+
? capabilityModules.stealth.createStealthClient(stealthBaseUrl, provider.stealth.profile, stealthClientOptions)
|
|
450
450
|
: capabilityModules.stealth.createStealthClient(stealthBaseUrl, stealthClientOptions)
|
|
451
451
|
: stealthProfile
|
|
452
|
-
? createLazyStealthClient(logStealthCleanupError, stealthBaseUrl,
|
|
452
|
+
? createLazyStealthClient(logStealthCleanupError, stealthBaseUrl, provider.stealth.profile, stealthClientOptions)
|
|
453
453
|
: createLazyStealthClient(logStealthCleanupError, stealthBaseUrl, stealthClientOptions)
|
|
454
454
|
: createStealthStub(),
|
|
455
455
|
browser: provider.runtime === "browser"
|
|
@@ -593,10 +593,10 @@ function createAuthFlowContext(provider, request, options, state, proxyTelemetry
|
|
|
593
593
|
stealth: stealthBaseUrl
|
|
594
594
|
? capabilityModules.stealth
|
|
595
595
|
? stealthProfile
|
|
596
|
-
? capabilityModules.stealth.createStealthClient(stealthBaseUrl,
|
|
596
|
+
? capabilityModules.stealth.createStealthClient(stealthBaseUrl, provider.stealth.profile, stealthClientOptions)
|
|
597
597
|
: capabilityModules.stealth.createStealthClient(stealthBaseUrl, stealthClientOptions)
|
|
598
598
|
: stealthProfile
|
|
599
|
-
? createLazyStealthClient(logStealthCleanupError, stealthBaseUrl,
|
|
599
|
+
? createLazyStealthClient(logStealthCleanupError, stealthBaseUrl, provider.stealth.profile, stealthClientOptions)
|
|
600
600
|
: createLazyStealthClient(logStealthCleanupError, stealthBaseUrl, stealthClientOptions)
|
|
601
601
|
: createStealthStub(),
|
|
602
602
|
...(provider.native
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { StealthProfile } from "../types.js";
|
|
2
2
|
export declare function generateLayer2Headers(profile: StealthProfile): Record<string, string>;
|
|
3
3
|
export declare function getStealthProfile(name: string): StealthProfile;
|
|
4
|
+
/** Returns the intent alias that replaces a registered version-pinned profile. */
|
|
5
|
+
export declare function getStealthProfileIntentAlias(name: string): string | undefined;
|
|
6
|
+
/** Internal compatibility catalog used by transport-parity tests. */
|
|
7
|
+
export declare function listRegisteredStealthProfiles(): string[];
|
|
4
8
|
export declare function listStealthProfiles(): string[];
|