@apifuse/provider-sdk 2.2.0-beta.44 → 2.2.0-beta.46
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 +8 -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 +3 -1
- package/dist/runtime/stealth.js +187 -22
- package/dist/server/serve-implementation.js +6 -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 +252 -26
- package/src/server/serve-implementation.ts +6 -4
- package/src/stealth/profiles.ts +135 -16
- package/src/types.ts +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @apifuse/provider-sdk Changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.0-beta.46
|
|
4
|
+
|
|
5
|
+
- Release candidate for main commit 0340fbc51b4ad71689bb1ec61b9b818dedda4587.
|
|
6
|
+
|
|
7
|
+
## 2.2.0-beta.45
|
|
8
|
+
|
|
9
|
+
- Release candidate for main commit 5d337d9fc1c77cd814ec7a4e1aa44ba0f8b2a5a9.
|
|
10
|
+
|
|
3
11
|
## 2.2.0-beta.44
|
|
4
12
|
|
|
5
13
|
- Release candidate for main commit a3091c0b4931c5ae48fe01b9776e4dc213db2716.
|
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,9 +1,11 @@
|
|
|
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
|
+
/** Abort all requests issued by this client. */
|
|
8
|
+
signal?: AbortSignal;
|
|
7
9
|
/**
|
|
8
10
|
* Proxy-only stealth transport overrides. Use only for upstream proxy products
|
|
9
11
|
* that terminate CONNECT with a private CA instead of tunneling the origin
|