@apifuse/provider-sdk 2.2.0-beta.62 → 2.2.0-beta.64

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 (261) hide show
  1. package/AUTHORING.md +367 -9
  2. package/CHANGELOG.md +8 -0
  3. package/LICENSE +21 -0
  4. package/README.md +1 -0
  5. package/bin/apifuse-check.ts +84 -2
  6. package/bin/apifuse-dev.ts +11 -31
  7. package/bin/apifuse-migrate-deployment.ts +200 -0
  8. package/bin/apifuse-pack-check.ts +11 -4
  9. package/bin/apifuse-pack-types.ts +187 -4
  10. package/bin/apifuse-record.ts +44 -32
  11. package/bin/apifuse-submit-check.ts +9 -5
  12. package/bin/apifuse-test.ts +3 -1
  13. package/dist/cli/commands.d.ts +1 -1
  14. package/dist/cli/commands.js +11 -0
  15. package/dist/cli/deployment-intent.d.ts +178 -0
  16. package/dist/cli/deployment-intent.js +277 -0
  17. package/dist/cli/migrate-deployment-intent.d.ts +132 -0
  18. package/dist/cli/migrate-deployment-intent.js +1552 -0
  19. package/dist/cli/migrate-provider-shape.d.ts +5 -1
  20. package/dist/cli/migrate-provider-shape.js +4 -2
  21. package/dist/cli/resolver-runtime.d.ts +21 -0
  22. package/dist/cli/resolver-runtime.js +31 -0
  23. package/dist/define.d.ts +3 -0
  24. package/dist/define.js +5 -0
  25. package/dist/dev.d.ts +3 -1
  26. package/dist/dev.js +6 -1
  27. package/dist/engine.d.ts +35 -1
  28. package/dist/engine.js +21 -2
  29. package/dist/error-resolution.d.ts +1 -0
  30. package/dist/error-resolution.js +54 -0
  31. package/dist/errors.d.ts +39 -1
  32. package/dist/errors.js +50 -0
  33. package/dist/handle-meta.d.ts +36 -1
  34. package/dist/handle-meta.js +16 -0
  35. package/dist/handle.d.ts +23 -5
  36. package/dist/handle.js +54 -8
  37. package/dist/health-scenario.d.ts +38 -0
  38. package/dist/i18n/error-messages.d.ts +86 -0
  39. package/dist/i18n/error-messages.js +192 -0
  40. package/dist/i18n/index.d.ts +1 -0
  41. package/dist/i18n/index.js +1 -0
  42. package/dist/i18n/keys.d.ts +12 -0
  43. package/dist/i18n/keys.js +76 -5
  44. package/dist/index.d.ts +11 -3
  45. package/dist/index.js +9 -1
  46. package/dist/lint.d.ts +15 -0
  47. package/dist/lint.js +469 -82
  48. package/dist/provider.d.ts +6 -6
  49. package/dist/provider.js +3 -3
  50. package/dist/runtime/browser-telemetry-binding.d.ts +5 -0
  51. package/dist/runtime/browser-telemetry-binding.js +83 -0
  52. package/dist/runtime/browser-telemetry.d.ts +76 -0
  53. package/dist/runtime/browser-telemetry.js +165 -0
  54. package/dist/runtime/browser.d.ts +3 -0
  55. package/dist/runtime/browser.js +208 -47
  56. package/dist/runtime/cache-telemetry.d.ts +71 -0
  57. package/dist/runtime/cache-telemetry.js +177 -0
  58. package/dist/runtime/cache.js +67 -7
  59. package/dist/runtime/capability-response.d.ts +3 -0
  60. package/dist/runtime/capability-response.js +56 -0
  61. package/dist/runtime/capability-telemetry.d.ts +46 -0
  62. package/dist/runtime/capability-telemetry.js +68 -0
  63. package/dist/runtime/diagnostic-env.d.ts +3 -0
  64. package/dist/runtime/diagnostic-env.js +10 -1
  65. package/dist/runtime/engine-mode.d.ts +74 -0
  66. package/dist/runtime/engine-mode.js +152 -0
  67. package/dist/runtime/handle.js +9 -1
  68. package/dist/runtime/http-telemetry-guard.d.ts +10 -0
  69. package/dist/runtime/http-telemetry-guard.js +62 -10
  70. package/dist/runtime/http.js +141 -35
  71. package/dist/runtime/instrumentation.js +92 -13
  72. package/dist/runtime/ocr-telemetry.d.ts +82 -0
  73. package/dist/runtime/ocr-telemetry.js +192 -0
  74. package/dist/runtime/ocr.js +35 -8
  75. package/dist/runtime/request-telemetry.d.ts +2 -0
  76. package/dist/runtime/request-telemetry.js +2 -1
  77. package/dist/runtime/resolver-runtime-options.d.ts +14 -0
  78. package/dist/runtime/resolver-runtime-options.js +33 -0
  79. package/dist/runtime/resolver-shared.d.ts +3 -2
  80. package/dist/runtime/resolver-shared.js +4 -4
  81. package/dist/runtime/resolver-telemetry.d.ts +1 -0
  82. package/dist/runtime/resolver-telemetry.js +1 -1
  83. package/dist/runtime/resolver-vendors/browser.d.ts +2 -0
  84. package/dist/runtime/resolver-vendors/browser.js +46 -0
  85. package/dist/runtime/resolver-vendors/hosts.d.ts +21 -0
  86. package/dist/runtime/resolver-vendors/hosts.js +35 -0
  87. package/dist/runtime/resolver-vendors/hypersolutions.js +2 -2
  88. package/dist/runtime/resolver-vendors/types.d.ts +5 -4
  89. package/dist/runtime/resolver.d.ts +4 -1
  90. package/dist/runtime/resolver.js +46 -13
  91. package/dist/runtime/state-telemetry.d.ts +79 -0
  92. package/dist/runtime/state-telemetry.js +194 -0
  93. package/dist/runtime/state.d.ts +3 -0
  94. package/dist/runtime/state.js +26 -7
  95. package/dist/runtime/stealth-cookies.d.ts +5 -1
  96. package/dist/runtime/stealth-cookies.js +21 -1
  97. package/dist/runtime/stealth-owned-headers.d.ts +26 -0
  98. package/dist/runtime/stealth-owned-headers.js +37 -0
  99. package/dist/runtime/stealth-telemetry.d.ts +92 -0
  100. package/dist/runtime/stealth-telemetry.js +179 -0
  101. package/dist/runtime/stealth.d.ts +4 -1
  102. package/dist/runtime/stealth.js +228 -85
  103. package/dist/runtime/stt-telemetry.d.ts +77 -0
  104. package/dist/runtime/stt-telemetry.js +176 -0
  105. package/dist/runtime/stt.js +15 -6
  106. package/dist/runtime-boundary-lint.d.ts +132 -0
  107. package/dist/runtime-boundary-lint.js +586 -0
  108. package/dist/server/error-stack-frames.d.ts +35 -0
  109. package/dist/server/error-stack-frames.js +219 -0
  110. package/dist/server/index.d.ts +7 -0
  111. package/dist/server/index.js +4 -0
  112. package/dist/server/sdk-version.d.ts +3 -0
  113. package/dist/server/sdk-version.js +19 -0
  114. package/dist/server/self-test.js +1 -18
  115. package/dist/server/sensitive-values.d.ts +1 -1
  116. package/dist/server/sensitive-values.js +9 -0
  117. package/dist/server/serve-implementation.d.ts +51 -2
  118. package/dist/server/serve-implementation.js +337 -90
  119. package/dist/server/types.d.ts +1 -0
  120. package/dist/server/types.js +7 -0
  121. package/dist/stateful/stateful-provider-owner-forwarder.js +22 -0
  122. package/dist/stateful/stateful-provider-runtime-context.d.ts +3 -0
  123. package/dist/stateful/stateful-provider-runtime-context.js +1 -0
  124. package/dist/types.d.ts +123 -1
  125. package/dist/typescript-module.d.ts +39 -0
  126. package/dist/typescript-module.js +72 -0
  127. package/package.json +6 -1
  128. package/src/cli/commands.ts +14 -0
  129. package/src/cli/deployment-intent.ts +364 -0
  130. package/src/cli/migrate-deployment-intent.ts +1908 -0
  131. package/src/cli/migrate-provider-shape.ts +10 -4
  132. package/src/cli/resolver-runtime.ts +72 -0
  133. package/src/define.ts +13 -0
  134. package/src/dev.ts +9 -2
  135. package/src/engine.ts +38 -2
  136. package/src/error-resolution.ts +58 -0
  137. package/src/errors.ts +77 -1
  138. package/src/handle-meta.ts +51 -1
  139. package/src/handle.ts +92 -10
  140. package/src/health-scenario.ts +38 -0
  141. package/src/i18n/error-messages.ts +255 -0
  142. package/src/i18n/index.ts +1 -0
  143. package/src/i18n/keys.ts +80 -6
  144. package/src/index.ts +86 -0
  145. package/src/lint.ts +604 -91
  146. package/src/provider.ts +12 -0
  147. package/src/runtime/browser-telemetry-binding.ts +92 -0
  148. package/src/runtime/browser-telemetry.ts +289 -0
  149. package/src/runtime/browser.ts +249 -65
  150. package/src/runtime/cache-telemetry.ts +247 -0
  151. package/src/runtime/cache.ts +68 -17
  152. package/src/runtime/capability-response.ts +53 -0
  153. package/src/runtime/capability-telemetry.ts +101 -0
  154. package/src/runtime/diagnostic-env.ts +11 -1
  155. package/src/runtime/engine-mode.ts +233 -0
  156. package/src/runtime/handle.ts +9 -1
  157. package/src/runtime/http-telemetry-guard.ts +66 -13
  158. package/src/runtime/http.ts +168 -37
  159. package/src/runtime/instrumentation.ts +105 -16
  160. package/src/runtime/ocr-telemetry.ts +308 -0
  161. package/src/runtime/ocr.ts +114 -74
  162. package/src/runtime/request-telemetry.ts +2 -1
  163. package/src/runtime/resolver-runtime-options.ts +50 -0
  164. package/src/runtime/resolver-shared.ts +9 -4
  165. package/src/runtime/resolver-telemetry.ts +1 -1
  166. package/src/runtime/resolver-vendors/browser.ts +65 -7
  167. package/src/runtime/resolver-vendors/hosts.ts +43 -0
  168. package/src/runtime/resolver-vendors/hypersolutions.ts +2 -2
  169. package/src/runtime/resolver-vendors/types.ts +5 -4
  170. package/src/runtime/resolver.ts +64 -21
  171. package/src/runtime/state-telemetry.ts +298 -0
  172. package/src/runtime/state.ts +46 -45
  173. package/src/runtime/stealth-cookies.ts +24 -1
  174. package/src/runtime/stealth-owned-headers.ts +41 -0
  175. package/src/runtime/stealth-telemetry.ts +304 -0
  176. package/src/runtime/stealth.ts +355 -150
  177. package/src/runtime/stt-telemetry.ts +282 -0
  178. package/src/runtime/stt.ts +93 -71
  179. package/src/runtime-boundary-lint.ts +703 -0
  180. package/src/server/error-stack-frames.ts +214 -0
  181. package/src/server/index.ts +53 -0
  182. package/src/server/sdk-version.ts +22 -0
  183. package/src/server/self-test.ts +1 -21
  184. package/src/server/sensitive-values.ts +14 -0
  185. package/src/server/serve-implementation.ts +569 -108
  186. package/src/server/types.ts +7 -0
  187. package/src/stateful/stateful-provider-owner-forwarder.ts +23 -0
  188. package/src/stateful/stateful-provider-runtime-context.ts +4 -0
  189. package/src/types.ts +128 -1
  190. package/src/typescript-module.ts +100 -0
  191. package/src/cli/__tests__/fixtures/migrate-operation-declaration/approval-override.ts.txt +0 -6
  192. package/src/cli/__tests__/fixtures/migrate-operation-declaration/codemod-syntax.ts.txt +0 -3
  193. package/src/cli/__tests__/fixtures/migrate-operation-declaration/connection-precedence.ts.txt +0 -10
  194. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-a.ts.txt +0 -3
  195. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-b.ts.txt +0 -3
  196. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-barrel.ts.txt +0 -2
  197. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-index.ts.txt +0 -3
  198. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-computed.ts.txt +0 -6
  199. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-a.ts.txt +0 -1
  200. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-b.ts.txt +0 -1
  201. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-index.ts.txt +0 -3
  202. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-barrel.ts.txt +0 -7
  203. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-catalog.ts.txt +0 -19
  204. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-docs.ts.txt +0 -6
  205. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-index.ts.txt +0 -3
  206. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-stores.ts.txt +0 -8
  207. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-indirect-config.ts.txt +0 -5
  208. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-nonstatic.ts.txt +0 -4
  209. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-package-index.ts.txt +0 -3
  210. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-index.ts.txt +0 -3
  211. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-leaf.ts.txt +0 -10
  212. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-one.ts.txt +0 -1
  213. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-two.ts.txt +0 -1
  214. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-composed-index.ts.txt +0 -3
  215. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-composed-one.ts.txt +0 -6
  216. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-composed-two.ts.txt +0 -6
  217. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-mixed-index.ts.txt +0 -14
  218. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-registry.ts.txt +0 -3
  219. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-barrel.ts.txt +0 -2
  220. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-index.ts.txt +0 -3
  221. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-leaf.ts.txt +0 -3
  222. package/src/cli/__tests__/fixtures/migrate-operation-declaration/docs-conflict.ts.txt +0 -8
  223. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-hoisted-array-spread.ts.txt +0 -15
  224. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-imported-array-spread.ts.txt +0 -9
  225. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-kebab-operation-id.ts.txt +0 -17
  226. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-map.ts.txt +0 -5
  227. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-mixed-array-spread.ts.txt +0 -24
  228. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-operation.ts.txt +0 -16
  229. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-snake-operation-id.ts.txt +0 -21
  230. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-duplicate-provider.ts.txt +0 -11
  231. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-id-unresolved.ts.txt +0 -7
  232. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-map.ts.txt +0 -3
  233. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-one-to-many.ts.txt +0 -18
  234. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-one-to-one.ts.txt +0 -13
  235. package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoist-all.ts.txt +0 -31
  236. package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoisted-const.ts.txt +0 -11
  237. package/src/cli/__tests__/fixtures/migrate-operation-declaration/imported-spread.ts.txt +0 -11
  238. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-map.ts.txt +0 -11
  239. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-cast-tail.ts.txt +0 -21
  240. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-ekitan.ts.txt +0 -11
  241. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-override.ts.txt +0 -14
  242. package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-canonical-en.json +0 -1
  243. package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-canonical-ja.json +0 -1
  244. package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-canonical-ko.json +0 -1
  245. package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-existing-operation-namespace-en.json +0 -8
  246. package/src/cli/__tests__/fixtures/migrate-operation-declaration/missing-english-locale.ts.txt +0 -7
  247. package/src/cli/__tests__/fixtures/migrate-operation-declaration/no-safety.ts.txt +0 -6
  248. package/src/cli/__tests__/fixtures/migrate-operation-declaration/non-literal.ts.txt +0 -7
  249. package/src/cli/__tests__/fixtures/migrate-operation-declaration/redundant-approval.ts.txt +0 -6
  250. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-binding-ambiguous.ts.txt +0 -12
  251. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-imported-other.ts.txt +0 -7
  252. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-imported.ts.txt +0 -5
  253. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-key-ambiguous.ts.txt +0 -19
  254. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-shorthand.ts.txt +0 -9
  255. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-typed.ts.txt +0 -11
  256. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-unrelated.ts.txt +0 -13
  257. package/src/cli/__tests__/fixtures/migrate-operation-declaration/safety-conflict.ts.txt +0 -7
  258. package/src/cli/__tests__/fixtures/migrate-operation-declaration/stream.ts.txt +0 -7
  259. package/src/cli/__tests__/fixtures/migrate-operation-declaration/tool-router-spread.ts.txt +0 -15
  260. package/src/cli/__tests__/fixtures/migrate-operation-declaration/unparseable.ts.txt +0 -4
  261. package/src/cli/__tests__/fixtures/migrate-operation-declaration/verbatim-template.ts.txt +0 -12
@@ -0,0 +1,152 @@
1
+ import { createInProcessProviderEngine, PROVIDER_ENGINE_MODE_ENV, } from "../engine.js";
2
+ import { ProviderError, ValidationError } from "../errors.js";
3
+ /**
4
+ * Engine attachment selection. Internal on purpose: ADR-0011 removes the
5
+ * in-process lane, so every symbol exported here would become a breaking removal
6
+ * later. The server surfaces the outcome on the `provider_engine_mode` boot
7
+ * event instead.
8
+ */
9
+ const KNOWN_PROVIDER_ENGINE_MODES = ["in-process", "remote"];
10
+ /** Mode used when neither `engineMode` nor `APIFUSE__ENGINE__MODE` names one. */
11
+ export const DEFAULT_PROVIDER_ENGINE_MODE = "in-process";
12
+ /** Remote engine client endpoint; engine-owned, so never declarable as a provider secret. */
13
+ export const REMOTE_ENGINE_CLIENT_URL_ENV = "APIFUSE__ENGINE__URL";
14
+ /** Remote engine client credential; registered with the diagnostic redactor by sensitive-values. */
15
+ export const REMOTE_ENGINE_CLIENT_API_KEY_ENV = "APIFUSE__ENGINE__API_KEY";
16
+ /** Engine env names that only mean something in remote mode. */
17
+ const REMOTE_ONLY_ENGINE_ENV_NAMES = [
18
+ REMOTE_ENGINE_CLIENT_URL_ENV,
19
+ REMOTE_ENGINE_CLIENT_API_KEY_ENV,
20
+ ];
21
+ function isKnownMode(value) {
22
+ return KNOWN_PROVIDER_ENGINE_MODES.includes(value);
23
+ }
24
+ function normalize(value) {
25
+ if (typeof value !== "string")
26
+ return undefined;
27
+ const trimmed = value.trim().toLowerCase();
28
+ return trimmed === "" ? undefined : trimmed;
29
+ }
30
+ /**
31
+ * Resolve the engine attachment mode once at boot.
32
+ *
33
+ * Precedence: an explicit `engine` object, then `APIFUSE__ENGINE__MODE`, then the
34
+ * `engineMode` option, then the default. The manifest outranks the option so
35
+ * provider code cannot silently downgrade what the deployment asked for; every
36
+ * disagreement is reported in `warnings` with a truthful `source` rather than
37
+ * being dropped.
38
+ *
39
+ * Deployment-authored input is never fatal (an unrecognized env value is warned
40
+ * about and ignored). A malformed `engineMode` option is a fault in the calling
41
+ * code, which no manifest can reach, so it throws.
42
+ */
43
+ export function resolveProviderEngineMode(input = {}) {
44
+ const environment = input.environment ?? process.env;
45
+ const warnings = [];
46
+ // Read directly: this is a mode name, not a credential, so it must not be
47
+ // registered with the diagnostic redactor the way readDiagnosticEnv is.
48
+ const rawEnv = normalize(environment[PROVIDER_ENGINE_MODE_ENV]);
49
+ let envMode;
50
+ if (rawEnv !== undefined) {
51
+ if (isKnownMode(rawEnv))
52
+ envMode = rawEnv;
53
+ else
54
+ warnings.push("invalid_env_value");
55
+ }
56
+ const optionMode = normalize(input.option);
57
+ if (input.option !== undefined && (optionMode === undefined || !isKnownMode(optionMode))) {
58
+ throw new ValidationError(`Invalid engineMode option "${String(input.option)}"; expected ${KNOWN_PROVIDER_ENGINE_MODES.join(" or ")}`, { fix: 'Pass engineMode: "in-process" | "remote", or omit it to use the default.' });
59
+ }
60
+ if (optionMode !== undefined && envMode !== undefined && optionMode !== envMode) {
61
+ warnings.push("env_overrode_option");
62
+ }
63
+ const requested = envMode ?? optionMode;
64
+ const requestedSource = envMode !== undefined ? "env" : optionMode !== undefined ? "option" : undefined;
65
+ // Normalized like every other mode input: an un-normalized host `kind` would
66
+ // reach the boot event and /readyz verbatim and mis-score the fleet audit.
67
+ const engineKind = input.engine === undefined ? undefined : (normalize(input.engine.kind) ?? "custom");
68
+ if (engineKind !== undefined) {
69
+ if (requested !== undefined && engineKind !== requested) {
70
+ warnings.push("engine_object_overrode_requested_mode");
71
+ }
72
+ return finalize(engineKind, "engine", environment, warnings);
73
+ }
74
+ return finalize(requested ?? DEFAULT_PROVIDER_ENGINE_MODE, requestedSource ?? "default", environment, warnings);
75
+ }
76
+ function finalize(mode, source, environment, warnings) {
77
+ // Presence of the remote engine env never selects a mode (the manifest
78
+ // generator projects it fleet-wide before the SDK can use it), but staying
79
+ // silent about it would hide a half-applied cutover. Skipped for a host-supplied
80
+ // engine: that object may be a remote client that consumes the env itself, so
81
+ // claiming the env is unused would be a false report.
82
+ if (mode !== "remote" &&
83
+ source !== "engine" &&
84
+ REMOTE_ONLY_ENGINE_ENV_NAMES.some((name) => normalize(environment[name]) !== undefined)) {
85
+ warnings.push("engine_env_ignored");
86
+ }
87
+ // Fail-closed for the ADR-0011 audit: everything that is not the remote lane
88
+ // still attaches capabilities inside the pod process and is scheduled for
89
+ // removal, including an opaque host engine that declares no kind.
90
+ return { mode, source, deprecated: mode !== "remote", warnings };
91
+ }
92
+ const UNAVAILABLE_ENGINES = new WeakSet();
93
+ /** True when the engine cannot attach, so readiness must report the process as unready. */
94
+ export function isUnavailableProviderEngine(engine) {
95
+ return UNAVAILABLE_ENGINES.has(engine);
96
+ }
97
+ /**
98
+ * Engine that refuses every attachment with `reason`.
99
+ *
100
+ * Requesting a mode this release cannot serve must not crash the process: the SDK
101
+ * answers a structured error per request and reports the process unready, the same
102
+ * trade-off `provider_secrets_missing` already makes at boot. Non-retryable, so a
103
+ * caller-side retry loop cannot turn a configuration fault into load.
104
+ */
105
+ export function createUnavailableProviderEngine(kind, reason) {
106
+ const engine = {
107
+ kind,
108
+ attach: () => {
109
+ throw new ProviderError(reason.message, {
110
+ code: reason.code,
111
+ retryable: false,
112
+ fix: reason.fix,
113
+ });
114
+ },
115
+ };
116
+ UNAVAILABLE_ENGINES.add(engine);
117
+ return engine;
118
+ }
119
+ /**
120
+ * Engine for a resolved mode. `remote` has no client in this release, so it
121
+ * resolves to an engine that fails every request instead of falling back to
122
+ * in-process (ADR-0011 Pitfall 2: in-process is never a fallback).
123
+ */
124
+ export function createEngineForMode(mode) {
125
+ // Default-deny on the mode name, not just on "remote": the mode union is open
126
+ // so a later lane can be named, and falling through to in-process for an
127
+ // unserved lane is the silent downgrade ADR-0011 Pitfall 2 forbids.
128
+ if (mode === DEFAULT_PROVIDER_ENGINE_MODE)
129
+ return createInProcessProviderEngine();
130
+ return createUnavailableProviderEngine(mode, {
131
+ message: `Provider engine mode "${mode}" was requested but this @apifuse/provider-sdk release only serves the ${DEFAULT_PROVIDER_ENGINE_MODE} engine`,
132
+ code: "PROVIDER_ENGINE_MODE_UNSUPPORTED",
133
+ fix: `Unset ${PROVIDER_ENGINE_MODE_ENV} (or the engineMode option), or upgrade to a release that serves this mode.`,
134
+ });
135
+ }
136
+ /**
137
+ * Fail fast for the CLIs (`apifuse dev`, `apifuse record`). They are developer
138
+ * tools, not pods: an unusable mode should stop the command rather than start a
139
+ * server that refuses every request.
140
+ */
141
+ export function assertProcessEngineModeSupported(environment = process.env) {
142
+ const resolution = resolveProviderEngineMode({ environment });
143
+ if (resolution.mode !== DEFAULT_PROVIDER_ENGINE_MODE) {
144
+ throw new ProviderError(`Provider engine mode "${resolution.mode}" was requested but this @apifuse/provider-sdk release only serves the ${DEFAULT_PROVIDER_ENGINE_MODE} engine`, {
145
+ code: "PROVIDER_ENGINE_MODE_UNSUPPORTED",
146
+ retryable: false,
147
+ details: { mode: resolution.mode, source: resolution.source },
148
+ fix: `Unset ${PROVIDER_ENGINE_MODE_ENV}, or upgrade to a release that serves this mode.`,
149
+ });
150
+ }
151
+ return resolution;
152
+ }
@@ -11,7 +11,7 @@
11
11
  import { randomInt, randomUUID } from "node:crypto";
12
12
  import { isProviderError } from "../errors.js";
13
13
  import { HandleError, handleRecoverySentence, isDraftKind, } from "../handle.js";
14
- import { formatHandleIssuers } from "../handle-meta.js";
14
+ import { formatHandleIssuers, handleHasDirectionalFieldNames } from "../handle-meta.js";
15
15
  import { BOUND_HANDLE_WORD_COUNT, findHandleWordWithinDistance, HIGH_PUBLIC_HANDLE_WORD_COUNT, ISSUABLE_HANDLE_WORDS, PUBLIC_HANDLE_WORD_COUNT, } from "./handle-wordlist.js";
16
16
  import { createMemoryProviderRuntimeState } from "./state.js";
17
17
  const RECORD_VERSION = 1;
@@ -153,6 +153,14 @@ export function formatNormalizationClasses(classes) {
153
153
  // Errors (spec §5)
154
154
  // ---------------------------------------------------------------------------
155
155
  function publicCollapsedMessage(kind) {
156
+ // "a new one" is unambiguous only while the kind has one name. When it is
157
+ // issued under `next_cursor` and accepted under `cursor`, the caller has to
158
+ // be told which field to read, so use the full recovery sentence — it names
159
+ // both keys and is identical for invalid / expired / not-found, which is
160
+ // what keeps the public collapse indistinguishable.
161
+ if (handleHasDirectionalFieldNames(kind)) {
162
+ return `\`${kind.fieldName}\` is not valid or has expired. ${handleRecoverySentence(kind)}`;
163
+ }
156
164
  const issuers = formatHandleIssuers(kind.issuedBy);
157
165
  const again = issuers ? `Call ${issuers} again to get a new one.` : "Request a new one.";
158
166
  return `\`${kind.fieldName}\` is not valid or has expired. ${again}`;
@@ -1,6 +1,16 @@
1
1
  import type { HttpRetrySummary } from "../types.js";
2
2
  import { type HttpTelemetryRequestSink, type HttpTelemetrySink } from "./http-telemetry.js";
3
+ /** Shared synchronous observer boundary for transport and capability contributors. */
4
+ export declare function observeTelemetry<T>(callback: () => T, fail: () => void): T | undefined;
5
+ /** Isolate synchronous void capability hooks using the same Promise defence. */
6
+ export declare function createTelemetryObserver(sink: {
7
+ markTelemetryFailed?(): void;
8
+ } | undefined): (callback: () => unknown) => void;
3
9
  /** Keep all public observers, including property access, outside transport failures. */
4
10
  export declare function startHttpTelemetry(sink: HttpTelemetrySink | undefined, retryPreset: HttpRetrySummary["preset"]): HttpTelemetryRequestSink & {
5
11
  observe(callback: () => void): void;
6
12
  };
13
+ /** Generic observer guard shared by capability hooks. Returned thenables are absorbed. */
14
+ export declare function observeTelemetryCallback(sink: {
15
+ markTelemetryFailed?: () => unknown;
16
+ } | undefined, callback: () => unknown): void;
@@ -1,3 +1,4 @@
1
+ import { isPromise } from "node:util/types";
1
2
  import { HttpTelemetryCollector, } from "./http-telemetry.js";
2
3
  const intrinsicPromiseThen = Promise.prototype.then;
3
4
  const ignoreObserverResult = () => { };
@@ -44,7 +45,7 @@ function isAsyncObserverReturn(value) {
44
45
  if (!value || (typeof value !== "object" && typeof value !== "function"))
45
46
  return false;
46
47
  try {
47
- if (!("then" in value) || typeof value.then !== "function")
48
+ if (!isPromise(value) && (!("then" in value) || typeof value.then !== "function"))
48
49
  return false;
49
50
  }
50
51
  catch {
@@ -53,8 +54,23 @@ function isAsyncObserverReturn(value) {
53
54
  absorbObserverPromise(value);
54
55
  return true;
55
56
  }
56
- /** Keep all public observers, including property access, outside transport failures. */
57
- export function startHttpTelemetry(sink, retryPreset) {
57
+ /** Shared synchronous observer boundary for transport and capability contributors. */
58
+ export function observeTelemetry(callback, fail) {
59
+ try {
60
+ const result = callback();
61
+ if (isAsyncObserverReturn(result)) {
62
+ fail();
63
+ return undefined;
64
+ }
65
+ return result;
66
+ }
67
+ catch {
68
+ fail();
69
+ return undefined;
70
+ }
71
+ }
72
+ /** Isolate synchronous void capability hooks using the same Promise defence. */
73
+ export function createTelemetryObserver(sink) {
58
74
  let failed = false;
59
75
  const fail = () => {
60
76
  if (failed)
@@ -64,24 +80,37 @@ export function startHttpTelemetry(sink, retryPreset) {
64
80
  isAsyncObserverReturn(sink?.markTelemetryFailed?.());
65
81
  }
66
82
  catch {
67
- // The failure reporter is itself an observer. Never recurse.
83
+ // The failure marker is also an observer.
68
84
  }
69
85
  };
70
- const observe = (callback) => {
86
+ return (callback) => {
71
87
  try {
72
88
  const result = callback();
73
- if (isAsyncObserverReturn(result)) {
74
- // The observer API is synchronous. Absorb invalid asynchronous returns too.
89
+ if (result !== undefined) {
90
+ isAsyncObserverReturn(result);
75
91
  fail();
76
- return undefined;
77
92
  }
78
- return result;
79
93
  }
80
94
  catch {
81
95
  fail();
82
- return undefined;
83
96
  }
84
97
  };
98
+ }
99
+ /** Keep all public observers, including property access, outside transport failures. */
100
+ export function startHttpTelemetry(sink, retryPreset) {
101
+ let failed = false;
102
+ const fail = () => {
103
+ if (failed)
104
+ return;
105
+ failed = true;
106
+ try {
107
+ isAsyncObserverReturn(sink?.markTelemetryFailed?.());
108
+ }
109
+ catch {
110
+ // The failure reporter is itself an observer. Never recurse.
111
+ }
112
+ };
113
+ const observe = (callback) => observeTelemetry(callback, fail);
85
114
  const result = observe(() => (sink ?? new HttpTelemetryCollector()).startRequest({ retryPreset }));
86
115
  const request = observe(() => {
87
116
  if (!result ||
@@ -148,3 +177,26 @@ export function startHttpTelemetry(sink, retryPreset) {
148
177
  },
149
178
  };
150
179
  }
180
+ /** Generic observer guard shared by capability hooks. Returned thenables are absorbed. */
181
+ export function observeTelemetryCallback(sink, callback) {
182
+ let failed = false;
183
+ const fail = () => {
184
+ if (failed)
185
+ return;
186
+ failed = true;
187
+ try {
188
+ isAsyncObserverReturn(sink?.markTelemetryFailed?.());
189
+ }
190
+ catch {
191
+ // Observer failure is never transport failure.
192
+ }
193
+ };
194
+ try {
195
+ const result = callback();
196
+ if (isAsyncObserverReturn(result) || result !== undefined)
197
+ fail();
198
+ }
199
+ catch {
200
+ fail();
201
+ }
202
+ }
@@ -84,19 +84,92 @@ function hasHeader(headers, name) {
84
84
  const needle = name.toLowerCase();
85
85
  return Object.keys(headers).some((key) => key.toLowerCase() === needle);
86
86
  }
87
- function withClientHeaders(options, clientOptions, body) {
88
- const headers = {
87
+ function withClientHeaderDefaults(headers, clientOptions, body) {
88
+ const merged = {
89
89
  ...(clientOptions.userAgent ? { "User-Agent": clientOptions.userAgent } : {}),
90
- ...options?.headers,
90
+ ...headers,
91
91
  };
92
- if (body !== undefined && !hasHeader(headers, "Content-Type")) {
93
- headers["Content-Type"] = "application/json";
92
+ if (body !== undefined && !hasHeader(merged, "Content-Type")) {
93
+ merged["Content-Type"] = "application/json";
94
94
  }
95
+ return merged;
96
+ }
97
+ function withClientHeaders(options, clientOptions, body) {
98
+ // The ambient client signal is merged into the per-request one here so every
99
+ // downstream read (retry loop, sleep, body consumption) sees a single signal.
100
+ // The header-factory branch needs it too, otherwise factory-header requests
101
+ // would lose ambient cancellation entirely.
102
+ const signal = mergeAbortSignals(clientOptions.signal, options?.signal);
103
+ // A header factory is resolved per issued attempt (resolveAttemptHeaders);
104
+ // the client defaults are applied to its output at that point.
105
+ if (typeof options?.headers === "function")
106
+ return { ...options, signal };
95
107
  return {
96
108
  ...options,
97
- headers,
109
+ headers: withClientHeaderDefaults(options?.headers, clientOptions, body),
110
+ signal,
98
111
  };
99
112
  }
113
+ function headerFactoryFailure(message, cause) {
114
+ // A dedicated code keeps a broken proof minter out of the retry predicate:
115
+ // toHttpTransportError would otherwise brand a plain throw as
116
+ // transport_network_error, which the default retry policy re-issues.
117
+ // retryable:false is explicit so the served error envelope does not
118
+ // advertise a deterministic signing failure as retryable, and
119
+ // category:provider_error keeps a provider-local minting bug out of the
120
+ // upstream-failure signal (status 0 would otherwise fall through the
121
+ // observability ladder to upstream_http / source upstream_failure).
122
+ return new TransportError(message, {
123
+ code: "http_header_factory_failed",
124
+ status: 0,
125
+ retryable: false,
126
+ category: "provider_error",
127
+ ...(cause instanceof Error ? { cause } : {}),
128
+ });
129
+ }
130
+ function isHeaderRecord(value) {
131
+ return value !== null && typeof value === "object" && !Array.isArray(value);
132
+ }
133
+ // Settle `promise`, or reject with the signal's reason as soon as it aborts, so
134
+ // a pending header factory cannot outlive the request deadline or an ambient
135
+ // cancellation (native fetch has not started yet, so nothing else observes them).
136
+ function settleBeforeAbort(promise, signal) {
137
+ if (!signal)
138
+ return promise;
139
+ return new Promise((resolve, reject) => {
140
+ const onAbort = () => reject(signal.reason);
141
+ if (signal.aborted)
142
+ onAbort();
143
+ else
144
+ signal.addEventListener("abort", onAbort, { once: true });
145
+ // Always attach handlers: a late rejection after the abort must settle
146
+ // here (a no-op on the already-rejected promise), never go unhandled.
147
+ promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
148
+ });
149
+ }
150
+ async function resolveAttemptHeaders(options, clientOptions, attempt, signal) {
151
+ const { headers } = options;
152
+ if (typeof headers !== "function")
153
+ return headers;
154
+ // The per-call timeout may have fired during proxy resolution: do not start
155
+ // the factory at all once the request is already aborted.
156
+ if (signal?.aborted)
157
+ throw signal.reason;
158
+ let minted;
159
+ try {
160
+ minted = await settleBeforeAbort(Promise.resolve().then(() => headers(attempt())), signal);
161
+ }
162
+ catch (error) {
163
+ // An abort is classified by toHttpTransportError (cancelled / timed out).
164
+ if (signal?.aborted)
165
+ throw error;
166
+ throw headerFactoryFailure("Request header factory failed", error);
167
+ }
168
+ if (!isHeaderRecord(minted)) {
169
+ throw headerFactoryFailure("Request header factory must return a header record");
170
+ }
171
+ return withClientHeaderDefaults(minted, clientOptions, options.body);
172
+ }
100
173
  function parseHttpData(body, headers) {
101
174
  const contentType = headers["content-type"] ?? headers["Content-Type"] ?? headers["CONTENT-TYPE"];
102
175
  if (contentType?.includes("application/json")) {
@@ -552,23 +625,29 @@ function serializeHttpRequestUrl(baseUrl, url, options) {
552
625
  throw redactSensitiveRequestError(error, url, options.sensitiveParams);
553
626
  }
554
627
  }
555
- async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, warn, statusRetryCodes, proxyAttemptOffset = 0, dedupe, observeProxy) {
628
+ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, warn, statusRetryCodes, proxyAttemptOffset = 0, dedupe, observeProxy, nextHeaderAttempt = () => 1) {
556
629
  const serializedUrl = serializeHttpRequestUrl(baseUrl, url, options);
557
630
  const { requestUrl } = serializedUrl;
558
631
  const controller = options.timeout ? new AbortController() : undefined;
559
- const signal = mergeAbortSignals(clientOptions.signal, controller?.signal);
560
- const timeoutHandle = options.timeout
632
+ const signal = mergeAbortSignals(options.signal, controller?.signal);
633
+ let timeoutHandle = options.timeout
561
634
  ? setTimeout(() => controller?.abort(), options.timeout)
562
635
  : undefined;
636
+ const disarmTimeout = () => {
637
+ if (!timeoutHandle)
638
+ return;
639
+ clearTimeout(timeoutHandle);
640
+ timeoutHandle = undefined;
641
+ };
563
642
  let proxy;
564
643
  try {
565
- throwIfAmbientAborted(clientOptions.signal);
644
+ throwIfAmbientAborted(options.signal);
566
645
  // Resolve inside the try (and after the timeout is armed) so allocator
567
646
  // failures are branded as TransportErrors and count against the request
568
647
  // deadline, exactly as an inline resolve would.
569
648
  proxy = await resolveNativeProxy(options, clientOptions, warn, proxyAttemptOffset);
570
649
  observeProxy?.(Boolean(proxy));
571
- throwIfAmbientAborted(clientOptions.signal);
650
+ throwIfAmbientAborted(options.signal);
572
651
  // For a registry allocator chain, skip an endpoint a prior attempt already
573
652
  // tried rather than re-issuing the same request. Returning the sentinel
574
653
  // (instead of breaking) lets the loop keep advancing the flat offset until
@@ -579,8 +658,12 @@ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, war
579
658
  }
580
659
  dedupe.attempted.add(proxy);
581
660
  }
661
+ // Resolve after the dedupe check so a skipped duplicate offset never burns
662
+ // a single-use proof, and with the serialized URL so URL-bound proofs
663
+ // match what is actually issued.
664
+ const requestHeaders = await resolveAttemptHeaders(options, clientOptions, () => ({ attempt: nextHeaderAttempt(), url: requestUrl, method }), signal);
582
665
  const requestInit = {
583
- headers: options.headers,
666
+ headers: requestHeaders,
584
667
  method,
585
668
  ...(proxy ? { proxy } : {}),
586
669
  ...(signal ? { signal } : {}),
@@ -608,36 +691,45 @@ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, war
608
691
  status: response.status,
609
692
  });
610
693
  }
611
- return toNativeHttpResponse(response);
694
+ // `timeout` bounds the header phase (types.ts RequestOptions.timeout), so it
695
+ // is disarmed before the body is read: awaiting the buffered body inside
696
+ // this try brands a mid-body failure as a transport error, but must not
697
+ // extend the deadline over the download. Cancellation still applies —
698
+ // `signal` stays attached to the native response. The discarded non-2xx
699
+ // bodies drained above stay under the deadline on purpose: nothing else
700
+ // bounds them, and their content is never returned to the caller.
701
+ disarmTimeout();
702
+ return await toNativeHttpResponse(response);
612
703
  }
613
704
  catch (error) {
614
705
  if (error instanceof SyntaxError) {
615
706
  throw redactSensitiveError(error, serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
616
707
  }
617
- const transportError = redactSensitiveError(toHttpTransportError(error, clientOptions.signal, controller?.signal), serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
708
+ const transportError = redactSensitiveError(toHttpTransportError(error, options.signal, controller?.signal), serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
618
709
  transportError.proxyUsed = Boolean(proxy);
619
710
  throw transportError;
620
711
  }
621
712
  finally {
622
- if (timeoutHandle)
623
- clearTimeout(timeoutHandle);
713
+ disarmTimeout();
624
714
  }
625
715
  }
626
716
  async function fetchNativeHttpStream(baseUrl, url, method, options, clientOptions, warn, observeProxy) {
627
717
  const serializedUrl = serializeHttpRequestUrl(baseUrl, url, options);
628
718
  const { requestUrl } = serializedUrl;
629
719
  const controller = options.timeout ? new AbortController() : undefined;
630
- const signal = mergeAbortSignals(clientOptions.signal, controller?.signal);
720
+ const signal = mergeAbortSignals(options.signal, controller?.signal);
631
721
  const timeoutHandle = options.timeout
632
722
  ? setTimeout(() => controller?.abort(), options.timeout)
633
723
  : undefined;
634
724
  try {
635
- throwIfAmbientAborted(clientOptions.signal);
725
+ throwIfAmbientAborted(options.signal);
636
726
  const proxy = await resolveNativeProxy(options, clientOptions, warn);
637
727
  observeProxy?.(Boolean(proxy));
638
- throwIfAmbientAborted(clientOptions.signal);
728
+ throwIfAmbientAborted(options.signal);
729
+ // Streams never retry: a header factory is resolved once, as attempt 1.
730
+ const headers = await resolveAttemptHeaders(options, clientOptions, () => ({ attempt: 1, url: requestUrl, method }), signal);
639
731
  const requestInit = {
640
- headers: options.headers,
732
+ headers,
641
733
  method,
642
734
  ...(proxy ? { proxy } : {}),
643
735
  ...(signal ? { signal } : {}),
@@ -653,15 +745,15 @@ async function fetchNativeHttpStream(baseUrl, url, method, options, clientOption
653
745
  status: response.status,
654
746
  });
655
747
  }
656
- // Per-call timeout remains header-scoped, while the ambient request signal
657
- // stays attached to the response body for its full consumption lifetime.
658
- return toNativeHttpStreamResponse(response, serializedUrl, clientOptions.signal);
748
+ // The stream timeout remains header-scoped; composed ambient and local
749
+ // cancellation stays attached to native fetch for the body's full lifetime.
750
+ return toNativeHttpStreamResponse(response, serializedUrl, options.signal);
659
751
  }
660
752
  catch (error) {
661
753
  if (error instanceof SyntaxError) {
662
754
  throw redactSensitiveError(error, serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
663
755
  }
664
- throw redactSensitiveError(toHttpTransportError(error, clientOptions.signal, controller?.signal), serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
756
+ throw redactSensitiveError(toHttpTransportError(error, options.signal, controller?.signal), serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
665
757
  }
666
758
  finally {
667
759
  if (timeoutHandle)
@@ -772,6 +864,15 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
772
864
  method: methodName,
773
865
  });
774
866
  const dedupeContext = dedupeAllocatorEndpoints ? { attempted: new Set() } : undefined;
867
+ // Numbers the attempts a header factory sees: only requests that reached
868
+ // header resolution count, so a failed proxy allocation (which increments
869
+ // the telemetry `issued` count below) or a dedupe-skipped offset does not
870
+ // make the first request that is actually built report `attempt: 2`.
871
+ let headerAttempts = 0;
872
+ const nextHeaderAttempt = () => {
873
+ headerAttempts += 1;
874
+ return headerAttempts;
875
+ };
775
876
  const executeOnce = async (proxyAttemptOffset = 0) => {
776
877
  const started = performance.now();
777
878
  let proxyUsed = false;
@@ -783,7 +884,7 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
783
884
  const outcome = await fetchNativeHttp(baseUrl, url, methodName, attemptOptions, clientOptions, warnOnce, statusRetryEnabled ? retryOptions?.statusCodes : undefined, proxyAttemptOffset, dedupeContext, (used, status) => {
784
885
  proxyUsed = used;
785
886
  observedStatus = status;
786
- });
887
+ }, nextHeaderAttempt);
787
888
  if (!isDedupeSkipOutcome(outcome))
788
889
  record({
789
890
  status: outcome.status,
@@ -800,7 +901,7 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
800
901
  }
801
902
  };
802
903
  if (!retryEnabled || !retryOptions) {
803
- throwIfAmbientAborted(clientOptions.signal);
904
+ throwIfAmbientAborted(headersOptions.signal);
804
905
  const outcome = await executeOnce();
805
906
  if (isDedupeSkipOutcome(outcome)) {
806
907
  // Single-shot path never de-duplicates (dedupeContext is undefined),
@@ -822,7 +923,7 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
822
923
  // allocations skip offsets.
823
924
  let issued = 0;
824
925
  for (let attempt = 1; attempt <= transportAttemptCap; attempt += 1) {
825
- throwIfAmbientAborted(clientOptions.signal);
926
+ throwIfAmbientAborted(headersOptions.signal);
826
927
  // Whether this offset actually issued a request (vs. a skipped duplicate),
827
928
  // so the catch counts a thrown *transport* failure once without
828
929
  // double-counting a status outcome that already incremented before it
@@ -840,7 +941,7 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
840
941
  issuedThisAttempt = true;
841
942
  if (isHttpStatusOutcome(outcome)) {
842
943
  if (outcome.retryable && issued < retryOptions.attempts) {
843
- await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers), clientOptions.signal);
944
+ await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers), headersOptions.signal);
844
945
  continue;
845
946
  }
846
947
  throw toUpstreamHttpError(outcome.status);
@@ -857,7 +958,7 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
857
958
  return response;
858
959
  }
859
960
  catch (error) {
860
- throwIfAmbientAborted(clientOptions.signal);
961
+ throwIfAmbientAborted(headersOptions.signal);
861
962
  if (!issuedThisAttempt)
862
963
  issued += 1;
863
964
  lastError = error;
@@ -870,7 +971,7 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
870
971
  options: retryOptions,
871
972
  proxyUsed,
872
973
  })) {
873
- await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt), clientOptions.signal);
974
+ await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt), headersOptions.signal);
874
975
  continue;
875
976
  }
876
977
  throw error;
@@ -911,7 +1012,7 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
911
1012
  // Streams never retry. Observe the header fetch without consuming its body.
912
1013
  const telemetry = startHttpTelemetry(scopedHttpTelemetry(clientOptions.httpTelemetry), "off");
913
1014
  const started = performance.now();
914
- const alreadyAborted = clientOptions.signal?.aborted === true;
1015
+ const alreadyAborted = headersOptions.signal?.aborted === true;
915
1016
  let proxyUsed = false;
916
1017
  const record = (event) => {
917
1018
  if (alreadyAborted)
@@ -941,10 +1042,15 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
941
1042
  delete: async (url, options) => request(url, "DELETE", options),
942
1043
  stream: async (url, options = {}) => streamRequest(url, options.method ?? "GET", options),
943
1044
  sse: async (url, options = {}) => {
944
- const headers = {
945
- Accept: "text/event-stream",
946
- ...options.headers,
947
- };
1045
+ const callerHeaders = options.headers;
1046
+ const headers = typeof callerHeaders === "function"
1047
+ ? async (attempt) => {
1048
+ // Pass a malformed result through unchanged so the factory
1049
+ // validation rejects it instead of spreading it into `{}`.
1050
+ const minted = await callerHeaders(attempt);
1051
+ return isHeaderRecord(minted) ? { Accept: "text/event-stream", ...minted } : minted;
1052
+ }
1053
+ : { Accept: "text/event-stream", ...callerHeaders };
948
1054
  const response = await streamRequest(url, options.method ?? "GET", {
949
1055
  ...options,
950
1056
  headers,