@apifuse/provider-sdk 2.2.0-beta.58 → 2.2.0-beta.60

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 (150) hide show
  1. package/AUTHORING.md +25 -0
  2. package/CHANGELOG.md +8 -0
  3. package/README.md +18 -2
  4. package/bin/apifuse-dev.ts +2 -1
  5. package/bin/apifuse-pack-types.ts +49 -0
  6. package/bin/apifuse-perf.ts +41 -4
  7. package/bin/apifuse-record.ts +9 -2
  8. package/dist/cli/templates/provider/README.md.tpl +15 -3
  9. package/dist/config/loader.d.ts +7 -0
  10. package/dist/config/loader.js +43 -18
  11. package/dist/declaration-validation.d.ts +2 -1
  12. package/dist/declaration-validation.js +39 -0
  13. package/dist/define.d.ts +4 -4
  14. package/dist/define.js +25 -1
  15. package/dist/engine.d.ts +16 -0
  16. package/dist/engine.js +59 -3
  17. package/dist/error-resolution.js +13 -0
  18. package/dist/index.d.ts +7 -4
  19. package/dist/index.js +4 -2
  20. package/dist/lint.js +38 -17
  21. package/dist/provider.d.ts +3 -3
  22. package/dist/provider.js +2 -2
  23. package/dist/runtime/bounded-expiring-map.d.ts +21 -0
  24. package/dist/runtime/bounded-expiring-map.js +64 -0
  25. package/dist/runtime/browser.js +2 -1
  26. package/dist/runtime/cache.d.ts +2 -0
  27. package/dist/runtime/cache.js +11 -3
  28. package/dist/runtime/diagnostic-env-values.d.ts +5 -0
  29. package/dist/runtime/diagnostic-env-values.js +57 -0
  30. package/dist/runtime/diagnostic-env.d.ts +11 -0
  31. package/dist/runtime/diagnostic-env.js +35 -0
  32. package/dist/runtime/diagnostic-redactor.d.ts +49 -0
  33. package/dist/runtime/diagnostic-redactor.js +569 -0
  34. package/dist/runtime/egress-lease.d.ts +65 -0
  35. package/dist/runtime/egress-lease.js +236 -0
  36. package/dist/runtime/env.js +2 -1
  37. package/dist/runtime/http.js +1 -0
  38. package/dist/runtime/instrumentation.js +3 -0
  39. package/dist/runtime/ocr.js +2 -1
  40. package/dist/runtime/otlp.js +5 -4
  41. package/dist/runtime/proxy-nodemaven.js +3 -2
  42. package/dist/runtime/proxy-retry-policy.d.ts +4 -1
  43. package/dist/runtime/proxy-retry-policy.js +11 -2
  44. package/dist/runtime/request-options.d.ts +7 -0
  45. package/dist/runtime/request-options.js +62 -22
  46. package/dist/runtime/request-telemetry.d.ts +7 -9
  47. package/dist/runtime/resolver-config.d.ts +7 -0
  48. package/dist/runtime/resolver-config.js +8 -0
  49. package/dist/runtime/resolver-public.d.ts +3 -1
  50. package/dist/runtime/resolver-public.js +1 -1
  51. package/dist/runtime/resolver-shared.d.ts +9 -0
  52. package/dist/runtime/resolver-shared.js +57 -0
  53. package/dist/runtime/resolver-telemetry.d.ts +134 -0
  54. package/dist/runtime/resolver-telemetry.js +204 -0
  55. package/dist/runtime/resolver-usage.d.ts +24 -0
  56. package/dist/runtime/resolver-usage.js +60 -0
  57. package/dist/runtime/resolver-vendors/bindings.d.ts +5 -0
  58. package/dist/runtime/resolver-vendors/bindings.js +5 -0
  59. package/dist/runtime/resolver-vendors/browser.js +0 -1
  60. package/dist/runtime/resolver-vendors/capsolver.d.ts +2 -2
  61. package/dist/runtime/resolver-vendors/capsolver.js +21 -9
  62. package/dist/runtime/resolver-vendors/hypersolutions.d.ts +27 -0
  63. package/dist/runtime/resolver-vendors/hypersolutions.js +369 -0
  64. package/dist/runtime/resolver-vendors/twocaptcha.d.ts +2 -2
  65. package/dist/runtime/resolver-vendors/twocaptcha.js +26 -10
  66. package/dist/runtime/resolver-vendors/types.d.ts +35 -5
  67. package/dist/runtime/resolver-vendors/types.js +20 -10
  68. package/dist/runtime/resolver.d.ts +5 -1
  69. package/dist/runtime/resolver.js +400 -111
  70. package/dist/runtime/stealth-akamai-sbsd.d.ts +54 -0
  71. package/dist/runtime/stealth-akamai-sbsd.js +142 -0
  72. package/dist/runtime/stealth.js +547 -53
  73. package/dist/runtime/stt.js +3 -2
  74. package/dist/runtime/trace.d.ts +5 -0
  75. package/dist/runtime/trace.js +115 -20
  76. package/dist/schema.d.ts +20 -0
  77. package/dist/schema.js +72 -2
  78. package/dist/server/index.d.ts +2 -0
  79. package/dist/server/index.js +1 -0
  80. package/dist/server/self-test-redaction.js +8 -33
  81. package/dist/server/self-test.d.ts +4 -0
  82. package/dist/server/self-test.js +10 -1
  83. package/dist/server/sensitive-values.d.ts +26 -0
  84. package/dist/server/sensitive-values.js +131 -0
  85. package/dist/server/serve-implementation.d.ts +7 -1
  86. package/dist/server/serve-implementation.js +394 -125
  87. package/dist/server/trace-output.d.ts +2 -1
  88. package/dist/server/trace-output.js +46 -14
  89. package/dist/server/types.d.ts +6 -0
  90. package/dist/server/types.js +14 -0
  91. package/dist/stealth/profiles.d.ts +7 -1
  92. package/dist/stealth/profiles.js +9 -0
  93. package/dist/testing/run.js +6 -0
  94. package/dist/trace-sanitization.d.ts +4 -3
  95. package/dist/trace-sanitization.js +45 -22
  96. package/dist/types.d.ts +106 -10
  97. package/package.json +2 -1
  98. package/src/cli/templates/provider/README.md.tpl +15 -3
  99. package/src/config/loader.ts +47 -18
  100. package/src/declaration-validation.ts +47 -1
  101. package/src/define.ts +45 -3
  102. package/src/engine.ts +78 -3
  103. package/src/error-resolution.ts +13 -0
  104. package/src/index.ts +34 -1
  105. package/src/lint.ts +44 -18
  106. package/src/provider.ts +10 -0
  107. package/src/runtime/bounded-expiring-map.ts +67 -0
  108. package/src/runtime/browser.ts +2 -1
  109. package/src/runtime/cache.ts +13 -3
  110. package/src/runtime/diagnostic-env-values.ts +56 -0
  111. package/src/runtime/diagnostic-env.ts +55 -0
  112. package/src/runtime/diagnostic-redactor.ts +662 -0
  113. package/src/runtime/egress-lease.ts +341 -0
  114. package/src/runtime/env.ts +2 -1
  115. package/src/runtime/http.ts +1 -0
  116. package/src/runtime/instrumentation.ts +3 -0
  117. package/src/runtime/ocr.ts +2 -1
  118. package/src/runtime/otlp.ts +5 -4
  119. package/src/runtime/proxy-nodemaven.ts +3 -2
  120. package/src/runtime/proxy-retry-policy.ts +12 -1
  121. package/src/runtime/request-options.ts +76 -28
  122. package/src/runtime/request-telemetry.ts +5 -7
  123. package/src/runtime/resolver-config.ts +12 -0
  124. package/src/runtime/resolver-public.ts +21 -0
  125. package/src/runtime/resolver-shared.ts +75 -0
  126. package/src/runtime/resolver-telemetry.ts +362 -0
  127. package/src/runtime/resolver-usage.ts +96 -0
  128. package/src/runtime/resolver-vendors/bindings.ts +5 -0
  129. package/src/runtime/resolver-vendors/browser.ts +0 -1
  130. package/src/runtime/resolver-vendors/capsolver.ts +22 -3
  131. package/src/runtime/resolver-vendors/hypersolutions.ts +480 -0
  132. package/src/runtime/resolver-vendors/twocaptcha.ts +26 -12
  133. package/src/runtime/resolver-vendors/types.ts +60 -12
  134. package/src/runtime/resolver.ts +468 -126
  135. package/src/runtime/stealth-akamai-sbsd.ts +207 -0
  136. package/src/runtime/stealth.ts +727 -95
  137. package/src/runtime/stt.ts +3 -2
  138. package/src/runtime/trace.ts +168 -26
  139. package/src/schema.ts +80 -2
  140. package/src/server/index.ts +21 -0
  141. package/src/server/self-test-redaction.ts +10 -40
  142. package/src/server/self-test.ts +13 -0
  143. package/src/server/sensitive-values.ts +200 -0
  144. package/src/server/serve-implementation.ts +561 -136
  145. package/src/server/trace-output.ts +77 -17
  146. package/src/server/types.ts +14 -0
  147. package/src/stealth/profiles.ts +10 -0
  148. package/src/testing/run.ts +6 -0
  149. package/src/trace-sanitization.ts +70 -23
  150. package/src/types.ts +124 -19
package/AUTHORING.md CHANGED
@@ -130,6 +130,12 @@ not list `APIFUSE__PROXY__SMARTPROXY_APP_KEY`,
130
130
  `APIFUSE__PROXY__NODEMAVEN_PASSWORD` in provider `secrets`; `proxy` contains
131
131
  policy intent only.
132
132
 
133
+ Hosted runtime settings are engine-owned as well: every `APIFUSE__CDP_POOL__*`
134
+ variable, the Cloudflare OCR/STT tokens and account identifier,
135
+ `APIFUSE__OCR__API_KEY`, `APIFUSE__CACHE__KEY_PEPPER`, and
136
+ `APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET`. `defineProvider`
137
+ rejects them in `secrets`; the engine reads them from its own environment.
138
+
133
139
  ### Factored operations
134
140
 
135
141
  `defineProvider(declaration)` returns the builder that accepts `operations`, so
@@ -220,6 +226,22 @@ const utcInput = resolveHealthCheckInputDateTokens({ date: "+45d" }, new Date(),
220
226
  Do not re-resolve a health assertion's dates in UTC when its case input used the
221
227
  default KST calendar.
222
228
 
229
+ ### Sensitive vs public fields
230
+
231
+ The platform fixture-privacy ratchet flags name-, phone-, and address-shaped keys
232
+ unless the schema at that path declares them one way or the other. Declare every hit:
233
+
234
+ - `sensitive(schema, kind)` / `fields.*()` — anything about a person: user, profile,
235
+ passenger, driver, or member names, emails, member ids, personal phone numbers.
236
+ Emits `x-apifuse-sensitive: true`; the value is redacted in published artifacts.
237
+ - `publicField(schema)` — reviewed public data: business, shop, product, artist, or
238
+ facility names, hospital/hotel/station phones, timezone names. Emits
239
+ `x-apifuse-sensitive: false`; nothing is redacted. It is a review verdict, not a mute.
240
+ Credential-shaped keys (token, password, apiKey, …) cannot be declared public.
241
+
242
+ Apply either helper to the leaf, before `.optional()`/`.nullable()`/`.default()`;
243
+ `publicField()` over a wrapper that hides a `sensitive()` leaf throws at definition time.
244
+
223
245
  ### Real-handler E2E in standard tests
224
246
 
225
247
  `runStandardTests(provider)` validates declarations and fixtures but reports a
@@ -501,6 +523,9 @@ SDK observability is emitted separately in the
501
523
  ```
502
524
 
503
525
  Treat this header as telemetry, not as provider-controlled public error detail.
526
+ It is SDK-owned even when a handler returns a raw `Response`: a caller-set
527
+ header is dropped, and a raw status of 400 or above gets a status-derived one
528
+ (retryable only for 408, 429, and 5xx) while the body is left untouched.
504
529
  Its category, taxonomy version, retryability, and optional upstream status match
505
530
  the structured `provider_request_failed` log event.
506
531
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @apifuse/provider-sdk Changelog
2
2
 
3
+ ## 2.2.0-beta.60
4
+
5
+ - Release candidate for main commit 5f417c6282b3a4d92bf31c8258ba913b5e53dc8b.
6
+
7
+ ## 2.2.0-beta.59
8
+
9
+ - Release candidate for main commit 55ae556fcf33e28a228c2d54ce76e7afa41e5dc7.
10
+
3
11
  ## 2.2.0-beta.58
4
12
 
5
13
  - Release candidate for main commit e137940caa8bc32ce768571a6197651e581c2270.
package/README.md CHANGED
@@ -76,7 +76,14 @@ The operation request body is the same envelope used by the APIFuse gateway:
76
76
  | `requestId` | yes | Any unique string is fine for local debugging; it is echoed in structured errors. |
77
77
  | `input` | yes | The operation input after schema validation. |
78
78
  | `headers` | no | Extra caller headers to expose through `ctx.request.headers`. |
79
- | `connection` | no | Omit for no-auth/public operations. For credential debugging, pass an object with `id`, `mode`, `secrets`, `metadata`, and `externalRef`. Do not pass `null`. |
79
+ | `connectionId` | no | Connection identity only; it does not include credentials. The gateway sends it for `optional` connection mode, and only when the caller supplied a connection that passed authorization. Exposed as `ctx.request.connectionId`. |
80
+ | `connection` | no | Credential-bearing connection data. The gateway sends it for `required` connection mode; omit it for no-auth/public (`none` mode) operations. For local debugging, pass an object with `id`, `mode`, `secrets`, `metadata`, and `externalRef`. Do not pass `null`. |
81
+
82
+ The gateway sends only `connection` for `required` mode, only `connectionId`
83
+ for `optional` mode (and nothing when the caller passed no connection), and
84
+ neither field for `none` mode. If a malformed or manually constructed envelope
85
+ contains both, a non-empty nested `connection.id` takes precedence over the
86
+ top-level `connectionId`; an empty string is treated as absent.
80
87
 
81
88
  Credential-bearing local smoke example:
82
89
 
@@ -146,7 +153,11 @@ the bad request path; provider/runtime failures include `code`, `message`, and
146
153
  a session with `ctx.stealth.createSession()` and use `session.redirects.run()`;
147
154
  inspect accumulated cookies through `session.cookies`. Select an SDK stealth
148
155
  identity with structured `stealth: { browser, os }` options; omitted `os`
149
- explicitly defaults to `macos`. Do not pin a browser version
156
+ explicitly defaults to `macos`. The transport owns `User-Agent`,
157
+ `Accept-Encoding`, `sec-ch-ua*`, and every `Sec-Fetch-*` header and throws
158
+ `STEALTH_HEADER_OVERRIDE_UNSUPPORTED` if you set one; declare
159
+ `stealth.requestClass: "navigation" | "xhr" | "post"` instead. Do not pin a
160
+ browser version
150
161
  or tune JA3, HTTP/2 SETTINGS, or
151
162
  pseudo-header order in provider code. Chrome, Firefox, and Safari profiles
152
163
  are supported; use `ctx.browser` when the provider needs browser execution.
@@ -325,3 +336,8 @@ External bounty contributors should submit standalone Provider source plus
325
336
  `bun run check` / `bun run test` evidence. APIFuse maintainers own monorepo
326
337
  import, registry generation, deployment projection checks, and release
327
338
  publishing.
339
+
340
+ Relayed user-generated text (listing bodies, reviews, posts) is contained by the
341
+ platform, not by the provider. Wrap such output fields in `untrustedContent(...)`
342
+ so the schema carries `x-apifuse-content-trust: untrusted` and
343
+ `x-apifuse-content-provenance: external`; do not strip instruction-like prose.
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env bun
2
+ import { readDiagnosticEnv } from "../src/runtime/diagnostic-env.js";
2
3
 
3
4
  import { existsSync } from "node:fs";
4
5
  import { dirname, relative, resolve } from "node:path";
@@ -87,7 +88,7 @@ export function createProviderContext(provider: ProviderDefinition): {
87
88
  process.env,
88
89
  provider.secrets?.map((secret) => secret.name) ?? [],
89
90
  );
90
- const providerEnv = { get: (key: string) => providerEnvironment[key] };
91
+ const providerEnv = { get: (key: string) => readDiagnosticEnv(key, providerEnvironment) };
91
92
  const engineEnv = createEnvContext([PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV]);
92
93
  const engineCredentials = readEngineProxyCredentials();
93
94
  const credential = createCredentialContext();
@@ -401,6 +401,55 @@ const NEGATIVE_CONTROLS = [
401
401
  "",
402
402
  ].join("\n"),
403
403
  },
404
+ {
405
+ filename: "negative-control-resolver-telemetry-description.ts",
406
+ expectedCode: "TS2353",
407
+ description: "resolver gateway telemetry rejects vendor error descriptions",
408
+ source: [
409
+ 'import type { ResolverTelemetryHeaderPayload } from "@apifuse/provider-sdk";',
410
+ "",
411
+ "export const mustNotCompile: ResolverTelemetryHeaderPayload = {",
412
+ "\tattempts: 0,",
413
+ "\tfailovers: 0,",
414
+ "\tvendorChain: [],",
415
+ "\tpollCount: 0,",
416
+ '\tvendorErrorDescription: "free text must stay log-only",',
417
+ "};",
418
+ "",
419
+ ].join("\n"),
420
+ },
421
+ {
422
+ filename: "negative-control-resolver-telemetry-phase.ts",
423
+ expectedCode: "TS2322",
424
+ description: "resolver gateway telemetry rejects phases outside its closed vocabulary",
425
+ source: [
426
+ 'import { closedEnum, type ResolverTelemetryHeaderPayload } from "@apifuse/provider-sdk";',
427
+ "",
428
+ "export const mustNotCompile: ResolverTelemetryHeaderPayload = {",
429
+ "\tattempts: 1,",
430
+ "\tfailovers: 0,",
431
+ '\tvendorChain: [closedEnum("hypersolutions")],',
432
+ "\tpollCount: 0,",
433
+ '\tattemptSamples: [{ v: closedEnum("hypersolutions"), p: closedEnum("solve_exchange"), o: closedEnum("error"), ms: 1 }],',
434
+ "};",
435
+ "",
436
+ ].join("\n"),
437
+ },
438
+ {
439
+ filename: "positive-control-resolver-telemetry-contributor.ts",
440
+ expectedCode: "",
441
+ description: "resolver telemetry contributor satisfies the gateway-ingestible contract",
442
+ source: [
443
+ 'import { closedEnum, ResolverTelemetryCollector, type GatewayIngestible, type ResolverTelemetryHeaderPayload, type ResolverTelemetryLogPayload, type ResolverTelemetryPhase, type TelemetryContributor } from "@apifuse/provider-sdk";',
444
+ "",
445
+ "export const resolver: TelemetryContributor<ResolverTelemetryLogPayload, ResolverTelemetryHeaderPayload> = new ResolverTelemetryCollector();",
446
+ 'const hyperPhases: ResolverTelemetryPhase[] = ["measure_ip", "fetch_script", "generate_payload", "post_payload"];',
447
+ 'export const hyperPhaseHeader: ResolverTelemetryHeaderPayload = { attempts: 1, failovers: 0, vendorChain: [closedEnum("hypersolutions")], pollCount: 0, attemptSamples: hyperPhases.map((phase) => ({ v: closedEnum("hypersolutions"), p: closedEnum(phase), o: closedEnum("ok"), ms: 1 })) };',
448
+ "declare const header: ResolverTelemetryHeaderPayload;",
449
+ "export const ingestible: GatewayIngestible<ResolverTelemetryHeaderPayload> = header;",
450
+ "",
451
+ ].join("\n"),
452
+ },
404
453
  ] as const;
405
454
 
406
455
  const tempRoot = mkdtempSync(join(tmpdir(), "apifuse-provider-sdk-pack-types-"));
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  // @ts-nocheck
3
+ import { readDiagnosticEnv } from "../src/runtime/diagnostic-env.js";
3
4
 
4
5
  import { existsSync } from "node:fs";
5
6
  import { mkdir, readFile, writeFile } from "node:fs/promises";
@@ -25,11 +26,20 @@ import {
25
26
  type StealthResponse,
26
27
  wrapWithInstrumentation,
27
28
  } from "../src/index.js";
29
+ import {
30
+ bindDiagnosticSensitiveRegistry,
31
+ type CompiledDiagnosticSensitiveValues,
32
+ compileProcessDiagnosticSensitiveValues,
33
+ createDiagnosticRedactor,
34
+ redactDiagnosticText,
35
+ } from "../src/runtime/diagnostic-redactor.js";
28
36
  import { computeStats, groupSpansByName, type PerfStats } from "../src/runtime/perf.js";
29
37
  import { createMemoryProviderRuntimeState } from "../src/runtime/state.js";
30
38
  import { createStealthClient } from "../src/runtime/stealth.js";
31
39
  import { createTraceContext, resolveTraceContextOptions } from "../src/runtime/trace.js";
32
40
  import { renderWaterfall } from "../src/runtime/waterfall.js";
41
+ import { collectStaticDiagnosticSensitiveValues } from "../src/server/sensitive-values.js";
42
+ import { sanitizeSpanForOutput } from "../src/trace-sanitization.js";
33
43
  import type { BrowserClient } from "../src/types.js";
34
44
 
35
45
  type CliArgs = {
@@ -100,6 +110,9 @@ export async function main() {
100
110
  const providerDirectory = resolve(process.cwd(), args.providerPath);
101
111
  const providerEntry = resolveProviderEntry(providerDirectory);
102
112
  const provider = await loadProvider(providerEntry);
113
+ const staticSensitiveValues = compileProcessDiagnosticSensitiveValues(
114
+ collectStaticDiagnosticSensitiveValues(provider),
115
+ );
103
116
  const providerId = basename(providerDirectory);
104
117
  const config = await loadApiFuseConfig(process.cwd());
105
118
  const operation = getOperation(provider, args.operation);
@@ -119,6 +132,7 @@ export async function main() {
119
132
  operationName: args.operation,
120
133
  outputSchema,
121
134
  proxyEnabled: false,
135
+ staticSensitiveValues,
122
136
  });
123
137
 
124
138
  let proxySuite: ProfileSuite | undefined;
@@ -135,6 +149,7 @@ export async function main() {
135
149
  operationName: args.operation,
136
150
  outputSchema,
137
151
  proxyEnabled: true,
152
+ staticSensitiveValues,
138
153
  });
139
154
  }
140
155
 
@@ -170,8 +185,8 @@ export async function main() {
170
185
  runs: args.runs,
171
186
  warmup: args.warmup,
172
187
  concurrency: args.concurrency,
173
- direct: directSuite,
174
- proxy: proxySuite,
188
+ direct: sanitizeProfileSuiteForExport(directSuite),
189
+ proxy: proxySuite ? sanitizeProfileSuiteForExport(proxySuite) : undefined,
175
190
  flamePath,
176
191
  });
177
192
  console.log(`Exported JSON: ${resolve(process.cwd(), args.exportPath)}`);
@@ -480,6 +495,7 @@ async function runProfileSuite(options: {
480
495
  operationName: string;
481
496
  outputSchema: { parse(input: unknown): unknown };
482
497
  proxyEnabled: boolean;
498
+ staticSensitiveValues: CompiledDiagnosticSensitiveValues;
483
499
  }): Promise<ProfileSuite> {
484
500
  for (let index = 0; index < options.args.warmup; index += 1) {
485
501
  await profileRun({
@@ -492,6 +508,7 @@ async function runProfileSuite(options: {
492
508
  operationName: options.operationName,
493
509
  outputSchema: options.outputSchema,
494
510
  proxyEnabled: options.proxyEnabled,
511
+ staticSensitiveValues: options.staticSensitiveValues,
495
512
  });
496
513
  }
497
514
 
@@ -509,6 +526,7 @@ async function runProfileSuite(options: {
509
526
  operationName: options.operationName,
510
527
  outputSchema: options.outputSchema,
511
528
  proxyEnabled: options.proxyEnabled,
529
+ staticSensitiveValues: options.staticSensitiveValues,
512
530
  }),
513
531
  );
514
532
  runs.push(...(await Promise.all(batch)));
@@ -545,6 +563,7 @@ async function profileRun(options: {
545
563
  operationName: string;
546
564
  outputSchema: { parse(input: unknown): unknown };
547
565
  proxyEnabled: boolean;
566
+ staticSensitiveValues: CompiledDiagnosticSensitiveValues;
548
567
  }): Promise<RunResult> {
549
568
  try {
550
569
  return await executeProfileRun({ ...options, forceFixtureReplay: false });
@@ -576,9 +595,16 @@ async function executeProfileRun(options: {
576
595
  operationName: string;
577
596
  outputSchema: { parse(input: unknown): unknown };
578
597
  proxyEnabled: boolean;
598
+ staticSensitiveValues: CompiledDiagnosticSensitiveValues;
579
599
  }): Promise<RunResult> {
580
600
  const _operation = getOperation(options.provider, options.operationName);
581
- const traceContext = createTraceContext(resolveTraceContextOptions(options.config.trace));
601
+ const sensitiveRegistry = createDiagnosticRedactor([], options.staticSensitiveValues);
602
+ const redact = (text: string) => redactDiagnosticText(text, sensitiveRegistry.redact);
603
+ const traceContext = createTraceContext({
604
+ ...resolveTraceContextOptions(options.config.trace),
605
+ redact,
606
+ });
607
+ bindDiagnosticSensitiveRegistry(traceContext, sensitiveRegistry);
582
608
  const baseContext = createBaseContext({
583
609
  config: options.config,
584
610
  provider: options.provider,
@@ -650,7 +676,7 @@ function createBaseContext(options: {
650
676
  });
651
677
 
652
678
  const env = {
653
- get: (key: string) => process.env[key],
679
+ get: (key: string) => readDiagnosticEnv(key),
654
680
  };
655
681
  const credential = {
656
682
  mode: "none" as const,
@@ -925,6 +951,17 @@ function selectRepresentativeRun(runs: RunResult[]): RunResult {
925
951
  return first;
926
952
  }
927
953
 
954
+ function sanitizeProfileSuiteForExport(suite: ProfileSuite): ProfileSuite {
955
+ // The perf CLI has no server request scope; retain the shared output policy on disk.
956
+ return {
957
+ ...suite,
958
+ runs: suite.runs.map((run) => ({
959
+ ...run,
960
+ spans: run.spans.map((span) => sanitizeSpanForOutput(span)),
961
+ })),
962
+ };
963
+ }
964
+
928
965
  async function writeExport(filePath: string, payload: unknown): Promise<void> {
929
966
  const absolutePath = resolve(process.cwd(), filePath);
930
967
  await mkdir(dirname(absolutePath), { recursive: true });
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env bun
2
+ import { readDiagnosticEnv } from "../src/runtime/diagnostic-env.js";
2
3
 
3
4
  import { existsSync, readFileSync, statSync } from "node:fs";
4
5
  import { mkdir, writeFile } from "node:fs/promises";
@@ -519,8 +520,8 @@ export function createCaptureContext(
519
520
  process.env,
520
521
  provider.secrets?.map((secret) => secret.name) ?? [],
521
522
  );
522
- const env = { get: (key: string) => providerEnvironment[key] };
523
- const engineEnv = { get: (key: string) => process.env[key] };
523
+ const env = { get: (key: string) => readDiagnosticEnv(key, providerEnvironment) };
524
+ const engineEnv = { get: (key: string) => readDiagnosticEnv(key) };
524
525
  const engineCredentials = readEngineProxyCredentials();
525
526
  const credential = {
526
527
  mode: "none" as const,
@@ -832,6 +833,12 @@ function proxyStealthSession(
832
833
  onResponse(order, response);
833
834
  return response;
834
835
  },
836
+ replayChallenged: async (...args: Parameters<StealthSession["replayChallenged"]>) => {
837
+ const order = reserveOrder();
838
+ const response = await session.replayChallenged(...args);
839
+ onResponse(order, response);
840
+ return response;
841
+ },
835
842
  cookies: session.cookies,
836
843
  redirects: {
837
844
  run: async (...args: Parameters<StealthSession["redirects"]["run"]>) => {
@@ -97,9 +97,21 @@ The `POST /v1/{operation}` body is a request envelope:
97
97
  - `requestId` is required and can be any unique local debugging string.
98
98
  - `input` contains the operation input shape.
99
99
  - `headers` is optional.
100
- - `connection` is optional; omit it for no-auth/public operations. For
101
- credential debugging, pass `{ "id", "mode", "secrets", "metadata",
102
- "externalRef" }` with local-only secret values.
100
+ - `connectionId` is optional connection identity only and does not include
101
+ credentials. The gateway sends it for `optional` connection mode, and only
102
+ when the caller supplied a connection that passed authorization. It is
103
+ exposed as `ctx.request.connectionId`.
104
+ - `connection` is optional credential-bearing connection data. The gateway
105
+ sends it for `required` connection mode; omit it for no-auth/public (`none`
106
+ mode) operations. For local debugging, pass
107
+ `{ "id", "mode", "secrets", "metadata", "externalRef" }` with local-only
108
+ secret values.
109
+
110
+ The gateway sends only `connection` for `required` mode, only `connectionId`
111
+ for `optional` mode (and nothing when the caller passed no connection), and
112
+ neither field for `none` mode. If a malformed or manually constructed envelope
113
+ contains both, a non-empty nested `connection.id` takes precedence over the
114
+ top-level `connectionId`; an empty string is treated as absent.
103
115
 
104
116
  Structured errors return an `error` object with `code`, `message`,
105
117
  `requestId`, and optional `details`; validation failures include field paths in
@@ -140,6 +140,13 @@ export declare function providerStateRedisUrlFromEnv(): string | undefined;
140
140
  /** @internal Test-only hook for exercising shared proxy-cache behavior. */
141
141
  export declare function __setProxyRedisForTests(redis: ProxyRedisClient | undefined): void;
142
142
  export declare function __setSmartproxyAllocatorDeadlineMsForTests(deadlineMs: number | undefined): void;
143
+ /** @internal Test-only hook for exercising the pool-cache capacity bound at a small size. */
144
+ export declare function __setSmartproxyPoolCacheMaxEntriesForTests(maxEntries: number | undefined): void;
145
+ /** @internal Test-only view of the in-memory Smartproxy cache sizes. */
146
+ export declare function __getSmartproxyCacheSizesForTests(): {
147
+ pools: number;
148
+ tombstones: number;
149
+ };
143
150
  export declare function resolveProxyConfig(options?: ProxyResolutionOptions): ResolvedProxyConfig;
144
151
  export declare function resolveProxyConfigAsync(options?: ProxyResolutionOptions): Promise<ResolvedProxyConfig>;
145
152
  /**
@@ -1,7 +1,9 @@
1
+ import { readDiagnosticEnv } from "../runtime/diagnostic-env.js";
1
2
  import { createHash, randomUUID } from "node:crypto";
2
3
  import { existsSync } from "node:fs";
3
4
  import { createRequire } from "node:module";
4
5
  import path from "node:path";
6
+ import { BoundedExpiringMap } from "../runtime/bounded-expiring-map.js";
5
7
  import { NODEMAVEN_DEFAULT_PROTOCOL, NODEMAVEN_FILTER_ENV, NODEMAVEN_MAX_POOL_SIZE, NODEMAVEN_PASSWORD_ENV, NODEMAVEN_USERNAME_ENV, nodemavenPoolSize, synthesizeNodemavenProxy, } from "../runtime/proxy-nodemaven.js";
6
8
  // "smartproxy" here is api.smartproxy.org — a residential proxy with an IP
7
9
  // extraction API (app_key → raw ip:port pool). It is NOT the company formerly
@@ -33,7 +35,13 @@ export class ProxyResolutionError extends Error {
33
35
  this.protocol = options?.protocol;
34
36
  }
35
37
  }
36
- const proxyCache = new Map();
38
+ // Keyed per (policy, affinity). Connection affinities are minted by callers, so
39
+ // without a cap a long-lived process accumulates one pool per affinity it ever
40
+ // saw. Evicting a still-fresh pool forces a fresh allocation (a different
41
+ // egress endpoint) for that affinity, so the cap is generous and every
42
+ // capacity eviction is reported as `diagnostics.poolCacheEvictions`.
43
+ const SMARTPROXY_POOL_CACHE_MAX_ENTRIES = 10_000;
44
+ let proxyCache = createSmartproxyPoolCache(SMARTPROXY_POOL_CACHE_MAX_ENTRIES);
37
45
  const require = createRequire(import.meta.url);
38
46
  const proxyInflight = new Map();
39
47
  const invalidatedProxyKeys = new Map();
@@ -56,17 +64,17 @@ const SMARTPROXY_ALLOCATOR_RETRY_BASE_MS = 25;
56
64
  const SMARTPROXY_EXTRACTION_CACHE_TTL_MS = 15_000;
57
65
  const SMARTPROXY_EXTRACTION_SOFT_REFRESH_MS = 10_000;
58
66
  function redisUrlFromEnv() {
59
- return (process.env.APIFUSE__PROVIDER__CACHE_REDIS_URL?.trim() ||
60
- process.env[REDIS_URL_ENV]?.trim() ||
67
+ return (readDiagnosticEnv(PROVIDER_CACHE_REDIS_URL_ENV)?.trim() ||
68
+ readDiagnosticEnv(REDIS_URL_ENV)?.trim() ||
61
69
  undefined);
62
70
  }
63
71
  export function providerCacheRedisUrlFromEnv() {
64
72
  return redisUrlFromEnv();
65
73
  }
66
74
  export function providerStateRedisUrlFromEnv() {
67
- return (process.env[PROVIDER_STATE_REDIS_URL_ENV]?.trim() ||
68
- process.env[PROVIDER_CACHE_REDIS_URL_ENV]?.trim() ||
69
- process.env[REDIS_URL_ENV]?.trim() ||
75
+ return (readDiagnosticEnv(PROVIDER_STATE_REDIS_URL_ENV)?.trim() ||
76
+ readDiagnosticEnv(PROVIDER_CACHE_REDIS_URL_ENV)?.trim() ||
77
+ readDiagnosticEnv(REDIS_URL_ENV)?.trim() ||
70
78
  undefined);
71
79
  }
72
80
  /** @internal Test-only hook for exercising shared proxy-cache behavior. */
@@ -76,6 +84,17 @@ export function __setProxyRedisForTests(redis) {
76
84
  export function __setSmartproxyAllocatorDeadlineMsForTests(deadlineMs) {
77
85
  smartproxyAllocatorDeadlineMsForTests = deadlineMs;
78
86
  }
87
+ /** @internal Test-only hook for exercising the pool-cache capacity bound at a small size. */
88
+ export function __setSmartproxyPoolCacheMaxEntriesForTests(maxEntries) {
89
+ proxyCache = createSmartproxyPoolCache(maxEntries ?? SMARTPROXY_POOL_CACHE_MAX_ENTRIES);
90
+ }
91
+ /** @internal Test-only view of the in-memory Smartproxy cache sizes. */
92
+ export function __getSmartproxyCacheSizesForTests() {
93
+ return { pools: proxyCache.size, tombstones: invalidatedProxyKeys.size };
94
+ }
95
+ function createSmartproxyPoolCache(maxEntries) {
96
+ return new BoundedExpiringMap(maxEntries, (pool) => pool.expiresAt);
97
+ }
79
98
  function getProxyRedis() {
80
99
  if (proxyRedisForTests)
81
100
  return proxyRedisForTests;
@@ -389,10 +408,10 @@ export async function resolveWithVendor(vendor, policy, options, context) {
389
408
  if (vendor === "nodemaven") {
390
409
  const startedAt = Date.now();
391
410
  const username = (context.credentials === undefined
392
- ? process.env[NODEMAVEN_USERNAME_ENV]
411
+ ? readDiagnosticEnv(NODEMAVEN_USERNAME_ENV)
393
412
  : context.credentials[NODEMAVEN_USERNAME_ENV])?.trim();
394
413
  const password = (context.credentials === undefined
395
- ? process.env[NODEMAVEN_PASSWORD_ENV]
414
+ ? readDiagnosticEnv(NODEMAVEN_PASSWORD_ENV)
396
415
  : context.credentials[NODEMAVEN_PASSWORD_ENV])?.trim();
397
416
  const filter = context.credentials === undefined
398
417
  ? process.env[NODEMAVEN_FILTER_ENV]
@@ -435,7 +454,7 @@ export async function resolveWithVendor(vendor, policy, options, context) {
435
454
  }
436
455
  // smartproxy allocation-style vendor.
437
456
  const appKey = (context.credentials === undefined
438
- ? process.env[SMARTPROXY_APP_KEY_ENV]
457
+ ? readDiagnosticEnv(SMARTPROXY_APP_KEY_ENV)
439
458
  : context.credentials[SMARTPROXY_APP_KEY_ENV])?.trim();
440
459
  if (!appKey) {
441
460
  // Guarded by vendorHasCredentials; treated as a vendor-internal failure.
@@ -457,6 +476,7 @@ export async function resolveWithVendor(vendor, policy, options, context) {
457
476
  ...allocated.pool.diagnostics,
458
477
  poolSize: allocated.pool.urls.length,
459
478
  poolIndex,
479
+ poolCacheEvictions: proxyCache.capacityEvictions,
460
480
  },
461
481
  };
462
482
  }
@@ -501,14 +521,14 @@ function envDefaultProvider() {
501
521
  function vendorHasCredentials(vendor, credentials) {
502
522
  if (vendor === "nodemaven") {
503
523
  return Boolean((credentials === undefined
504
- ? process.env[NODEMAVEN_USERNAME_ENV]
524
+ ? readDiagnosticEnv(NODEMAVEN_USERNAME_ENV)
505
525
  : credentials[NODEMAVEN_USERNAME_ENV])?.trim() &&
506
526
  (credentials === undefined
507
- ? process.env[NODEMAVEN_PASSWORD_ENV]
527
+ ? readDiagnosticEnv(NODEMAVEN_PASSWORD_ENV)
508
528
  : credentials[NODEMAVEN_PASSWORD_ENV])?.trim());
509
529
  }
510
530
  return Boolean((credentials === undefined
511
- ? process.env[SMARTPROXY_APP_KEY_ENV]
531
+ ? readDiagnosticEnv(SMARTPROXY_APP_KEY_ENV)
512
532
  : credentials[SMARTPROXY_APP_KEY_ENV])?.trim());
513
533
  }
514
534
  function missingCredentialEnv(vendor) {
@@ -695,8 +715,8 @@ async function allocateSmartproxy(policy, appKey, lifetimeMinutes, affinityKey,
695
715
  const now = startedAt;
696
716
  const invalidatedUntil = invalidatedProxyKeys.get(cacheKey) ?? 0;
697
717
  const skipCached = invalidatedUntil > now;
698
- const cached = proxyCache.get(cacheKey);
699
- if (!skipCached && cached && isFresh(cached, now)) {
718
+ const cached = skipCached ? undefined : proxyCache.get(cacheKey, now);
719
+ if (cached) {
700
720
  if (shouldSoftRefresh(cached, now)) {
701
721
  void refreshSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes, protocol, ambientDefaults, sharedCache);
702
722
  return {
@@ -751,7 +771,7 @@ async function readSmartproxyRedisPool(cacheKey, startedAt) {
751
771
  const now = Date.now();
752
772
  if (!isFresh(pool, now))
753
773
  return null;
754
- proxyCache.set(cacheKey, pool);
774
+ proxyCache.set(cacheKey, pool, now);
755
775
  return {
756
776
  pool,
757
777
  telemetry: telemetryForPool(pool, "redis_hit", startedAt, { redisReadMs }),
@@ -962,7 +982,7 @@ async function allocateAndStoreSmartproxyPool(cacheKey, policy, appKey, lifetime
962
982
  rawConnect: true,
963
983
  },
964
984
  };
965
- proxyCache.set(cacheKey, result);
985
+ proxyCache.set(cacheKey, result, allocatedAt);
966
986
  if (options.redis && options.poolKey) {
967
987
  const redis = options.redis;
968
988
  const poolKey = options.poolKey;
@@ -1159,12 +1179,17 @@ function markSmartproxyCacheInvalidated(options = {}) {
1159
1179
  }
1160
1180
  const lifetimeMinutes = resolveSmartproxyLifetime(policy);
1161
1181
  const appKey = (options.engineCredentials === undefined
1162
- ? process.env[SMARTPROXY_APP_KEY_ENV]
1182
+ ? readDiagnosticEnv(SMARTPROXY_APP_KEY_ENV)
1163
1183
  : options.engineCredentials[SMARTPROXY_APP_KEY_ENV])?.trim();
1164
1184
  if (!appKey)
1165
1185
  return undefined;
1166
1186
  const cacheKey = buildSmartproxyCacheKey(policy, appKey, options.affinityKey, lifetimeMinutes, options.protocol ?? VENDOR_DEFAULT_PROTOCOL.smartproxy);
1167
- invalidatedProxyKeys.set(cacheKey, Date.now() + SMARTPROXY_INVALIDATION_SKIP_REDIS_MS);
1187
+ const now = Date.now();
1188
+ for (const [key, invalidatedUntil] of invalidatedProxyKeys) {
1189
+ if (invalidatedUntil <= now)
1190
+ invalidatedProxyKeys.delete(key);
1191
+ }
1192
+ invalidatedProxyKeys.set(cacheKey, now + SMARTPROXY_INVALIDATION_SKIP_REDIS_MS);
1168
1193
  proxyCache.delete(cacheKey);
1169
1194
  proxyInflight.delete(cacheKey);
1170
1195
  return cacheKey;
@@ -13,6 +13,7 @@ export declare const DECLARATION_RULE_IDS: {
13
13
  readonly proxySmartproxyGeo: "proxy-smartproxy-country-only";
14
14
  readonly operationUpstreamProxy: "operation-upstream-proxy-unsupported";
15
15
  readonly pinnedWireFieldPathValid: "public-schema-pinned-wire-field-valid";
16
+ readonly resolverClientProfileFamily: "resolver-client-profile-family";
16
17
  };
17
18
  export type DeclarationRuleId = (typeof DECLARATION_RULE_IDS)[keyof typeof DECLARATION_RULE_IDS];
18
19
  export type DeclarationViolation = {
@@ -24,7 +25,7 @@ export type DeclarationViolation = {
24
25
  export declare function declarationInvalidError(violations: readonly DeclarationViolation[]): ProviderError;
25
26
  /** Enforces declaration rules whose runtime behavior would otherwise fail open. */
26
27
  export declare function validateFailClosedDeclaration(provider: ProviderDefinition): void;
27
- type ProviderDeclarationRulesInput = Pick<ProviderDefinition, "healthJourneys" | "meta" | "proxy">;
28
+ type ProviderDeclarationRulesInput = Pick<ProviderDefinition, "healthJourneys" | "meta" | "proxy" | "resolver" | "stealth">;
28
29
  type OperationDeclarationRulesInput = Pick<ProviderDefinition, "operations">;
29
30
  /** Enforces fail-closed rules that only depend on the provider declaration. */
30
31
  export declare function validateFailClosedProviderDeclaration(provider: ProviderDeclarationRulesInput): void;
@@ -1,6 +1,8 @@
1
1
  import { describeSchema } from "./contract-serialization.js";
2
2
  import { ProviderError } from "./errors.js";
3
3
  import { HealthScenarioSchema } from "./health-scenario.js";
4
+ import { kindRequiresClientProfile } from "./runtime/resolver-config.js";
5
+ import { resolverClientProfileFamily, resolveStealthProfileSelection } from "./stealth/profiles.js";
4
6
  export const DECLARATION_INVALID_CODE = "DECLARATION_INVALID";
5
7
  export const DECLARATION_RULE_IDS = {
6
8
  challengeShape: "credentials-challenge-shape",
@@ -14,6 +16,7 @@ export const DECLARATION_RULE_IDS = {
14
16
  proxySmartproxyGeo: "proxy-smartproxy-country-only",
15
17
  operationUpstreamProxy: "operation-upstream-proxy-unsupported",
16
18
  pinnedWireFieldPathValid: "public-schema-pinned-wire-field-valid",
19
+ resolverClientProfileFamily: "resolver-client-profile-family",
17
20
  };
18
21
  export function declarationInvalidError(violations) {
19
22
  const summary = violations
@@ -32,6 +35,7 @@ export function validateFailClosedDeclaration(provider) {
32
35
  validatePinnedWireFieldPaths(provider, violations);
33
36
  validateSchemaDeclaration(provider, violations);
34
37
  validateProxyDeclaration(provider, violations);
38
+ validateResolverClientProfileDeclaration(provider, violations);
35
39
  validateOperationDeclaration(provider, violations);
36
40
  if (violations.length > 0)
37
41
  throw declarationInvalidError(violations);
@@ -54,6 +58,7 @@ function collectProviderDeclarationViolations(provider, violations) {
54
58
  validateHealthDeclaration(provider, violations);
55
59
  validatePinnedWireFieldPaths(provider, violations);
56
60
  validateProxyDeclaration(provider, violations);
61
+ validateResolverClientProfileDeclaration(provider, violations);
57
62
  }
58
63
  function validatePinnedWireFieldPaths(provider, violations) {
59
64
  const pins = provider.meta?.contract?.pinnedWireFieldPaths;
@@ -245,6 +250,40 @@ function validateProxyDeclaration(provider, violations) {
245
250
  }
246
251
  }
247
252
  }
253
+ /**
254
+ * An akamai_sbsd solve runs on the provider's own stealth session, so the declared
255
+ * resolver.clientProfile must name the browser family that session emulates. The
256
+ * stealth runtime re-checks per request (a fetch may override the browser); this
257
+ * rule fails the declaration instead of the first challenged request.
258
+ */
259
+ function validateResolverClientProfileDeclaration(provider, violations) {
260
+ const resolver = provider.resolver;
261
+ if (!resolver ||
262
+ !Array.isArray(resolver.kinds) ||
263
+ !resolver.kinds.some(kindRequiresClientProfile) ||
264
+ typeof resolver.clientProfile !== "string") {
265
+ return;
266
+ }
267
+ let sessionBrowser;
268
+ try {
269
+ sessionBrowser = resolveStealthProfileSelection(provider.stealth).browser;
270
+ }
271
+ catch {
272
+ // An unsupported stealth selection is reported by the stealth runtime itself.
273
+ return;
274
+ }
275
+ const family = resolverClientProfileFamily(resolver.clientProfile);
276
+ if (family === sessionBrowser)
277
+ return;
278
+ violations.push({
279
+ ruleId: DECLARATION_RULE_IDS.resolverClientProfileFamily,
280
+ path: "resolver.clientProfile",
281
+ message: family
282
+ ? `"${resolver.clientProfile}" names ${family}, but the stealth session browser is ${sessionBrowser}.`
283
+ : `"${resolver.clientProfile}" does not name a supported browser family (chrome, firefox, safari).`,
284
+ fix: `Declare stealth.browser as the family resolver.clientProfile names, or set resolver.clientProfile to the ${sessionBrowser} profile the SBSD session runs on.`,
285
+ });
286
+ }
248
287
  function declaredProxyVendors(policy) {
249
288
  const vendors = [...(policy.providers ?? [])];
250
289
  if (policy.provider !== undefined)