@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/package.json
CHANGED
|
@@ -21,6 +21,7 @@ export const DECLARATION_RULE_IDS = {
|
|
|
21
21
|
proxyNoMixedVendors: "proxy-no-mixed-vendors",
|
|
22
22
|
proxySmartproxyGeo: "proxy-smartproxy-country-only",
|
|
23
23
|
operationUpstreamProxy: "operation-upstream-proxy-unsupported",
|
|
24
|
+
pinnedWireFieldPathValid: "public-schema-pinned-wire-field-valid",
|
|
24
25
|
} as const;
|
|
25
26
|
|
|
26
27
|
export type DeclarationRuleId =
|
|
@@ -53,13 +54,14 @@ export function declarationInvalidError(
|
|
|
53
54
|
export function validateFailClosedDeclaration(provider: ProviderDefinition): void {
|
|
54
55
|
const violations: DeclarationViolation[] = [];
|
|
55
56
|
validateHealthDeclaration(provider, violations);
|
|
57
|
+
validatePinnedWireFieldPaths(provider, violations);
|
|
56
58
|
validateSchemaDeclaration(provider, violations);
|
|
57
59
|
validateProxyDeclaration(provider, violations);
|
|
58
60
|
validateOperationDeclaration(provider, violations);
|
|
59
61
|
if (violations.length > 0) throw declarationInvalidError(violations);
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
type ProviderDeclarationRulesInput = Pick<ProviderDefinition, "healthJourneys" | "proxy">;
|
|
64
|
+
type ProviderDeclarationRulesInput = Pick<ProviderDefinition, "healthJourneys" | "meta" | "proxy">;
|
|
63
65
|
type OperationDeclarationRulesInput = Pick<ProviderDefinition, "operations">;
|
|
64
66
|
|
|
65
67
|
/** Enforces fail-closed rules that only depend on the provider declaration. */
|
|
@@ -85,9 +87,71 @@ function collectProviderDeclarationViolations(
|
|
|
85
87
|
violations: DeclarationViolation[],
|
|
86
88
|
): void {
|
|
87
89
|
validateHealthDeclaration(provider, violations);
|
|
90
|
+
validatePinnedWireFieldPaths(provider, violations);
|
|
88
91
|
validateProxyDeclaration(provider, violations);
|
|
89
92
|
}
|
|
90
93
|
|
|
94
|
+
function validatePinnedWireFieldPaths(
|
|
95
|
+
provider: ProviderDeclarationRulesInput,
|
|
96
|
+
violations: DeclarationViolation[],
|
|
97
|
+
): void {
|
|
98
|
+
const pins = provider.meta?.contract?.pinnedWireFieldPaths;
|
|
99
|
+
if (pins === undefined) return;
|
|
100
|
+
|
|
101
|
+
if (!Array.isArray(pins)) {
|
|
102
|
+
violations.push({
|
|
103
|
+
ruleId: DECLARATION_RULE_IDS.pinnedWireFieldPathValid,
|
|
104
|
+
path: "meta.contract.pinnedWireFieldPaths",
|
|
105
|
+
message: "pinned wire field paths must be an array.",
|
|
106
|
+
fix: "Declare a readonly array of { path, reason } entries.",
|
|
107
|
+
});
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const seenPaths = new Set<string>();
|
|
112
|
+
for (const [index, pin] of pins.entries()) {
|
|
113
|
+
const entryPath = `meta.contract.pinnedWireFieldPaths[${index}]`;
|
|
114
|
+
if (!pin || typeof pin !== "object" || Array.isArray(pin)) {
|
|
115
|
+
violations.push({
|
|
116
|
+
ruleId: DECLARATION_RULE_IDS.pinnedWireFieldPathValid,
|
|
117
|
+
path: entryPath,
|
|
118
|
+
message: "each pinned wire field path must be an object.",
|
|
119
|
+
fix: `Replace ${entryPath} with { path: "operations.<operation-id>.<field-path>", reason: "<non-empty reason>" }.`,
|
|
120
|
+
});
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const path = "path" in pin ? pin.path : undefined;
|
|
125
|
+
const reason = "reason" in pin ? pin.reason : undefined;
|
|
126
|
+
if (typeof path !== "string" || path.trim().length === 0) {
|
|
127
|
+
violations.push({
|
|
128
|
+
ruleId: DECLARATION_RULE_IDS.pinnedWireFieldPathValid,
|
|
129
|
+
path: `${entryPath}.path`,
|
|
130
|
+
message: "path must be a non-empty string.",
|
|
131
|
+
fix: `Set ${entryPath}.path to the exact public-schema-upstream-field diagnostic path.`,
|
|
132
|
+
});
|
|
133
|
+
} else if (seenPaths.has(path)) {
|
|
134
|
+
violations.push({
|
|
135
|
+
ruleId: DECLARATION_RULE_IDS.pinnedWireFieldPathValid,
|
|
136
|
+
path: `${entryPath}.path`,
|
|
137
|
+
message: `duplicate pinned wire field path ${JSON.stringify(path)}.`,
|
|
138
|
+
fix: `Keep exactly one declaration for ${JSON.stringify(path)}.`,
|
|
139
|
+
});
|
|
140
|
+
} else {
|
|
141
|
+
seenPaths.add(path);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (typeof reason !== "string" || reason.trim().length === 0) {
|
|
145
|
+
violations.push({
|
|
146
|
+
ruleId: DECLARATION_RULE_IDS.pinnedWireFieldPathValid,
|
|
147
|
+
path: `${entryPath}.reason`,
|
|
148
|
+
message: "reason must be a non-empty string.",
|
|
149
|
+
fix: `Document why renaming the exact wire field would break the upstream contract.`,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
91
155
|
function collectOperationDeclarationViolations(
|
|
92
156
|
provider: OperationDeclarationRulesInput,
|
|
93
157
|
violations: DeclarationViolation[],
|
package/src/define.ts
CHANGED
|
@@ -638,6 +638,10 @@ export interface ProviderDeclaration {
|
|
|
638
638
|
implementationProfile?: ProviderImplementationProfile;
|
|
639
639
|
contract?: {
|
|
640
640
|
publicSchemaFieldNames?: "normalized";
|
|
641
|
+
readonly pinnedWireFieldPaths?: readonly {
|
|
642
|
+
readonly path: string;
|
|
643
|
+
readonly reason: string;
|
|
644
|
+
}[];
|
|
641
645
|
};
|
|
642
646
|
};
|
|
643
647
|
healthMonitor?: ProviderHealthMonitorConfig;
|
package/src/lint.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
|
|
1
3
|
import type { ZodType } from "zod";
|
|
2
4
|
|
|
3
5
|
import {
|
|
@@ -7,6 +9,17 @@ import {
|
|
|
7
9
|
import { lintPublicSchemaFieldNames } from "./public-schema-field-lint.js";
|
|
8
10
|
import { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_SENSITIVE_META_KEY } from "./schema.js";
|
|
9
11
|
|
|
12
|
+
const requireModule = createRequire(import.meta.url);
|
|
13
|
+
// `typeof import(...)` keeps the type without emitting a static import: the
|
|
14
|
+
// typescript package is a CLI-only dependency and src/lint.ts is production
|
|
15
|
+
// runtime, which the typescript-import-boundary test enforces.
|
|
16
|
+
let typeScriptModule: typeof import("typescript") | undefined;
|
|
17
|
+
|
|
18
|
+
function getTypeScript(): typeof import("typescript") {
|
|
19
|
+
typeScriptModule ??= requireModule("typescript") as typeof import("typescript");
|
|
20
|
+
return typeScriptModule;
|
|
21
|
+
}
|
|
22
|
+
|
|
10
23
|
type AuthModeLike =
|
|
11
24
|
| "none"
|
|
12
25
|
| "platform-managed"
|
|
@@ -120,6 +133,10 @@ function isAuthLifecycleOperationId(operationId: string, authMode: string): bool
|
|
|
120
133
|
|
|
121
134
|
type ProviderContractMetaLike = {
|
|
122
135
|
publicSchemaFieldNames?: "normalized";
|
|
136
|
+
pinnedWireFieldPaths?: readonly {
|
|
137
|
+
readonly path: string;
|
|
138
|
+
readonly reason: string;
|
|
139
|
+
}[];
|
|
123
140
|
};
|
|
124
141
|
|
|
125
142
|
type SchemaLike = ZodType & {
|
|
@@ -153,6 +170,17 @@ type ProviderLintOptions = {
|
|
|
153
170
|
mode?: ProviderLintMode;
|
|
154
171
|
};
|
|
155
172
|
|
|
173
|
+
export interface ProviderLintInformation {
|
|
174
|
+
rule: string;
|
|
175
|
+
message: string;
|
|
176
|
+
field?: string;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface ProviderLintResult {
|
|
180
|
+
diagnostics: LintDiagnostic[];
|
|
181
|
+
information: ProviderLintInformation[];
|
|
182
|
+
}
|
|
183
|
+
|
|
156
184
|
type ProviderSourceLike = {
|
|
157
185
|
authFlowSource?: string;
|
|
158
186
|
providerSourceFiles?: Record<string, string>;
|
|
@@ -894,6 +922,146 @@ function lintSelfHostedBrowserPatterns(
|
|
|
894
922
|
const THROWN_ERROR_CONSTRUCTION_PATTERN = /new\s+(?:ProviderError|ValidationError)\s*\(/g;
|
|
895
923
|
|
|
896
924
|
const TEST_SOURCE_FILE_PATTERN = /(?:^|\/)(?:__tests__|__mocks__)\/|\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
925
|
+
const RECORDED_FIXTURE_SOURCE_FILE_PATTERN =
|
|
926
|
+
/(?:^|\/)__fixtures__(?:\/|$)|(?:^|\/)__tests__\/fixtures(?:\/|$)/;
|
|
927
|
+
const JAVASCRIPT_SOURCE_FILE_PATTERN = /\.[cm]?[jt]sx?$/;
|
|
928
|
+
const VERSIONED_PROFILE_LITERAL_PATTERN =
|
|
929
|
+
/\b(?:chrome|chromium|firefox|safari|edge|opera|ios[-_]safari)[-_]\d+(?:[._-]\d+)*(?=$|[^A-Za-z0-9])/i;
|
|
930
|
+
const VERSIONED_USER_AGENT_PATTERN = /\b(?:Chrome|CriOS|Firefox|FxiOS|EdgA?|OPR)\/\d+(?:\.\d+)*/i;
|
|
931
|
+
const VERSIONED_SAFARI_USER_AGENT_PATTERN = /\bVersion\/(\d+(?:\.\d+)*)(?=[\s\S]*\bSafari\/\d)/i;
|
|
932
|
+
const VERSIONED_CLIENT_HINT_PATTERN = /(?:^|[;,\s])v\s*=\s*["']?\d+/i;
|
|
933
|
+
|
|
934
|
+
type BrowserVersionLiteralKind = "profile" | "user-agent" | "sec-ch-ua";
|
|
935
|
+
|
|
936
|
+
type BrowserVersionLiteralFinding = {
|
|
937
|
+
kind: BrowserVersionLiteralKind;
|
|
938
|
+
literal: string;
|
|
939
|
+
position: number;
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
function staticStringText(node: import("typescript").Node | undefined): string | undefined {
|
|
943
|
+
if (!node) return undefined;
|
|
944
|
+
const ts = getTypeScript();
|
|
945
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text;
|
|
946
|
+
return undefined;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function staticPropertyName(node: import("typescript").PropertyName): string | undefined {
|
|
950
|
+
const ts = getTypeScript();
|
|
951
|
+
if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) {
|
|
952
|
+
return node.text;
|
|
953
|
+
}
|
|
954
|
+
if (ts.isComputedPropertyName(node)) return staticStringText(node.expression);
|
|
955
|
+
return undefined;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function isSecChUaHeaderName(value: string | undefined): boolean {
|
|
959
|
+
return value?.toLowerCase() === "sec-ch-ua";
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function collectBrowserVersionLiteralFindings(source: string): BrowserVersionLiteralFinding[] {
|
|
963
|
+
const ts = getTypeScript();
|
|
964
|
+
const sourceFile = ts.createSourceFile(
|
|
965
|
+
"provider-source.ts",
|
|
966
|
+
source,
|
|
967
|
+
ts.ScriptTarget.Latest,
|
|
968
|
+
true,
|
|
969
|
+
ts.ScriptKind.TSX,
|
|
970
|
+
);
|
|
971
|
+
const findings: BrowserVersionLiteralFinding[] = [];
|
|
972
|
+
const seen = new Set<string>();
|
|
973
|
+
|
|
974
|
+
const addFinding = (kind: BrowserVersionLiteralKind, literal: string, position: number) => {
|
|
975
|
+
const key = `${kind}:${position}:${literal}`;
|
|
976
|
+
if (seen.has(key)) return;
|
|
977
|
+
seen.add(key);
|
|
978
|
+
findings.push({ kind, literal, position });
|
|
979
|
+
};
|
|
980
|
+
|
|
981
|
+
const inspectLiteral = (text: string, position: number) => {
|
|
982
|
+
const profile = text.match(VERSIONED_PROFILE_LITERAL_PATTERN)?.[0];
|
|
983
|
+
if (profile) addFinding("profile", profile, position);
|
|
984
|
+
|
|
985
|
+
const userAgent =
|
|
986
|
+
text.match(VERSIONED_USER_AGENT_PATTERN)?.[0] ??
|
|
987
|
+
text.match(VERSIONED_SAFARI_USER_AGENT_PATTERN)?.[0];
|
|
988
|
+
if (userAgent) addFinding("user-agent", userAgent, position);
|
|
989
|
+
};
|
|
990
|
+
|
|
991
|
+
const inspectSecChUaValue = (node: import("typescript").Node | undefined) => {
|
|
992
|
+
const text = staticStringText(node);
|
|
993
|
+
if (text && VERSIONED_CLIENT_HINT_PATTERN.test(text)) {
|
|
994
|
+
addFinding("sec-ch-ua", "sec-ch-ua", node!.getStart(sourceFile));
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
const visit = (node: import("typescript").Node) => {
|
|
999
|
+
const text = staticStringText(node);
|
|
1000
|
+
if (text !== undefined) inspectLiteral(text, node.getStart(sourceFile));
|
|
1001
|
+
|
|
1002
|
+
if (ts.isPropertyAssignment(node) && isSecChUaHeaderName(staticPropertyName(node.name))) {
|
|
1003
|
+
inspectSecChUaValue(node.initializer);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
if (ts.isCallExpression(node) && isSecChUaHeaderName(staticStringText(node.arguments[0]))) {
|
|
1007
|
+
inspectSecChUaValue(node.arguments[1]);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
if (
|
|
1011
|
+
ts.isArrayLiteralExpression(node) &&
|
|
1012
|
+
isSecChUaHeaderName(staticStringText(node.elements[0]))
|
|
1013
|
+
) {
|
|
1014
|
+
inspectSecChUaValue(node.elements[1]);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
ts.forEachChild(node, visit);
|
|
1018
|
+
};
|
|
1019
|
+
|
|
1020
|
+
visit(sourceFile);
|
|
1021
|
+
return findings.sort((left, right) => left.position - right.position);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function browserVersionLiteralMessage(finding: BrowserVersionLiteralFinding): string {
|
|
1025
|
+
switch (finding.kind) {
|
|
1026
|
+
case "profile":
|
|
1027
|
+
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".`;
|
|
1028
|
+
case "user-agent":
|
|
1029
|
+
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.`;
|
|
1030
|
+
case "sec-ch-ua":
|
|
1031
|
+
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.';
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function lintBrowserVersionLiterals(provider: ProviderSourceLike): LintDiagnostic[] {
|
|
1036
|
+
const sources: Array<{ field: string; source: string }> = [];
|
|
1037
|
+
const sourceFiles = Object.entries(provider.providerSourceFiles ?? {}).filter(
|
|
1038
|
+
([filePath]) =>
|
|
1039
|
+
JAVASCRIPT_SOURCE_FILE_PATTERN.test(filePath) &&
|
|
1040
|
+
!TEST_SOURCE_FILE_PATTERN.test(filePath) &&
|
|
1041
|
+
!RECORDED_FIXTURE_SOURCE_FILE_PATTERN.test(filePath),
|
|
1042
|
+
);
|
|
1043
|
+
if (sourceFiles.length > 0) {
|
|
1044
|
+
for (const [filePath, source] of sourceFiles) {
|
|
1045
|
+
sources.push({ field: `sourceFiles.${filePath}`, source });
|
|
1046
|
+
}
|
|
1047
|
+
} else {
|
|
1048
|
+
if (provider.authFlowSource)
|
|
1049
|
+
sources.push({ field: "auth.flow", source: provider.authFlowSource });
|
|
1050
|
+
for (const [operationKey, operation] of Object.entries(provider.operations ?? {})) {
|
|
1051
|
+
const source = getOperationSource(operation);
|
|
1052
|
+
if (source) sources.push({ field: `operations.${operationKey}.handler`, source });
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
return sources.flatMap(({ field, source }) =>
|
|
1057
|
+
collectBrowserVersionLiteralFindings(source).map((finding) => ({
|
|
1058
|
+
rule: "browser-version-literal",
|
|
1059
|
+
level: "error" as const,
|
|
1060
|
+
field,
|
|
1061
|
+
message: browserVersionLiteralMessage(finding),
|
|
1062
|
+
})),
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
897
1065
|
|
|
898
1066
|
/**
|
|
899
1067
|
* Skips a string literal starting at `startIndex` (which must point at the
|
|
@@ -1298,6 +1466,67 @@ export function lintOperation(op: {
|
|
|
1298
1466
|
return diagnostics;
|
|
1299
1467
|
}
|
|
1300
1468
|
|
|
1469
|
+
function declaredPinnedWireFieldPaths(provider: {
|
|
1470
|
+
meta?: { contract?: ProviderContractMetaLike };
|
|
1471
|
+
}): readonly { readonly path: string; readonly reason: string }[] {
|
|
1472
|
+
const pins = provider.meta?.contract?.pinnedWireFieldPaths;
|
|
1473
|
+
if (!Array.isArray(pins)) return [];
|
|
1474
|
+
return pins.filter(
|
|
1475
|
+
(pin) =>
|
|
1476
|
+
pin !== null &&
|
|
1477
|
+
typeof pin === "object" &&
|
|
1478
|
+
typeof pin.path === "string" &&
|
|
1479
|
+
pin.path.trim().length > 0 &&
|
|
1480
|
+
typeof pin.reason === "string" &&
|
|
1481
|
+
pin.reason.trim().length > 0,
|
|
1482
|
+
);
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
function applyPinnedWireFieldPaths(
|
|
1486
|
+
provider: { meta?: { contract?: ProviderContractMetaLike } },
|
|
1487
|
+
diagnostics: readonly LintDiagnostic[],
|
|
1488
|
+
): ProviderLintResult {
|
|
1489
|
+
const pins = declaredPinnedWireFieldPaths(provider);
|
|
1490
|
+
if (pins.length === 0) {
|
|
1491
|
+
return { diagnostics: [...diagnostics], information: [] };
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
const pinsByPath = new Map(pins.map((pin) => [pin.path, pin]));
|
|
1495
|
+
const matchedPaths = new Set<string>();
|
|
1496
|
+
const remainingDiagnostics = diagnostics.filter((diagnostic) => {
|
|
1497
|
+
if (
|
|
1498
|
+
diagnostic.rule !== "public-schema-upstream-field" ||
|
|
1499
|
+
diagnostic.field === undefined ||
|
|
1500
|
+
!pinsByPath.has(diagnostic.field)
|
|
1501
|
+
) {
|
|
1502
|
+
return true;
|
|
1503
|
+
}
|
|
1504
|
+
matchedPaths.add(diagnostic.field);
|
|
1505
|
+
return false;
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
const information: ProviderLintInformation[] = [];
|
|
1509
|
+
for (const pin of pins) {
|
|
1510
|
+
if (matchedPaths.has(pin.path)) {
|
|
1511
|
+
information.push({
|
|
1512
|
+
rule: "public-schema-pinned-wire-field",
|
|
1513
|
+
field: pin.path,
|
|
1514
|
+
message: `Suppressed exact public-schema-upstream-field diagnostic. Reason: ${pin.reason}`,
|
|
1515
|
+
});
|
|
1516
|
+
continue;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
remainingDiagnostics.push({
|
|
1520
|
+
rule: "public-schema-pinned-wire-field-stale",
|
|
1521
|
+
level: "error",
|
|
1522
|
+
field: pin.path,
|
|
1523
|
+
message: `Pinned wire field path ${JSON.stringify(pin.path)} matches no current public-schema-upstream-field diagnostic; remove the stale declaration.`,
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
return { diagnostics: remainingDiagnostics, information };
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1301
1530
|
export function lintProvider(
|
|
1302
1531
|
provider: {
|
|
1303
1532
|
id?: string;
|
|
@@ -1340,6 +1569,14 @@ export function lintProvider(
|
|
|
1340
1569
|
},
|
|
1341
1570
|
options: ProviderLintOptions = {},
|
|
1342
1571
|
): LintDiagnostic[] {
|
|
1572
|
+
return lintProviderWithInformation(provider, options).diagnostics;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
/** Internal detailed result used by the CLI to render suppression audits. */
|
|
1576
|
+
export function lintProviderWithInformation(
|
|
1577
|
+
provider: Parameters<typeof lintProvider>[0],
|
|
1578
|
+
options: ProviderLintOptions = {},
|
|
1579
|
+
): ProviderLintResult {
|
|
1343
1580
|
const diagnostics: LintDiagnostic[] = [
|
|
1344
1581
|
...lintAllowedHosts(provider.id, provider.allowedHosts),
|
|
1345
1582
|
...lintReviewed(provider.id, provider.reviewed),
|
|
@@ -1348,6 +1585,7 @@ export function lintProvider(
|
|
|
1348
1585
|
...lintCredentialWriteUsage(provider),
|
|
1349
1586
|
...lintPlaywrightDirectImports(provider),
|
|
1350
1587
|
...lintSelfHostedBrowserPatterns(provider, options),
|
|
1588
|
+
...lintBrowserVersionLiterals(provider),
|
|
1351
1589
|
...lintUndeclaredThrownErrorCodes(provider),
|
|
1352
1590
|
];
|
|
1353
1591
|
|
|
@@ -1375,7 +1613,7 @@ export function lintProvider(
|
|
|
1375
1613
|
}
|
|
1376
1614
|
|
|
1377
1615
|
if (!provider.operations) {
|
|
1378
|
-
return diagnostics;
|
|
1616
|
+
return applyPinnedWireFieldPaths(provider, diagnostics);
|
|
1379
1617
|
}
|
|
1380
1618
|
|
|
1381
1619
|
diagnostics.push(
|
|
@@ -1411,5 +1649,5 @@ export function lintProvider(
|
|
|
1411
1649
|
),
|
|
1412
1650
|
);
|
|
1413
1651
|
|
|
1414
|
-
return diagnostics;
|
|
1652
|
+
return applyPinnedWireFieldPaths(provider, diagnostics);
|
|
1415
1653
|
}
|
package/src/runtime/insights.ts
CHANGED
|
@@ -27,7 +27,7 @@ const DNS_WARN_MS = 5;
|
|
|
27
27
|
const BROWSER_IDLE_MS = 5_000;
|
|
28
28
|
const REFRESH_WARN_RATE = 0.1;
|
|
29
29
|
|
|
30
|
-
const TLS_REUSE_FIX = `const session = ctx.stealth.createSession({ profile: 'chrome-
|
|
30
|
+
const TLS_REUSE_FIX = `const session = ctx.stealth.createSession({ profile: 'chrome-desktop' });
|
|
31
31
|
const resp = await session.fetch(url, opts);`;
|
|
32
32
|
|
|
33
33
|
const TRANSFORM_FIX = `transformResponse: (raw) => {
|
|
@@ -39,7 +39,7 @@ const LARGE_RESPONSE_FIX = `const resp = await ctx.http.get('/items', {
|
|
|
39
39
|
});`;
|
|
40
40
|
|
|
41
41
|
const DNS_FIX = `// Enable DNS caching or reuse a long-lived session per host.
|
|
42
|
-
const session = ctx.stealth.createSession({ profile: 'chrome-
|
|
42
|
+
const session = ctx.stealth.createSession({ profile: 'chrome-desktop' });
|
|
43
43
|
await session.fetch(url, opts);`;
|
|
44
44
|
|
|
45
45
|
const PROXY_FIX = `// Re-check whether this operation really needs a proxy.
|