@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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @apifuse/provider-sdk Changelog
2
2
 
3
+ ## 2.2.0-beta.45
4
+
5
+ - Release candidate for main commit 5d337d9fc1c77cd814ec7a4e1aa44ba0f8b2a5a9.
6
+
7
+ ## 2.2.0-beta.44
8
+
9
+ - Release candidate for main commit a3091c0b4931c5ae48fe01b9776e4dc213db2716.
10
+
3
11
  ## 2.2.0-beta.43
4
12
 
5
13
  - Release candidate for main commit 5b8b6048514fb500ead400495bd5e1ed8695ed6d.
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-146`; do not tune JA3, HTTP/2 SETTINGS, or
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
@@ -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 { lintProvider, type ProviderLintMode } from "../src/lint.js";
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 diagnostics = lintProvider({ ...provider, providerSourceFiles }, { mode: lintMode });
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 = diagnostics.map((diagnostic) => {
385
- const field = diagnostic.field ? `${diagnostic.field}: ` : "";
386
- return `${diagnostic.level.toUpperCase()} ${diagnostic.rule} ${field}${diagnostic.message}`;
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
  }
@@ -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-146' });
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-146' });
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-146";
4
+ export declare const DEFAULT_PROFILE = "chrome-desktop";
5
5
  export type StealthClientOptions = ProxyResolutionOptions & {
6
6
  warn?: (message: string) => void;
7
7
  /**
@@ -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-146";
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
- const DEFAULT_WREQ_PROFILE_MAPPING = resolveDefaultWreqProfileMapping();
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
- identifier = DEFAULT_WREQ_PROFILE_MAPPING.identifier;
129
- os = DEFAULT_WREQ_PROFILE_MAPPING.os;
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) {
@@ -9,7 +9,7 @@ import { validateFailClosedDeclaration } from "../declaration-validation.js";
9
9
  import { safeProviderErrorObservability } from "../error-observability.js";
10
10
  import { SDK_OWNED_PROVIDER_ERROR_CODES, SDK_RUNTIME_OWNED_ERROR_CODES, SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES, } from "../error-resolution.js";
11
11
  import { AuthError, isProviderError, isSessionExpiredError, isTransportError, isValidationError, ProviderError, } from "../errors.js";
12
- import { REDACTED_FIXTURE_VALUE, sanitizeDiagnosticText, } from "../fixture-sanitization.js";
12
+ import { sanitizeDiagnosticText } from "../fixture-sanitization.js";
13
13
  import { loadProviderLocaleCatalogs, localizeAuthTurn, } from "../i18n/catalog.js";
14
14
  import { categoryForStatus, sourceForCategory, isRetryableCategory, PROVIDER_OBSERVABILITY_TAXONOMY_VERSION, } from "../observability.js";
15
15
  import { createScratchpad } from "../runtime/auth-flow.js";
@@ -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, stealthProfile.name, stealthClientOptions)
449
+ ? capabilityModules.stealth.createStealthClient(stealthBaseUrl, provider.stealth.profile, stealthClientOptions)
450
450
  : capabilityModules.stealth.createStealthClient(stealthBaseUrl, stealthClientOptions)
451
451
  : stealthProfile
452
- ? createLazyStealthClient(logStealthCleanupError, stealthBaseUrl, stealthProfile.name, stealthClientOptions)
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, stealthProfile.name, stealthClientOptions)
596
+ ? capabilityModules.stealth.createStealthClient(stealthBaseUrl, provider.stealth.profile, stealthClientOptions)
597
597
  : capabilityModules.stealth.createStealthClient(stealthBaseUrl, stealthClientOptions)
598
598
  : stealthProfile
599
- ? createLazyStealthClient(logStealthCleanupError, stealthBaseUrl, stealthProfile.name, stealthClientOptions)
599
+ ? createLazyStealthClient(logStealthCleanupError, stealthBaseUrl, provider.stealth.profile, stealthClientOptions)
600
600
  : createLazyStealthClient(logStealthCleanupError, stealthBaseUrl, stealthClientOptions)
601
601
  : createStealthStub(),
602
602
  ...(provider.native
@@ -979,47 +979,8 @@ function extractRequestId(raw) {
979
979
  }
980
980
  const MAX_PROVIDER_ERROR_CAUSE_FRAMES = 5;
981
981
  const MAX_PROVIDER_ERROR_CAUSE_MESSAGE_LENGTH = 300;
982
- const UNSTRUCTURED_PROVIDER_ERROR_CAUSE_MESSAGE = "[UNSTRUCTURED_UPSTREAM_TEXT]";
983
- const PROVIDER_ERROR_CAUSE_RETAINED_URL_RUN = /https?:\/\/[^\s"'<>]+/giu;
984
- const PROVIDER_ERROR_CAUSE_TOKEN_RUN = /\S+/gu;
985
- const PROVIDER_ERROR_CAUSE_TOKEN_EDGE_PUNCTUATION = /^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu;
986
- const STRUCTURALLY_SAFE_PROVIDER_ERROR_CAUSE_WORDS = new Set([
987
- "completion",
988
- "diagnostic",
989
- "provider",
990
- "rejected",
991
- "returned",
992
- "upstream",
993
- ]);
994
- /**
995
- * Cause frames fail closed when sanitization leaves a plausible credential-shaped free-text run.
996
- * Redaction sentinels and retained URLs are ignored. Every other whitespace token (or each side
997
- * of a structured key=value token) with at least eight Unicode characters must reduce to this
998
- * small vocabulary drawn from SDK diagnostics; counting punctuation keeps bare passwords opaque.
999
- */
1000
- function isStructurallySafeProviderErrorCauseMessage(message) {
1001
- const classifiableMessage = message
1002
- .replaceAll(REDACTED_FIXTURE_VALUE, " ")
1003
- .replace(PROVIDER_ERROR_CAUSE_RETAINED_URL_RUN, " ");
1004
- for (const match of classifiableMessage.matchAll(PROVIDER_ERROR_CAUSE_TOKEN_RUN)) {
1005
- const token = match[0];
1006
- for (const run of token.split("=")) {
1007
- const diagnosticWord = run
1008
- .replace(PROVIDER_ERROR_CAUSE_TOKEN_EDGE_PUNCTUATION, "")
1009
- .toLowerCase();
1010
- if (STRUCTURALLY_SAFE_PROVIDER_ERROR_CAUSE_WORDS.has(diagnosticWord))
1011
- continue;
1012
- if ([...run].length >= 8)
1013
- return false;
1014
- }
1015
- }
1016
- return true;
1017
- }
1018
982
  function providerErrorCauseMessage(message) {
1019
983
  const sanitized = sanitizeDiagnosticText(message);
1020
- if (!isStructurallySafeProviderErrorCauseMessage(sanitized)) {
1021
- return UNSTRUCTURED_PROVIDER_ERROR_CAUSE_MESSAGE;
1022
- }
1023
984
  return sanitized.length > MAX_PROVIDER_ERROR_CAUSE_MESSAGE_LENGTH
1024
985
  ? `${sanitized.slice(0, MAX_PROVIDER_ERROR_CAUSE_MESSAGE_LENGTH)}… [truncated]`
1025
986
  : sanitized;
@@ -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[];