@apifuse/provider-sdk 2.2.0-beta.43 → 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.
@@ -1,4 +1,11 @@
1
+ import { createRequire } from "node:module";
1
2
  import { SDKError } from "../errors.js";
3
+ const requireModule = createRequire(import.meta.url);
4
+ let wreqProfileApi;
5
+ function getWreqProfileApi() {
6
+ wreqProfileApi ??= requireModule("wreq-js");
7
+ return wreqProfileApi;
8
+ }
2
9
  const CHROMIUM_HEADER_ORDER = [
3
10
  ":method",
4
11
  ":authority",
@@ -65,6 +72,50 @@ const SAFARI_H2_SETTINGS = {
65
72
  const CHROMIUM_JA3 = "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513-65037,29-23-24,0";
66
73
  const FIREFOX_JA3 = "771,4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-28-27-43-45-51,29-23-24-25,0";
67
74
  const SAFARI_JA3 = "771,4865-4866-4867-49196-49195-52393-49200-49199-49188-49192-159-158-107-103-57-51-157-156-61-60-53-47-255,0-23-65281-10-11-16-5-13-18-51-45-43-27,29-23-24-25,0";
75
+ /**
76
+ * The newest Chromium build wreq-js can emulate, resolved on first profile use.
77
+ *
78
+ * A literal version here rots: it went stale twice (146 was six releases behind
79
+ * stable when an upstream integrity analyzer flagged it), and a fingerprint that
80
+ * advertises an old Chrome is exactly what bot managers score against. wreq-js
81
+ * already ships the profile table, so the newest entry is derived from it rather
82
+ * than restated. `resolveProfile("chrome")` is deliberately NOT used because its
83
+ * conservative default can lag newer entries in that table.
84
+ *
85
+ * package.json pins wreq-js exactly on purpose. Even a semver-minor wreq-js update
86
+ * can change the profile table, emitted headers, native bindings, and therefore
87
+ * the wire fingerprint inherited by every provider. Upgrade that exact version
88
+ * only in a dedicated change with profile-parity and packed-native verification.
89
+ */
90
+ function resolveLatestChromiumProfile() {
91
+ const { getProfiles } = getWreqProfileApi();
92
+ let newest;
93
+ for (const name of getProfiles()) {
94
+ const match = /^chrome_(\d+)$/.exec(name);
95
+ if (!match)
96
+ continue;
97
+ const version = Number(match[1]);
98
+ if (!newest || version > newest.version)
99
+ newest = { name, version };
100
+ }
101
+ if (!newest) {
102
+ throw new SDKError("wreq-js exposes no chrome_<version> emulation profile.", {
103
+ code: "STEALTH_PROFILE_UNAVAILABLE",
104
+ });
105
+ }
106
+ return { wreqName: newest.name, version: `${newest.version}.0.0.0` };
107
+ }
108
+ /** The user agent wreq-js itself emits for that profile, so the two never disagree. */
109
+ function chromiumUserAgent(latest) {
110
+ const { getEmulationHeaders } = getWreqProfileApi();
111
+ for (const [name, value] of getEmulationHeaders(latest.wreqName, "macos")) {
112
+ if (String(name).toLowerCase() === "user-agent")
113
+ return String(value);
114
+ }
115
+ throw new SDKError(`wreq-js profile ${latest.wreqName} exposes no user-agent header.`, {
116
+ code: "STEALTH_PROFILE_UNAVAILABLE",
117
+ });
118
+ }
68
119
  function createProfile(name, definition) {
69
120
  return {
70
121
  name,
@@ -120,10 +171,21 @@ export function generateLayer2Headers(profile) {
120
171
  }
121
172
  return headers;
122
173
  }
123
- const STEALTH_PROFILE_ALIASES = {
124
- "chrome-desktop": "chrome-146",
174
+ const STATIC_STEALTH_PROFILE_ALIASES = {
175
+ "firefox-desktop": "firefox-147",
176
+ "safari-desktop": "safari-17",
177
+ "safari-mobile": "ios-safari-26",
125
178
  };
126
- const STEALTH_PROFILES = {
179
+ const PUBLIC_STEALTH_PROFILE_NAMES = [
180
+ "chrome-desktop",
181
+ "firefox-desktop",
182
+ "safari-desktop",
183
+ "safari-mobile",
184
+ "generic-desktop",
185
+ "generic-mobile",
186
+ ];
187
+ const PUBLIC_STEALTH_PROFILE_NAME_SET = new Set(PUBLIC_STEALTH_PROFILE_NAMES);
188
+ const STATIC_STEALTH_PROFILES = {
127
189
  "chrome-146": createProfile("chrome-146", {
128
190
  platform: "macos",
129
191
  version: "146.0.0.0",
@@ -223,15 +285,6 @@ const STEALTH_PROFILES = {
223
285
  h2Settings: SAFARI_H2_SETTINGS,
224
286
  headerOrder: SAFARI_HEADER_ORDER,
225
287
  }),
226
- "generic-desktop": createProfile("generic-desktop", {
227
- platform: "macos",
228
- version: "146.0.0.0",
229
- userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
230
- tlsClientIdentifier: "chrome_146",
231
- ja3: CHROMIUM_JA3,
232
- h2Settings: CHROMIUM_H2_SETTINGS,
233
- headerOrder: CHROMIUM_HEADER_ORDER,
234
- }),
235
288
  "generic-mobile": createProfile("generic-mobile", {
236
289
  platform: "ios",
237
290
  version: "26.0",
@@ -242,9 +295,38 @@ const STEALTH_PROFILES = {
242
295
  headerOrder: SAFARI_HEADER_ORDER,
243
296
  }),
244
297
  };
298
+ let stealthProfileCatalog;
299
+ function getStealthProfileCatalog() {
300
+ if (stealthProfileCatalog)
301
+ return stealthProfileCatalog;
302
+ const latest = resolveLatestChromiumProfile();
303
+ const currentName = `chrome-${latest.version.split(".")[0]}`;
304
+ const currentProfile = createProfile(currentName, {
305
+ platform: "macos",
306
+ version: latest.version,
307
+ userAgent: chromiumUserAgent(latest),
308
+ tlsClientIdentifier: latest.wreqName,
309
+ ja3: CHROMIUM_JA3,
310
+ h2Settings: CHROMIUM_H2_SETTINGS,
311
+ headerOrder: CHROMIUM_HEADER_ORDER,
312
+ });
313
+ stealthProfileCatalog = {
314
+ aliases: {
315
+ "chrome-desktop": currentName,
316
+ ...STATIC_STEALTH_PROFILE_ALIASES,
317
+ },
318
+ profiles: {
319
+ [currentName]: currentProfile,
320
+ ...STATIC_STEALTH_PROFILES,
321
+ "generic-desktop": createProfile("generic-desktop", currentProfile),
322
+ },
323
+ };
324
+ return stealthProfileCatalog;
325
+ }
245
326
  export function getStealthProfile(name) {
246
- const canonicalName = STEALTH_PROFILE_ALIASES[name] ?? name;
247
- const profile = STEALTH_PROFILES[canonicalName];
327
+ const catalog = getStealthProfileCatalog();
328
+ const canonicalName = catalog.aliases[name] ?? name;
329
+ const profile = catalog.profiles[canonicalName];
248
330
  if (!profile) {
249
331
  throw new SDKError(`Unknown stealth profile: ${name}`);
250
332
  }
@@ -254,6 +336,27 @@ export function getStealthProfile(name) {
254
336
  headerOrder: profile.headerOrder ? [...profile.headerOrder] : undefined,
255
337
  };
256
338
  }
339
+ /** Returns the intent alias that replaces a registered version-pinned profile. */
340
+ export function getStealthProfileIntentAlias(name) {
341
+ if (PUBLIC_STEALTH_PROFILE_NAME_SET.has(name))
342
+ return undefined;
343
+ if (/^(?:chrome|chromium|edge)[-_]\d/i.test(name)) {
344
+ return "chrome-desktop";
345
+ }
346
+ if (/^firefox[-_]\d/i.test(name))
347
+ return "firefox-desktop";
348
+ if (/^(?:ios[-_]safari|safari[-_](?:ios|ipad))[-_]\d/i.test(name)) {
349
+ return "safari-mobile";
350
+ }
351
+ if (/^safari[-_]\d/i.test(name))
352
+ return "safari-desktop";
353
+ return undefined;
354
+ }
355
+ /** Internal compatibility catalog used by transport-parity tests. */
356
+ export function listRegisteredStealthProfiles() {
357
+ const catalog = getStealthProfileCatalog();
358
+ return [...Object.keys(catalog.profiles), ...Object.keys(catalog.aliases)];
359
+ }
257
360
  export function listStealthProfiles() {
258
- return [...Object.keys(STEALTH_PROFILES), ...Object.keys(STEALTH_PROFILE_ALIASES)];
361
+ return [...PUBLIC_STEALTH_PROFILE_NAMES];
259
362
  }
package/dist/types.d.ts CHANGED
@@ -956,6 +956,10 @@ export interface ProviderMeta {
956
956
  publicProfile?: ProviderPublicProfile;
957
957
  contract?: {
958
958
  publicSchemaFieldNames?: "normalized";
959
+ readonly pinnedWireFieldPaths?: readonly {
960
+ readonly path: string;
961
+ readonly reason: string;
962
+ }[];
959
963
  };
960
964
  }
961
965
  export type RequestParamPrimitive = string | number | boolean | null | undefined;
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.2.0-beta.43",
2
+ "version": "2.2.0-beta.45",
3
3
  "name": "@apifuse/provider-sdk",
4
4
  "private": false,
5
5
  "type": "module",
@@ -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
  }
@@ -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-146' });
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-146' });
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.
@@ -18,7 +18,7 @@ import {
18
18
  vendorFromResolvedSource,
19
19
  } from "../config/loader.js";
20
20
  import { SDKError, TransportError } from "../errors.js";
21
- import { getStealthProfile } from "../stealth/profiles.js";
21
+ import { getStealthProfile, getStealthProfileIntentAlias } from "../stealth/profiles.js";
22
22
  import type {
23
23
  HttpMethod,
24
24
  StealthClient,
@@ -67,7 +67,7 @@ import {
67
67
  serializeRequestUrl,
68
68
  } from "./request-options.js";
69
69
 
70
- export const DEFAULT_PROFILE = "chrome-146";
70
+ export const DEFAULT_PROFILE = "chrome-desktop";
71
71
 
72
72
  const MISSING_PROXY_WARNING =
73
73
  "[provider-sdk] Provider requested proxy routing, but no proxy URL was configured. Continuing without proxy.";
@@ -244,7 +244,12 @@ function resolveDefaultWreqProfileMapping(): { identifier: string; os: Emulation
244
244
  return { identifier, os: profile.platform };
245
245
  }
246
246
 
247
- const DEFAULT_WREQ_PROFILE_MAPPING = resolveDefaultWreqProfileMapping();
247
+ let defaultWreqProfileMapping: ReturnType<typeof resolveDefaultWreqProfileMapping> | undefined;
248
+
249
+ function getDefaultWreqProfileMapping(): ReturnType<typeof resolveDefaultWreqProfileMapping> {
250
+ defaultWreqProfileMapping ??= resolveDefaultWreqProfileMapping();
251
+ return defaultWreqProfileMapping;
252
+ }
248
253
 
249
254
  export function resolveWreqProfile(
250
255
  profileName: string,
@@ -268,8 +273,9 @@ export function resolveWreqProfile(
268
273
  // profile strings still run with the transport default instead of failing
269
274
  // before the request starts. Removed built-in profile aliases above remain
270
275
  // explicit errors so callers do not accidentally pin retired fingerprints.
271
- identifier = DEFAULT_WREQ_PROFILE_MAPPING.identifier;
272
- os = DEFAULT_WREQ_PROFILE_MAPPING.os;
276
+ const defaultMapping = getDefaultWreqProfileMapping();
277
+ identifier = defaultMapping.identifier;
278
+ os = defaultMapping.os;
273
279
  }
274
280
 
275
281
  const browser = closestWreqProfile(identifier, wreqProfiles);
@@ -772,6 +778,12 @@ function createSessionFetcher(
772
778
  let closed = false;
773
779
  let hasWarnedMissingProxy = false;
774
780
  const warn = clientOptions.warn ?? console.warn;
781
+ const intentAlias = getStealthProfileIntentAlias(defaultProfile);
782
+ if (intentAlias) {
783
+ warn(
784
+ `[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.`,
785
+ );
786
+ }
775
787
  const cookieJar = new StealthCookieJar([], baseUrl);
776
788
 
777
789
  async function getClientEntry(