@apifuse/provider-sdk 2.2.0-beta.67 → 2.2.0-beta.69

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.
Files changed (77) hide show
  1. package/AUTHORING.md +34 -2
  2. package/CHANGELOG.md +8 -0
  3. package/README.md +18 -7
  4. package/bin/apifuse-record.ts +81 -21
  5. package/bin/apifuse-submit-check.ts +170 -28
  6. package/dist/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl +138 -23
  7. package/dist/cli/templates/provider/README.md.tpl +17 -9
  8. package/dist/config/loader.d.ts +16 -0
  9. package/dist/config/loader.js +25 -8
  10. package/dist/error-resolution.d.ts +13 -0
  11. package/dist/error-resolution.js +72 -35
  12. package/dist/fixture-sanitization.js +159 -15
  13. package/dist/lint.d.ts +2 -0
  14. package/dist/lint.js +549 -11
  15. package/dist/runtime/diagnostic-redactor.d.ts +6 -1
  16. package/dist/runtime/diagnostic-redactor.js +16 -14
  17. package/dist/runtime/executor.js +3 -0
  18. package/dist/runtime/http.d.ts +2 -0
  19. package/dist/runtime/http.js +17 -103
  20. package/dist/runtime/instrumentation.js +4 -1
  21. package/dist/runtime/provider.d.ts +7 -0
  22. package/dist/runtime/provider.js +14 -0
  23. package/dist/runtime/redirect-hop-headers.d.ts +19 -0
  24. package/dist/runtime/redirect-hop-headers.js +58 -0
  25. package/dist/runtime/redirects.d.ts +33 -1
  26. package/dist/runtime/redirects.js +113 -0
  27. package/dist/runtime/resolver-span-attributes.d.ts +34 -0
  28. package/dist/runtime/resolver-span-attributes.js +93 -0
  29. package/dist/runtime/resolver-usage.d.ts +8 -0
  30. package/dist/runtime/resolver-usage.js +5 -11
  31. package/dist/runtime/resolver-vendors/browser.js +5 -4
  32. package/dist/runtime/resolver-vendors/capsolver.js +11 -10
  33. package/dist/runtime/resolver-vendors/twocaptcha.js +9 -8
  34. package/dist/runtime/resolver.js +7 -6
  35. package/dist/runtime/stealth-owned-headers.d.ts +21 -8
  36. package/dist/runtime/stealth-owned-headers.js +28 -8
  37. package/dist/runtime/stealth-telemetry.d.ts +1 -1
  38. package/dist/runtime/stealth.js +158 -46
  39. package/dist/runtime/trace.js +2 -2
  40. package/dist/server/index.d.ts +1 -0
  41. package/dist/server/serve-implementation.d.ts +2 -1
  42. package/dist/server/serve-implementation.js +45 -30
  43. package/dist/server/state-backend-report.d.ts +70 -0
  44. package/dist/server/state-backend-report.js +113 -0
  45. package/dist/testing/run.js +14 -7
  46. package/dist/trace-sanitization.js +1 -1
  47. package/dist/types.d.ts +30 -6
  48. package/package.json +2 -2
  49. package/src/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl +138 -23
  50. package/src/cli/templates/provider/README.md.tpl +17 -9
  51. package/src/config/loader.ts +49 -12
  52. package/src/error-resolution.ts +89 -35
  53. package/src/fixture-sanitization.ts +175 -17
  54. package/src/lint.ts +595 -10
  55. package/src/runtime/diagnostic-redactor.ts +19 -13
  56. package/src/runtime/executor.ts +3 -0
  57. package/src/runtime/http.ts +24 -112
  58. package/src/runtime/instrumentation.ts +4 -1
  59. package/src/runtime/provider.ts +15 -0
  60. package/src/runtime/redirect-hop-headers.ts +71 -0
  61. package/src/runtime/redirects.ts +150 -1
  62. package/src/runtime/resolver-span-attributes.ts +139 -0
  63. package/src/runtime/resolver-usage.ts +8 -5
  64. package/src/runtime/resolver-vendors/browser.ts +5 -4
  65. package/src/runtime/resolver-vendors/capsolver.ts +11 -10
  66. package/src/runtime/resolver-vendors/twocaptcha.ts +9 -8
  67. package/src/runtime/resolver.ts +7 -6
  68. package/src/runtime/stealth-owned-headers.ts +34 -8
  69. package/src/runtime/stealth-telemetry.ts +6 -1
  70. package/src/runtime/stealth.ts +180 -58
  71. package/src/runtime/trace.ts +6 -2
  72. package/src/server/index.ts +4 -0
  73. package/src/server/serve-implementation.ts +58 -31
  74. package/src/server/state-backend-report.ts +185 -0
  75. package/src/testing/run.ts +15 -2
  76. package/src/trace-sanitization.ts +1 -1
  77. package/src/types.ts +30 -6
@@ -4,6 +4,14 @@ import type { TraceRecorder } from "./trace.js";
4
4
  export declare const RESOLVER_PAID_USAGE_SPAN = "resolver.usage";
5
5
  export type ResolverPaidUsageOutcome = "success" | "vendor_error" | "timeout" | "abandoned";
6
6
  export type ResolverPaidUsageEndpoint = "capsolver:create_task" | "twocaptcha:create_task" | "hyper:ip" | "hyper:sbsd_create";
7
+ /**
8
+ * How one call to the endpoint is counted. `metered` endpoints are the vendors' task or
9
+ * payload creation calls: the SDK counts one unit per call (the vendor ledger stays
10
+ * authoritative for refunds of failed tasks). Hyper's `/ip` reflector is authenticated
11
+ * but has no published price, so it is recorded with 0 units and `billing: "unconfirmed"`
12
+ * rather than invented as a paid unit; the span still proves the call was made.
13
+ */
14
+ export type ResolverPaidUsageBilling = "metered" | "unconfirmed";
7
15
  /**
8
16
  * Records one `resolver.usage` span around a paid vendor call. Callers validate credentials
9
17
  * and transport first, so a span means a potentially billable request was attempted (a
@@ -1,11 +1,5 @@
1
+ import { resolverSpanAttributes } from "./resolver-span-attributes.js";
1
2
  export const RESOLVER_PAID_USAGE_SPAN = "resolver.usage";
2
- /**
3
- * How one call to the endpoint is counted. `metered` endpoints are the vendors' task or
4
- * payload creation calls: the SDK counts one unit per call (the vendor ledger stays
5
- * authoritative for refunds of failed tasks). Hyper's `/ip` reflector is authenticated
6
- * but has no published price, so it is recorded with 0 units and `billing: "unconfirmed"`
7
- * rather than invented as a paid unit; the span still proves the call was made.
8
- */
9
3
  const ENDPOINT_BILLING = {
10
4
  "capsolver:create_task": { billable_units: 1, billing: "metered" },
11
5
  "twocaptcha:create_task": { billable_units: 1, billing: "metered" },
@@ -42,7 +36,7 @@ export async function recordPaidResolverCreate(options) {
42
36
  }
43
37
  return options.create();
44
38
  }
45
- const baseAttributes = {
39
+ const baseAttributes = resolverSpanAttributes({
46
40
  vendor: options.vendor,
47
41
  challenge_kind: options.kind,
48
42
  endpoint: options.endpoint,
@@ -51,10 +45,10 @@ export async function recordPaidResolverCreate(options) {
51
45
  ...(options.usage ? { vendor_index: options.usage.vendorIndex } : {}),
52
46
  ...(options.round === undefined ? {} : { round: options.round }),
53
47
  resolver_identity_scope: options.usage?.resolverIdentityScope,
54
- };
48
+ });
55
49
  return options.traceRecorder.runSpan(RESOLVER_PAID_USAGE_SPAN, options.create, {
56
50
  attributes: baseAttributes,
57
- onSuccess: () => ({ outcome: "success" }),
58
- onError: (error) => ({ outcome: errorOutcome(error, options.signal) }),
51
+ onSuccess: () => resolverSpanAttributes({ outcome: "success" }),
52
+ onError: (error) => resolverSpanAttributes({ outcome: errorOutcome(error, options.signal) }),
59
53
  });
60
54
  }
@@ -1,5 +1,6 @@
1
1
  import { isProviderError } from "../../errors.js";
2
2
  import { createBrowserClient } from "../browser.js";
3
+ import { resolverSpanAttributes } from "../resolver-span-attributes.js";
3
4
  import { resolverChallengeIssuingIdentity } from "./bindings.js";
4
5
  import { assertResolverHostAllowed, normalizedResolverHostname } from "./hosts.js";
5
6
  import { ResolverVendorUnavailableError, } from "./types.js";
@@ -141,15 +142,15 @@ async function runBoundedCleanup(cleanup, timeoutMs, challengeKind, operation, t
141
142
  }
142
143
  await traceRecorder
143
144
  .runSpan("resolver.vendor.cleanup", boundedCleanup, {
144
- attributes: {
145
+ attributes: resolverSpanAttributes({
145
146
  vendor: BROWSER_VENDOR_ID,
146
147
  challenge_kind: challengeKind,
147
148
  operation,
148
- },
149
+ }),
149
150
  onError(error) {
150
- return {
151
+ return resolverSpanAttributes({
151
152
  error_message: error instanceof Error ? error.message : String(error),
152
- };
153
+ });
153
154
  },
154
155
  })
155
156
  .catch(() => undefined);
@@ -2,6 +2,7 @@ import { getStealthProfile } from "../../stealth/profiles.js";
2
2
  import { redactSensitiveText } from "../request-options.js";
3
3
  import { DEFAULT_STEALTH_PROFILE } from "../stealth.js";
4
4
  import { recordPaidResolverCreate } from "../resolver-usage.js";
5
+ import { resolverSpanAttributes } from "../resolver-span-attributes.js";
5
6
  import { assertResolverHostAllowed } from "./hosts.js";
6
7
  import { ResolverChallengeVerdictError, ResolverVendorUnavailableError, recordResolverVendorPoll, resolverVendorSupports, } from "./types.js";
7
8
  const CAPSOLVER_VENDOR_ID = "capsolver";
@@ -263,7 +264,7 @@ function endpoint(baseUrl, path) {
263
264
  }
264
265
  function spanErrorAttributes(error, phase) {
265
266
  if (error instanceof ResolverVendorUnavailableError) {
266
- return {
267
+ return resolverSpanAttributes({
267
268
  unavailability_reason: error.reason,
268
269
  transport_phase: error.phase,
269
270
  ...(error instanceof CapsolverUnavailableError
@@ -275,21 +276,21 @@ function spanErrorAttributes(error, phase) {
275
276
  response_length: error.responseLength,
276
277
  }
277
278
  : {}),
278
- };
279
+ });
279
280
  }
280
281
  if (error instanceof CapsolverVerdictError) {
281
- return {
282
+ return resolverSpanAttributes({
282
283
  verdict_reason: error.reason,
283
284
  transport_phase: error.phase,
284
285
  vendor_error_code: error.errorCode,
285
286
  vendor_error_description: error.errorDescription,
286
- };
287
+ });
287
288
  }
288
289
  if (error instanceof CapsolverSolveTimeoutError) {
289
- return {
290
+ return resolverSpanAttributes({
290
291
  unavailability_reason: "timeout",
291
292
  transport_phase: phase,
292
- };
293
+ });
293
294
  }
294
295
  return undefined;
295
296
  }
@@ -436,10 +437,10 @@ export function createCapsolverResolverVendorAdapter(options) {
436
437
  };
437
438
  const taskId = traceRecorder
438
439
  ? await traceRecorder.runSpan("resolver.vendor.create_task", createTask, {
439
- attributes: {
440
+ attributes: resolverSpanAttributes({
440
441
  vendor: CAPSOLVER_VENDOR_ID,
441
442
  challenge_kind: challenge.kind,
442
- },
443
+ }),
443
444
  onError: (error) => spanErrorAttributes(error, "create_task"),
444
445
  })
445
446
  : await createTask();
@@ -506,10 +507,10 @@ export function createCapsolverResolverVendorAdapter(options) {
506
507
  };
507
508
  return traceRecorder
508
509
  ? await traceRecorder.runSpan("resolver.vendor.poll_result", pollResult, {
509
- attributes: {
510
+ attributes: resolverSpanAttributes({
510
511
  vendor: CAPSOLVER_VENDOR_ID,
511
512
  challenge_kind: challenge.kind,
512
- },
513
+ }),
513
514
  onError: (error) => spanErrorAttributes(error, "poll_result"),
514
515
  })
515
516
  : await pollResult();
@@ -1,4 +1,5 @@
1
1
  import { recordPaidResolverCreate } from "../resolver-usage.js";
2
+ import { resolverSpanAttributes } from "../resolver-span-attributes.js";
2
3
  import { assertResolverHostAllowed } from "./hosts.js";
3
4
  import { ResolverChallengeVerdictError, ResolverVendorUnavailableError, recordResolverVendorPoll, resolverVendorSupports, } from "./types.js";
4
5
  const TWOCAPTCHA_VENDOR_ID = "2captcha";
@@ -198,19 +199,19 @@ function endpoint(baseUrl, path) {
198
199
  }
199
200
  function spanErrorAttributes(error, phase) {
200
201
  if (error instanceof ResolverVendorUnavailableError) {
201
- return {
202
+ return resolverSpanAttributes({
202
203
  unavailability_reason: error.reason,
203
204
  transport_phase: error.phase,
204
- };
205
+ });
205
206
  }
206
207
  // The solve budget raises TwoCaptchaSolveTimeoutError inside the spanned closure and
207
208
  // the outer catch converts it only after the span has been finalized, so the span has
208
209
  // to recognize it here or slow solves lose exactly the attribution these spans add.
209
210
  if (error instanceof TwoCaptchaSolveTimeoutError) {
210
- return {
211
+ return resolverSpanAttributes({
211
212
  unavailability_reason: "timeout",
212
213
  transport_phase: phase,
213
- };
214
+ });
214
215
  }
215
216
  return undefined;
216
217
  }
@@ -345,10 +346,10 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
345
346
  };
346
347
  const taskId = traceRecorder
347
348
  ? await traceRecorder.runSpan("resolver.vendor.create_task", createTask, {
348
- attributes: {
349
+ attributes: resolverSpanAttributes({
349
350
  vendor: TWOCAPTCHA_VENDOR_ID,
350
351
  challenge_kind: challenge.kind,
351
- },
352
+ }),
352
353
  onError: (error) => spanErrorAttributes(error, "create_task"),
353
354
  })
354
355
  : await createTask();
@@ -387,10 +388,10 @@ export function createTwoCaptchaResolverVendorAdapter(options) {
387
388
  };
388
389
  return traceRecorder
389
390
  ? await traceRecorder.runSpan("resolver.vendor.poll_result", pollResult, {
390
- attributes: {
391
+ attributes: resolverSpanAttributes({
391
392
  vendor: TWOCAPTCHA_VENDOR_ID,
392
393
  challenge_kind: challenge.kind,
393
- },
394
+ }),
394
395
  onError: (error) => spanErrorAttributes(error, "poll_result"),
395
396
  })
396
397
  : await pollResult();
@@ -13,6 +13,7 @@ import { createTwoCaptchaResolverVendorAdapter } from "./resolver-vendors/twocap
13
13
  import { RESOLVER_VENDOR_CAPABILITIES, ResolverVendorUnavailableError, attachResolverVendorPollObserver, resolveProviderResolverVendors, resolverVendorSupports, } from "./resolver-vendors/types.js";
14
14
  import { createUnsupportedResolverClient, inheritResolverTelemetryBinding, registerResolverTelemetryBinding, RESOLVER_INSTRUMENTATION_METADATA, } from "./resolver-shared.js";
15
15
  import { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__HYPERSOLUTIONS__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, DEFAULT_RESOLVER_TIMEOUT_MS, kindRequiresClientProfile, } from "./resolver-config.js";
16
+ import { resolverSpanAttributes } from "./resolver-span-attributes.js";
16
17
  import { DEFAULT_STEALTH_PROFILE } from "./stealth.js";
17
18
  export { createUnsupportedResolverClient, RESOLVER_INSTRUMENTATION_METADATA, } from "./resolver-shared.js";
18
19
  export { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__HYPERSOLUTIONS__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, DEFAULT_RESOLVER_TIMEOUT_MS, } from "./resolver-config.js";
@@ -356,7 +357,7 @@ function unavailableAttempt(error, forTelemetry = false) {
356
357
  }
357
358
  function unavailableSpanAttributes(error) {
358
359
  const attempt = unavailableAttempt(error);
359
- return {
360
+ return resolverSpanAttributes({
360
361
  unavailability_reason: error.reason,
361
362
  missing_fields: error.missingFields,
362
363
  cause_name: attempt.cause?.name,
@@ -364,7 +365,7 @@ function unavailableSpanAttributes(error) {
364
365
  upstream_host: attempt.upstreamHost,
365
366
  transport_phase: attempt.phase,
366
367
  transport_round: attempt.round,
367
- };
368
+ });
368
369
  }
369
370
  function resolverTelemetryPhase(value) {
370
371
  return value === "create_task" ||
@@ -657,8 +658,8 @@ async function invalidateResolverSolutionWithOutcome(resolver, challenge, soluti
657
658
  return await invalidate();
658
659
  }
659
660
  return await metadata.traceRecorder.runSpan("resolver.cache.invalidate", invalidate, {
660
- attributes: { challenge_kind: challenge.kind },
661
- onSuccess: (outcome) => ({ outcome }),
661
+ attributes: resolverSpanAttributes({ challenge_kind: challenge.kind }),
662
+ onSuccess: (outcome) => resolverSpanAttributes({ outcome }),
662
663
  });
663
664
  }
664
665
  /** Remove the cached entry for the exact solution object returned by this resolver. */
@@ -864,12 +865,12 @@ function createResolverChainClient(options) {
864
865
  };
865
866
  solution = traceRecorder
866
867
  ? await traceRecorder.runSpan("resolver.vendor.attempt", solveAttempt, {
867
- attributes: {
868
+ attributes: resolverSpanAttributes({
868
869
  vendor: adapter.id,
869
870
  challenge_kind: challenge.kind,
870
871
  client_profile: options.clientProfile,
871
872
  resolver_identity_source: identityResolution.userAgentSource,
872
- },
873
+ }),
873
874
  onError(error) {
874
875
  return error instanceof ResolverVendorUnavailableError
875
876
  ? unavailableSpanAttributes(error)
@@ -3,18 +3,24 @@
3
3
  *
4
4
  * `ctx.stealth` composes these from the Chrome emulation and the request class,
5
5
  * so a caller value is rejected at request time with `SDKError` code
6
- * `STEALTH_HEADER_OVERRIDE_UNSUPPORTED` (`assertCallerHeadersSupported` in
7
- * stealth.ts). The `browser-version-literal` lint reports a subset of these
8
- * names — the `sec-fetch-*` and `sec-ch-ua*` families — in files that use
9
- * `ctx.stealth` (`isReportedOwnedHeaderName` in lint.ts). Both read this
10
- * module, so the lint cannot claim a rejection the runtime does not perform;
11
- * it reports deliberately less than the runtime rejects (host, connection,
6
+ * `STEALTH_HEADER_OVERRIDE_UNSUPPORTED` ({@link assertCallerHeadersSupported}).
7
+ * The offline test harness (`runStandardTests`) calls the same function on
8
+ * every `ctx.stealth` request it serves, so a provider test fails exactly where
9
+ * production would, whichever file or variable the header came from. That is
10
+ * the cross-file enforcement.
11
+ *
12
+ * The `browser-version-literal` lint reports a subset of these names — the
13
+ * `sec-fetch-*` and `sec-ch-ua*` families — in files that use `ctx.stealth`
14
+ * (`isReportedOwnedHeaderName` in lint.ts). Both read this module, so the lint
15
+ * cannot claim a rejection the runtime does not perform; it reports
16
+ * deliberately less than the runtime rejects (host, connection,
12
17
  * accept-encoding and a non-literal user-agent stay silent — see that
13
18
  * predicate for why), so a green `apifuse check` is not proof that no owned
14
19
  * header is set.
15
20
  *
16
- * Leaf module by design: no imports, so the lazily loaded CLI lint path does
17
- * not pull the stealth runtime in.
21
+ * Kept off the stealth runtime: the only import is `errors.js`, whose own
22
+ * imports are type-only, so the lazily loaded CLI lint path does not pull the
23
+ * stealth runtime in.
18
24
  */
19
25
  export declare const SDK_OWNED_EXACT_CHROME_HEADERS: ReadonlySet<string>;
20
26
  export declare const SDK_OWNED_CHROME_HEADER_PREFIX = "sec-fetch-";
@@ -24,3 +30,10 @@ export declare const SDK_OWNED_CHROME_HEADER_PREFIX = "sec-fetch-";
24
30
  * `sec-fetch-*` name.
25
31
  */
26
32
  export declare function isStealthOwnedHeaderName(name: string): boolean;
33
+ /**
34
+ * Throws `SDKError` `STEALTH_HEADER_OVERRIDE_UNSUPPORTED` for the first caller
35
+ * header `ctx.stealth` owns. An `undefined` value is skipped, as the transport
36
+ * drops it before sending. The stealth runtime and the offline test harness
37
+ * both call this, so the two cannot disagree about what is rejected.
38
+ */
39
+ export declare function assertCallerHeadersSupported(headers: Readonly<Record<string, string | readonly string[] | undefined>>): void;
@@ -1,20 +1,27 @@
1
+ import { SDKError } from "../errors.js";
1
2
  /**
2
3
  * Header names the stealth transport owns.
3
4
  *
4
5
  * `ctx.stealth` composes these from the Chrome emulation and the request class,
5
6
  * so a caller value is rejected at request time with `SDKError` code
6
- * `STEALTH_HEADER_OVERRIDE_UNSUPPORTED` (`assertCallerHeadersSupported` in
7
- * stealth.ts). The `browser-version-literal` lint reports a subset of these
8
- * names — the `sec-fetch-*` and `sec-ch-ua*` families — in files that use
9
- * `ctx.stealth` (`isReportedOwnedHeaderName` in lint.ts). Both read this
10
- * module, so the lint cannot claim a rejection the runtime does not perform;
11
- * it reports deliberately less than the runtime rejects (host, connection,
7
+ * `STEALTH_HEADER_OVERRIDE_UNSUPPORTED` ({@link assertCallerHeadersSupported}).
8
+ * The offline test harness (`runStandardTests`) calls the same function on
9
+ * every `ctx.stealth` request it serves, so a provider test fails exactly where
10
+ * production would, whichever file or variable the header came from. That is
11
+ * the cross-file enforcement.
12
+ *
13
+ * The `browser-version-literal` lint reports a subset of these names — the
14
+ * `sec-fetch-*` and `sec-ch-ua*` families — in files that use `ctx.stealth`
15
+ * (`isReportedOwnedHeaderName` in lint.ts). Both read this module, so the lint
16
+ * cannot claim a rejection the runtime does not perform; it reports
17
+ * deliberately less than the runtime rejects (host, connection,
12
18
  * accept-encoding and a non-literal user-agent stay silent — see that
13
19
  * predicate for why), so a green `apifuse check` is not proof that no owned
14
20
  * header is set.
15
21
  *
16
- * Leaf module by design: no imports, so the lazily loaded CLI lint path does
17
- * not pull the stealth runtime in.
22
+ * Kept off the stealth runtime: the only import is `errors.js`, whose own
23
+ * imports are type-only, so the lazily loaded CLI lint path does not pull the
24
+ * stealth runtime in.
18
25
  */
19
26
  export const SDK_OWNED_EXACT_CHROME_HEADERS = new Set([
20
27
  "host",
@@ -35,3 +42,16 @@ export function isStealthOwnedHeaderName(name) {
35
42
  const lower = name.toLowerCase();
36
43
  return (SDK_OWNED_EXACT_CHROME_HEADERS.has(lower) || lower.startsWith(SDK_OWNED_CHROME_HEADER_PREFIX));
37
44
  }
45
+ /**
46
+ * Throws `SDKError` `STEALTH_HEADER_OVERRIDE_UNSUPPORTED` for the first caller
47
+ * header `ctx.stealth` owns. An `undefined` value is skipped, as the transport
48
+ * drops it before sending. The stealth runtime and the offline test harness
49
+ * both call this, so the two cannot disagree about what is rejected.
50
+ */
51
+ export function assertCallerHeadersSupported(headers) {
52
+ for (const [name, value] of Object.entries(headers)) {
53
+ if (value === undefined || !isStealthOwnedHeaderName(name))
54
+ continue;
55
+ throw new SDKError(`Stealth transport owns the "${name.toLowerCase()}" header; remove it from headers.`, { code: "STEALTH_HEADER_OVERRIDE_UNSUPPORTED" });
56
+ }
57
+ }
@@ -1,7 +1,7 @@
1
1
  import type { StealthBrowser, StealthChallengeClassification, StealthOS, StealthProfileDescriptor } from "../types.js";
2
2
  import { type ClosedEnum, type TelemetryContributor } from "./request-telemetry.js";
3
3
  export type StealthTelemetryErrorCode = "transport_network_error" | "transport_timeout" | "transport_cancelled" | "upstream_http_error" | "response_too_large" | "proxy_connect_failed" | "PROXY_POOL_STALE" | "PROXY_EDGE_AUTH_REJECTED" | "PROXY_AUTH_IP_DENIED" | "PROXY_EDGE_TLS_REJECTED" | "PROXY_REQUIRED" | "other";
4
- export type StealthTelemetryRequestClass = "navigation" | "script_navigation" | "xhr" | "post";
4
+ export type StealthTelemetryRequestClass = "navigation" | "script_navigation" | "form_post" | "xhr" | "post";
5
5
  export type StealthTelemetryAttemptKind = "request" | "resolver" | "proxy_diagnostic";
6
6
  export type StealthTelemetrySbsdOutcome = StealthChallengeClassification["outcome"] | "detected" | "refetch_clear";
7
7
  export type StealthTelemetryDiagnostics = {