@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 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-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,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-146";
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