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

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 (243) hide show
  1. package/AUTHORING.md +268 -9
  2. package/CHANGELOG.md +4 -0
  3. package/README.md +1 -0
  4. package/bin/apifuse-check.ts +84 -2
  5. package/bin/apifuse-dev.ts +11 -31
  6. package/bin/apifuse-migrate-deployment.ts +200 -0
  7. package/bin/apifuse-pack-check.ts +11 -4
  8. package/bin/apifuse-pack-types.ts +187 -4
  9. package/bin/apifuse-record.ts +44 -32
  10. package/bin/apifuse-test.ts +3 -1
  11. package/dist/cli/commands.d.ts +1 -1
  12. package/dist/cli/commands.js +11 -0
  13. package/dist/cli/deployment-intent.d.ts +178 -0
  14. package/dist/cli/deployment-intent.js +277 -0
  15. package/dist/cli/migrate-deployment-intent.d.ts +132 -0
  16. package/dist/cli/migrate-deployment-intent.js +1552 -0
  17. package/dist/cli/migrate-provider-shape.d.ts +5 -1
  18. package/dist/cli/migrate-provider-shape.js +4 -2
  19. package/dist/cli/resolver-runtime.d.ts +21 -0
  20. package/dist/cli/resolver-runtime.js +31 -0
  21. package/dist/define.d.ts +3 -0
  22. package/dist/define.js +5 -0
  23. package/dist/dev.d.ts +3 -1
  24. package/dist/dev.js +6 -1
  25. package/dist/engine.d.ts +35 -1
  26. package/dist/engine.js +21 -2
  27. package/dist/error-resolution.d.ts +1 -0
  28. package/dist/error-resolution.js +54 -0
  29. package/dist/errors.d.ts +39 -1
  30. package/dist/errors.js +50 -0
  31. package/dist/i18n/error-messages.d.ts +86 -0
  32. package/dist/i18n/error-messages.js +192 -0
  33. package/dist/i18n/index.d.ts +1 -0
  34. package/dist/i18n/index.js +1 -0
  35. package/dist/i18n/keys.d.ts +12 -0
  36. package/dist/i18n/keys.js +76 -5
  37. package/dist/index.d.ts +7 -1
  38. package/dist/index.js +6 -0
  39. package/dist/lint.d.ts +10 -0
  40. package/dist/lint.js +412 -49
  41. package/dist/provider.d.ts +5 -5
  42. package/dist/provider.js +2 -2
  43. package/dist/runtime/browser-telemetry-binding.d.ts +5 -0
  44. package/dist/runtime/browser-telemetry-binding.js +83 -0
  45. package/dist/runtime/browser-telemetry.d.ts +76 -0
  46. package/dist/runtime/browser-telemetry.js +165 -0
  47. package/dist/runtime/browser.d.ts +3 -0
  48. package/dist/runtime/browser.js +208 -47
  49. package/dist/runtime/cache-telemetry.d.ts +71 -0
  50. package/dist/runtime/cache-telemetry.js +177 -0
  51. package/dist/runtime/cache.js +67 -7
  52. package/dist/runtime/capability-response.d.ts +3 -0
  53. package/dist/runtime/capability-response.js +56 -0
  54. package/dist/runtime/capability-telemetry.d.ts +46 -0
  55. package/dist/runtime/capability-telemetry.js +68 -0
  56. package/dist/runtime/diagnostic-env.d.ts +3 -0
  57. package/dist/runtime/diagnostic-env.js +10 -1
  58. package/dist/runtime/engine-mode.d.ts +74 -0
  59. package/dist/runtime/engine-mode.js +152 -0
  60. package/dist/runtime/http-telemetry-guard.d.ts +10 -0
  61. package/dist/runtime/http-telemetry-guard.js +62 -10
  62. package/dist/runtime/http.js +141 -35
  63. package/dist/runtime/instrumentation.js +92 -13
  64. package/dist/runtime/ocr-telemetry.d.ts +82 -0
  65. package/dist/runtime/ocr-telemetry.js +192 -0
  66. package/dist/runtime/ocr.js +30 -8
  67. package/dist/runtime/request-telemetry.d.ts +2 -0
  68. package/dist/runtime/request-telemetry.js +2 -1
  69. package/dist/runtime/resolver-runtime-options.d.ts +14 -0
  70. package/dist/runtime/resolver-runtime-options.js +33 -0
  71. package/dist/runtime/resolver-shared.d.ts +3 -2
  72. package/dist/runtime/resolver-shared.js +4 -4
  73. package/dist/runtime/resolver-telemetry.d.ts +1 -0
  74. package/dist/runtime/resolver-telemetry.js +1 -1
  75. package/dist/runtime/resolver-vendors/browser.d.ts +2 -0
  76. package/dist/runtime/resolver-vendors/browser.js +46 -0
  77. package/dist/runtime/resolver-vendors/hosts.d.ts +21 -0
  78. package/dist/runtime/resolver-vendors/hosts.js +35 -0
  79. package/dist/runtime/resolver-vendors/hypersolutions.js +2 -2
  80. package/dist/runtime/resolver-vendors/types.d.ts +5 -4
  81. package/dist/runtime/resolver.d.ts +4 -1
  82. package/dist/runtime/resolver.js +46 -13
  83. package/dist/runtime/state-telemetry.d.ts +79 -0
  84. package/dist/runtime/state-telemetry.js +194 -0
  85. package/dist/runtime/state.d.ts +3 -0
  86. package/dist/runtime/state.js +26 -7
  87. package/dist/runtime/stealth-cookies.d.ts +5 -1
  88. package/dist/runtime/stealth-cookies.js +21 -1
  89. package/dist/runtime/stealth-owned-headers.d.ts +26 -0
  90. package/dist/runtime/stealth-owned-headers.js +37 -0
  91. package/dist/runtime/stealth-telemetry.d.ts +92 -0
  92. package/dist/runtime/stealth-telemetry.js +179 -0
  93. package/dist/runtime/stealth.d.ts +4 -1
  94. package/dist/runtime/stealth.js +228 -85
  95. package/dist/runtime/stt-telemetry.d.ts +77 -0
  96. package/dist/runtime/stt-telemetry.js +176 -0
  97. package/dist/runtime/stt.js +15 -6
  98. package/dist/server/error-stack-frames.d.ts +35 -0
  99. package/dist/server/error-stack-frames.js +219 -0
  100. package/dist/server/index.d.ts +7 -0
  101. package/dist/server/index.js +4 -0
  102. package/dist/server/sdk-version.d.ts +3 -0
  103. package/dist/server/sdk-version.js +19 -0
  104. package/dist/server/self-test.js +1 -18
  105. package/dist/server/sensitive-values.d.ts +1 -1
  106. package/dist/server/sensitive-values.js +9 -0
  107. package/dist/server/serve-implementation.d.ts +51 -2
  108. package/dist/server/serve-implementation.js +337 -90
  109. package/dist/server/types.d.ts +1 -0
  110. package/dist/server/types.js +7 -0
  111. package/dist/stateful/stateful-provider-owner-forwarder.js +22 -0
  112. package/dist/stateful/stateful-provider-runtime-context.d.ts +3 -0
  113. package/dist/stateful/stateful-provider-runtime-context.js +1 -0
  114. package/dist/types.d.ts +94 -1
  115. package/package.json +6 -1
  116. package/src/cli/commands.ts +14 -0
  117. package/src/cli/deployment-intent.ts +364 -0
  118. package/src/cli/migrate-deployment-intent.ts +1908 -0
  119. package/src/cli/migrate-provider-shape.ts +10 -4
  120. package/src/cli/resolver-runtime.ts +72 -0
  121. package/src/define.ts +13 -0
  122. package/src/dev.ts +9 -2
  123. package/src/engine.ts +38 -2
  124. package/src/error-resolution.ts +58 -0
  125. package/src/errors.ts +77 -1
  126. package/src/i18n/error-messages.ts +255 -0
  127. package/src/i18n/index.ts +1 -0
  128. package/src/i18n/keys.ts +80 -6
  129. package/src/index.ts +72 -0
  130. package/src/lint.ts +522 -50
  131. package/src/provider.ts +10 -0
  132. package/src/runtime/browser-telemetry-binding.ts +92 -0
  133. package/src/runtime/browser-telemetry.ts +289 -0
  134. package/src/runtime/browser.ts +249 -65
  135. package/src/runtime/cache-telemetry.ts +247 -0
  136. package/src/runtime/cache.ts +68 -17
  137. package/src/runtime/capability-response.ts +53 -0
  138. package/src/runtime/capability-telemetry.ts +101 -0
  139. package/src/runtime/diagnostic-env.ts +11 -1
  140. package/src/runtime/engine-mode.ts +233 -0
  141. package/src/runtime/http-telemetry-guard.ts +66 -13
  142. package/src/runtime/http.ts +168 -37
  143. package/src/runtime/instrumentation.ts +105 -16
  144. package/src/runtime/ocr-telemetry.ts +308 -0
  145. package/src/runtime/ocr.ts +109 -74
  146. package/src/runtime/request-telemetry.ts +2 -1
  147. package/src/runtime/resolver-runtime-options.ts +50 -0
  148. package/src/runtime/resolver-shared.ts +9 -4
  149. package/src/runtime/resolver-telemetry.ts +1 -1
  150. package/src/runtime/resolver-vendors/browser.ts +65 -7
  151. package/src/runtime/resolver-vendors/hosts.ts +43 -0
  152. package/src/runtime/resolver-vendors/hypersolutions.ts +2 -2
  153. package/src/runtime/resolver-vendors/types.ts +5 -4
  154. package/src/runtime/resolver.ts +64 -21
  155. package/src/runtime/state-telemetry.ts +298 -0
  156. package/src/runtime/state.ts +46 -45
  157. package/src/runtime/stealth-cookies.ts +24 -1
  158. package/src/runtime/stealth-owned-headers.ts +41 -0
  159. package/src/runtime/stealth-telemetry.ts +304 -0
  160. package/src/runtime/stealth.ts +355 -150
  161. package/src/runtime/stt-telemetry.ts +282 -0
  162. package/src/runtime/stt.ts +93 -71
  163. package/src/server/error-stack-frames.ts +214 -0
  164. package/src/server/index.ts +53 -0
  165. package/src/server/sdk-version.ts +22 -0
  166. package/src/server/self-test.ts +1 -21
  167. package/src/server/sensitive-values.ts +14 -0
  168. package/src/server/serve-implementation.ts +569 -108
  169. package/src/server/types.ts +7 -0
  170. package/src/stateful/stateful-provider-owner-forwarder.ts +23 -0
  171. package/src/stateful/stateful-provider-runtime-context.ts +4 -0
  172. package/src/types.ts +99 -1
  173. package/src/cli/__tests__/fixtures/migrate-operation-declaration/approval-override.ts.txt +0 -6
  174. package/src/cli/__tests__/fixtures/migrate-operation-declaration/codemod-syntax.ts.txt +0 -3
  175. package/src/cli/__tests__/fixtures/migrate-operation-declaration/connection-precedence.ts.txt +0 -10
  176. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-a.ts.txt +0 -3
  177. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-b.ts.txt +0 -3
  178. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-barrel.ts.txt +0 -2
  179. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-index.ts.txt +0 -3
  180. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-computed.ts.txt +0 -6
  181. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-a.ts.txt +0 -1
  182. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-b.ts.txt +0 -1
  183. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-index.ts.txt +0 -3
  184. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-barrel.ts.txt +0 -7
  185. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-catalog.ts.txt +0 -19
  186. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-docs.ts.txt +0 -6
  187. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-index.ts.txt +0 -3
  188. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-stores.ts.txt +0 -8
  189. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-indirect-config.ts.txt +0 -5
  190. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-nonstatic.ts.txt +0 -4
  191. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-package-index.ts.txt +0 -3
  192. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-index.ts.txt +0 -3
  193. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-leaf.ts.txt +0 -10
  194. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-one.ts.txt +0 -1
  195. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-two.ts.txt +0 -1
  196. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-composed-index.ts.txt +0 -3
  197. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-composed-one.ts.txt +0 -6
  198. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-composed-two.ts.txt +0 -6
  199. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-mixed-index.ts.txt +0 -14
  200. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-registry.ts.txt +0 -3
  201. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-barrel.ts.txt +0 -2
  202. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-index.ts.txt +0 -3
  203. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-leaf.ts.txt +0 -3
  204. package/src/cli/__tests__/fixtures/migrate-operation-declaration/docs-conflict.ts.txt +0 -8
  205. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-hoisted-array-spread.ts.txt +0 -15
  206. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-imported-array-spread.ts.txt +0 -9
  207. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-kebab-operation-id.ts.txt +0 -17
  208. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-map.ts.txt +0 -5
  209. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-mixed-array-spread.ts.txt +0 -24
  210. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-operation.ts.txt +0 -16
  211. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-snake-operation-id.ts.txt +0 -21
  212. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-duplicate-provider.ts.txt +0 -11
  213. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-id-unresolved.ts.txt +0 -7
  214. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-map.ts.txt +0 -3
  215. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-one-to-many.ts.txt +0 -18
  216. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-one-to-one.ts.txt +0 -13
  217. package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoist-all.ts.txt +0 -31
  218. package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoisted-const.ts.txt +0 -11
  219. package/src/cli/__tests__/fixtures/migrate-operation-declaration/imported-spread.ts.txt +0 -11
  220. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-map.ts.txt +0 -11
  221. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-cast-tail.ts.txt +0 -21
  222. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-ekitan.ts.txt +0 -11
  223. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-override.ts.txt +0 -14
  224. package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-canonical-en.json +0 -1
  225. package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-canonical-ja.json +0 -1
  226. package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-canonical-ko.json +0 -1
  227. package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-existing-operation-namespace-en.json +0 -8
  228. package/src/cli/__tests__/fixtures/migrate-operation-declaration/missing-english-locale.ts.txt +0 -7
  229. package/src/cli/__tests__/fixtures/migrate-operation-declaration/no-safety.ts.txt +0 -6
  230. package/src/cli/__tests__/fixtures/migrate-operation-declaration/non-literal.ts.txt +0 -7
  231. package/src/cli/__tests__/fixtures/migrate-operation-declaration/redundant-approval.ts.txt +0 -6
  232. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-binding-ambiguous.ts.txt +0 -12
  233. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-imported-other.ts.txt +0 -7
  234. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-imported.ts.txt +0 -5
  235. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-key-ambiguous.ts.txt +0 -19
  236. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-shorthand.ts.txt +0 -9
  237. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-typed.ts.txt +0 -11
  238. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-unrelated.ts.txt +0 -13
  239. package/src/cli/__tests__/fixtures/migrate-operation-declaration/safety-conflict.ts.txt +0 -7
  240. package/src/cli/__tests__/fixtures/migrate-operation-declaration/stream.ts.txt +0 -7
  241. package/src/cli/__tests__/fixtures/migrate-operation-declaration/tool-router-spread.ts.txt +0 -15
  242. package/src/cli/__tests__/fixtures/migrate-operation-declaration/unparseable.ts.txt +0 -4
  243. package/src/cli/__tests__/fixtures/migrate-operation-declaration/verbatim-template.ts.txt +0 -12
@@ -0,0 +1,282 @@
1
+ import type { SttContext } from "../types.js";
2
+ import {
3
+ add,
4
+ capabilityErrorText,
5
+ capabilityFacts,
6
+ capabilityMetadata,
7
+ capabilityText,
8
+ inputBytes,
9
+ integer,
10
+ observeCapability,
11
+ registerCapabilityInput,
12
+ } from "./capability-telemetry.js";
13
+ import {
14
+ type ClosedEnum,
15
+ closedEnum,
16
+ type SpanIndex,
17
+ type TelemetryContributor,
18
+ } from "./request-telemetry.js";
19
+ export type SttTelemetryBackend = "cloudflare-workers-ai" | "custom" | "unavailable";
20
+ export type SttTelemetryEngine = "workers-ai" | "custom";
21
+ export type SttTelemetryErrorCode =
22
+ | "STT_UNAVAILABLE"
23
+ | "UNSUPPORTED_STT_BACKEND"
24
+ | "STT_UPSTREAM_FAILED"
25
+ | "STT_AUDIO_TOO_LARGE"
26
+ | "UNSUPPORTED_STT_OPTION"
27
+ | "INVALID_STT_AUDIO"
28
+ | "INVALID_STT_VERIFICATION_CODE_OPTIONS"
29
+ | "NO_CODE_FOUND"
30
+ | "AMBIGUOUS_CODE"
31
+ | "transport_network_error"
32
+ | "transport_timeout"
33
+ | "other";
34
+ export type SttTelemetryModel = "whisper-large-v3-turbo";
35
+ const ERROR_CODES: ReadonlySet<string> = new Set([
36
+ "STT_UNAVAILABLE",
37
+ "UNSUPPORTED_STT_BACKEND",
38
+ "STT_UPSTREAM_FAILED",
39
+ "STT_AUDIO_TOO_LARGE",
40
+ "UNSUPPORTED_STT_OPTION",
41
+ "INVALID_STT_AUDIO",
42
+ "INVALID_STT_VERIFICATION_CODE_OPTIONS",
43
+ "NO_CODE_FOUND",
44
+ "AMBIGUOUS_CODE",
45
+ "transport_network_error",
46
+ "transport_timeout",
47
+ "other",
48
+ ]);
49
+ const MODELS = new Map<string, SttTelemetryModel>([
50
+ ["@cf/openai/whisper-large-v3-turbo", "whisper-large-v3-turbo"],
51
+ ]);
52
+ function errorCode(error: unknown): SttTelemetryErrorCode {
53
+ const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
54
+ return typeof code === "string" && ERROR_CODES.has(code)
55
+ ? (code as SttTelemetryErrorCode)
56
+ : "other";
57
+ }
58
+ export type SttTelemetryEvent = {
59
+ backend: SttTelemetryBackend;
60
+ engine: SttTelemetryEngine;
61
+ model?: string;
62
+ ms: number;
63
+ status?: number;
64
+ audioBytes?: number;
65
+ durationMs?: number;
66
+ usage?: number;
67
+ warnings?: number;
68
+ errorCode?: SttTelemetryErrorCode;
69
+ diagnostics?: string;
70
+ };
71
+ export interface SttTelemetrySink {
72
+ record(event: SttTelemetryEvent): void;
73
+ markTelemetryFailed?(): void;
74
+ }
75
+ export type SttTelemetryLogPayload = {
76
+ telemetryFailed?: true;
77
+ diagnostics?: string[];
78
+ backend: SttTelemetryBackend;
79
+ engine: SttTelemetryEngine;
80
+ model?: string;
81
+ ms: number;
82
+ status?: number;
83
+ audioBytes: number;
84
+ durationMs: number;
85
+ usage: number;
86
+ warnings: number;
87
+ lastErrorCode?: SttTelemetryErrorCode;
88
+
89
+ samples?: Array<Omit<SttTelemetryEvent, "backend" | "engine" | "model" | "diagnostics">>;
90
+ samplesDropped?: number;
91
+ };
92
+ export type SttTelemetryHeaderPayload = {
93
+ backend: ClosedEnum<SttTelemetryBackend>;
94
+ engine: ClosedEnum<SttTelemetryEngine>;
95
+ model?: ClosedEnum<SttTelemetryModel>;
96
+ ms: number;
97
+ status?: number;
98
+ audioBytes: number;
99
+ durationMs: number;
100
+ usage: number;
101
+ warnings: number;
102
+ lastErrorCode?: ClosedEnum<SttTelemetryErrorCode>;
103
+
104
+ samples?: Array<{
105
+ ms: number;
106
+ status?: number;
107
+ audioBytes?: number;
108
+ durationMs?: number;
109
+ usage?: number;
110
+ warnings?: number;
111
+ errorCode?: ClosedEnum<SttTelemetryErrorCode>;
112
+ }>;
113
+ samplesDropped?: number;
114
+ };
115
+ export class SttTelemetryCollector
116
+ implements
117
+ SttTelemetrySink,
118
+ TelemetryContributor<SttTelemetryLogPayload, SttTelemetryHeaderPayload>
119
+ {
120
+ readonly key = "stt" as const;
121
+ readonly #redact?: (text: string) => string;
122
+ #samples: NonNullable<SttTelemetryLogPayload["samples"]> = [];
123
+ #summary: Omit<SttTelemetryLogPayload, "samples" | "samplesDropped"> | undefined;
124
+ #diagnostics: string[] = [];
125
+ #dropped = 0;
126
+ #failed = false;
127
+ constructor(options: { redact?: (text: string) => string } = {}) {
128
+ this.#redact = options.redact;
129
+ }
130
+ markTelemetryFailed(): void {
131
+ this.#failed = true;
132
+ this.#summary ??= {
133
+ backend: "custom",
134
+ engine: "custom",
135
+ ms: 0,
136
+ audioBytes: 0,
137
+ durationMs: 0,
138
+ usage: 0,
139
+ warnings: 0,
140
+ };
141
+ }
142
+ record(event: SttTelemetryEvent): void {
143
+ const sample = {
144
+ ms: integer(event.ms),
145
+ ...(event.status === undefined ? {} : { status: integer(event.status) }),
146
+ ...(event.audioBytes === undefined ? {} : { audioBytes: integer(event.audioBytes) }),
147
+ ...(event.durationMs === undefined ? {} : { durationMs: integer(event.durationMs) }),
148
+ ...(event.usage === undefined ? {} : { usage: integer(event.usage) }),
149
+ ...(event.warnings === undefined ? {} : { warnings: integer(event.warnings) }),
150
+ ...(event.errorCode ? { errorCode: event.errorCode } : {}),
151
+
152
+ ...(event.diagnostics === undefined
153
+ ? {}
154
+ : { diagnostics: capabilityText(event.diagnostics, this.#redact, true) }),
155
+ };
156
+ const prior = this.#summary;
157
+ this.#summary = {
158
+ backend: event.backend,
159
+ engine: event.engine,
160
+ ...(event.model === undefined ? {} : { model: capabilityText(event.model, this.#redact) }),
161
+ ms: add(prior?.ms ?? 0, sample.ms),
162
+ ...(sample.status === undefined ? {} : { status: sample.status }),
163
+ audioBytes: add(prior?.audioBytes ?? 0, sample.audioBytes ?? 0),
164
+ durationMs: add(prior?.durationMs ?? 0, sample.durationMs ?? 0),
165
+ usage: add(prior?.usage ?? 0, sample.usage ?? 0),
166
+ warnings: add(prior?.warnings ?? 0, sample.warnings ?? 0),
167
+ ...(event.errorCode ? { lastErrorCode: event.errorCode } : {}),
168
+ };
169
+ if (this.#samples.length < 24) {
170
+ const { diagnostics, ...closedSample } = sample;
171
+ this.#samples.push(closedSample);
172
+ if (diagnostics !== undefined) this.#diagnostics.push(diagnostics);
173
+ } else this.#dropped = add(this.#dropped, 1);
174
+ }
175
+ toLogPayload(_spans: SpanIndex): SttTelemetryLogPayload | undefined {
176
+ if (!this.#summary) return undefined;
177
+ return {
178
+ ...this.#summary,
179
+ ...(this.#diagnostics.length ? { diagnostics: [...this.#diagnostics] } : {}),
180
+ ...(this.#failed ? { telemetryFailed: true as const } : {}),
181
+ samples: this.#samples.map((s) => ({ ...s })),
182
+ ...(this.#dropped ? { samplesDropped: this.#dropped } : {}),
183
+ };
184
+ }
185
+ toHeaderPayload(log: SttTelemetryLogPayload): SttTelemetryHeaderPayload {
186
+ return {
187
+ backend: closedEnum(log.backend),
188
+ engine: closedEnum(log.engine),
189
+ ...(log.model && MODELS.has(log.model) ? { model: closedEnum(MODELS.get(log.model)!) } : {}),
190
+ ms: log.ms,
191
+ ...(log.status === undefined ? {} : { status: log.status }),
192
+ audioBytes: log.audioBytes,
193
+ durationMs: log.durationMs,
194
+ usage: log.usage,
195
+ warnings: log.warnings,
196
+ ...(log.lastErrorCode ? { lastErrorCode: closedEnum(log.lastErrorCode) } : {}),
197
+
198
+ ...(log.samples
199
+ ? {
200
+ samples: log.samples.map((s) => ({
201
+ ms: s.ms,
202
+ ...(s.status === undefined ? {} : { status: s.status }),
203
+ ...(s.audioBytes === undefined ? {} : { audioBytes: s.audioBytes }),
204
+ ...(s.durationMs === undefined ? {} : { durationMs: s.durationMs }),
205
+ ...(s.usage === undefined ? {} : { usage: s.usage }),
206
+ ...(s.warnings === undefined ? {} : { warnings: s.warnings }),
207
+ ...(s.errorCode ? { errorCode: closedEnum(s.errorCode) } : {}),
208
+ })),
209
+ }
210
+ : {}),
211
+ ...(log.samplesDropped ? { samplesDropped: log.samplesDropped } : {}),
212
+ };
213
+ }
214
+ }
215
+ export function bindSttTelemetry(
216
+ context: SttContext,
217
+ sink: SttTelemetrySink,
218
+ options: { backend?: SttTelemetryBackend; engine?: SttTelemetryEngine; model?: string } = {},
219
+ ): SttContext {
220
+ const metadata = capabilityMetadata(context);
221
+ const backend =
222
+ options.backend ?? (metadata?.backend as SttTelemetryBackend | undefined) ?? "custom";
223
+ const engine = options.engine ?? (metadata?.engine as SttTelemetryEngine | undefined) ?? "custom";
224
+ const model = options.model ?? metadata?.model;
225
+ async function run<
226
+ T extends {
227
+ text: string;
228
+ durationMs?: number;
229
+ usage?: { audioBytes?: number; audioDurationMs?: number };
230
+ warnings?: readonly unknown[];
231
+ },
232
+ >(input: { kind: string; data?: string; url?: string }, operation: () => Promise<T>): Promise<T> {
233
+ const started = performance.now();
234
+ const facts: { status?: number; finishReason?: string; diagnostics?: string } = {};
235
+ observeCapability(sink, () => registerCapabilityInput(input));
236
+ let result: T;
237
+ try {
238
+ result = await capabilityFacts.run(facts, operation);
239
+ } catch (error) {
240
+ observeCapability(sink, () => {
241
+ const status =
242
+ error &&
243
+ typeof error === "object" &&
244
+ "status" in error &&
245
+ typeof error.status === "number"
246
+ ? error.status
247
+ : undefined;
248
+ return sink.record({
249
+ backend,
250
+ engine,
251
+ model,
252
+ ms: performance.now() - started,
253
+ status: facts.status ?? status,
254
+ audioBytes: inputBytes(input),
255
+ errorCode: errorCode(error),
256
+
257
+ diagnostics: `${capabilityErrorText(error)} ${facts.diagnostics ?? ""}`,
258
+ });
259
+ });
260
+ throw error;
261
+ }
262
+ observeCapability(sink, () =>
263
+ sink.record({
264
+ backend,
265
+ engine,
266
+ model: model,
267
+ ms: performance.now() - started,
268
+ status: facts.status,
269
+ audioBytes: result.usage?.audioBytes ?? inputBytes(input),
270
+ durationMs: result.durationMs,
271
+ usage: result.usage?.audioDurationMs,
272
+ warnings: result.warnings?.length ?? 0,
273
+ }),
274
+ );
275
+ return result;
276
+ }
277
+ return {
278
+ ...context,
279
+ transcribe: (request) => run(request.audio, () => context.transcribe(request)),
280
+ extractVerificationCode: (text, opts) => context.extractVerificationCode(text, opts),
281
+ };
282
+ }
@@ -1,4 +1,3 @@
1
- import { readDiagnosticEnv } from "./diagnostic-env.js";
2
1
  import { ProviderError, TransportError, ValidationError } from "../errors.js";
3
2
  import type {
4
3
  Bcp47Locale,
@@ -15,6 +14,13 @@ import type {
15
14
  VerificationCodeCandidateSource,
16
15
  VerificationCodeExtractionResult,
17
16
  } from "../types.js";
17
+ import { readCapabilityErrorBody } from "./capability-response.js";
18
+ import {
19
+ captureCapability,
20
+ registerCapabilityInput,
21
+ tagCapability,
22
+ } from "./capability-telemetry.js";
23
+ import { readDiagnosticEnv, registerDiagnosticValue } from "./diagnostic-env.js";
18
24
  import { createTimeoutController, isTimeoutLikeError } from "./timeout.js";
19
25
 
20
26
  export const APIFUSE__STT__BACKEND_ENV = "APIFUSE__STT__BACKEND";
@@ -50,15 +56,18 @@ function providerError(message: string, options: { code: string; fix?: string })
50
56
  }
51
57
 
52
58
  function createErrorSttClient(options: ErrorSttClientOptions): SttContext {
53
- return {
54
- async transcribe() {
55
- throw providerError(options.message, {
56
- code: options.code,
57
- fix: options.fix,
58
- });
59
+ return tagCapability(
60
+ {
61
+ async transcribe() {
62
+ throw providerError(options.message, {
63
+ code: options.code,
64
+ fix: options.fix,
65
+ });
66
+ },
67
+ extractVerificationCode,
59
68
  },
60
- extractVerificationCode,
61
- };
69
+ { backend: "unavailable", engine: "custom" },
70
+ );
62
71
  }
63
72
 
64
73
  export function createUnsupportedSttClient(reason?: string): SttContext {
@@ -273,72 +282,85 @@ export function createCloudflareWorkersAiSttClient(
273
282
  ): SttContext {
274
283
  const model = options.model ?? DEFAULT_CLOUDFLARE_WORKERS_AI_STT_MODEL;
275
284
  const runFetch = options.fetch ?? fetch;
276
- return {
277
- async transcribe(request) {
278
- const warnings = [];
279
- if (request.initialPrompt && effectivePromptPolicy(request) !== "custom-hint") {
280
- const warning = warnOrThrowUnsupportedOption(
281
- request,
282
- "initialPrompt is honored only when promptPolicy is custom-hint",
283
- );
284
- if (warning) warnings.push(warning);
285
- }
286
- const audioBytes = assertBase64Audio(request.audio, request.maxAudioBytes);
287
- const timeout = createTimeoutController(request.timeoutMs ?? DEFAULT_STT_TIMEOUT_MS);
288
- try {
289
- let response: Response;
290
- try {
291
- response = await runFetch(
292
- `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(options.accountId)}/ai/run/${model}`,
293
- {
294
- method: "POST",
295
- headers: {
296
- Authorization: `Bearer ${options.apiToken}`,
297
- "Content-Type": "application/json",
298
- },
299
- body: JSON.stringify(toCloudflareInput(request)),
300
- signal: timeout.controller.signal,
301
- },
285
+ return tagCapability(
286
+ {
287
+ async transcribe(request) {
288
+ registerCapabilityInput(request.audio);
289
+ registerDiagnosticValue(options.apiToken);
290
+ registerDiagnosticValue(`Bearer ${options.apiToken}`);
291
+ const warnings = [];
292
+ if (request.initialPrompt && effectivePromptPolicy(request) !== "custom-hint") {
293
+ const warning = warnOrThrowUnsupportedOption(
294
+ request,
295
+ "initialPrompt is honored only when promptPolicy is custom-hint",
302
296
  );
303
- } catch (error) {
304
- throw toSttTransportError(error);
297
+ if (warning) warnings.push(warning);
305
298
  }
306
- const payload = await response.json().catch(() => undefined);
307
- if (!response.ok) {
308
- throw new TransportError("STT upstream request failed", {
309
- code: "STT_UPSTREAM_FAILED",
310
- status: response.status,
311
- upstreamStatus: response.status,
312
- });
313
- }
314
- const envelope = unknownRecord(payload);
315
- if (envelope?.success === false) {
316
- throw new TransportError("STT upstream returned an error", {
317
- code: "STT_UPSTREAM_FAILED",
318
- status: 502,
319
- });
320
- }
321
- const transcript = toSttTranscript(payload, audioBytes);
322
- const withWarnings =
323
- warnings.length > 0
324
- ? {
325
- ...transcript,
326
- warnings: [...(transcript.warnings ?? []), ...warnings],
327
- }
328
- : transcript;
329
- if (request.mode === "otp" || request.verificationCode) {
330
- return {
331
- ...withWarnings,
332
- verificationCode: extractVerificationCode(withWarnings.text, request.verificationCode),
333
- };
299
+ const audioBytes = assertBase64Audio(request.audio, request.maxAudioBytes);
300
+ const timeout = createTimeoutController(request.timeoutMs ?? DEFAULT_STT_TIMEOUT_MS);
301
+ try {
302
+ let response: Response;
303
+ try {
304
+ response = await runFetch(
305
+ `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(options.accountId)}/ai/run/${model}`,
306
+ {
307
+ method: "POST",
308
+ headers: {
309
+ Authorization: `Bearer ${options.apiToken}`,
310
+ "Content-Type": "application/json",
311
+ },
312
+ body: JSON.stringify(toCloudflareInput(request)),
313
+ signal: timeout.controller.signal,
314
+ },
315
+ );
316
+ } catch (error) {
317
+ throw toSttTransportError(error);
318
+ }
319
+ captureCapability({ status: response.status });
320
+ if (!response.ok) {
321
+ const echoed = await readCapabilityErrorBody(response, timeout.controller.signal);
322
+ captureCapability({ diagnostics: echoed });
323
+ throw new TransportError("STT upstream request failed", {
324
+ code: "STT_UPSTREAM_FAILED",
325
+ status: response.status,
326
+ upstreamStatus: response.status,
327
+ });
328
+ }
329
+ const payload = await response.json().catch(() => undefined);
330
+ const envelope = unknownRecord(payload);
331
+ if (envelope?.success === false) {
332
+ captureCapability({ diagnostics: JSON.stringify(payload) });
333
+ throw new TransportError("STT upstream returned an error", {
334
+ code: "STT_UPSTREAM_FAILED",
335
+ status: 502,
336
+ });
337
+ }
338
+ const transcript = toSttTranscript(payload, audioBytes);
339
+ const withWarnings =
340
+ warnings.length > 0
341
+ ? {
342
+ ...transcript,
343
+ warnings: [...(transcript.warnings ?? []), ...warnings],
344
+ }
345
+ : transcript;
346
+ if (request.mode === "otp" || request.verificationCode) {
347
+ return {
348
+ ...withWarnings,
349
+ verificationCode: extractVerificationCode(
350
+ withWarnings.text,
351
+ request.verificationCode,
352
+ ),
353
+ };
354
+ }
355
+ return withWarnings;
356
+ } finally {
357
+ timeout.clear();
334
358
  }
335
- return withWarnings;
336
- } finally {
337
- timeout.clear();
338
- }
359
+ },
360
+ extractVerificationCode,
339
361
  },
340
- extractVerificationCode,
341
- };
362
+ { backend: "cloudflare-workers-ai", engine: "workers-ai", model },
363
+ );
342
364
  }
343
365
 
344
366
  const EN_DIGITS: Record<string, string> = {
@@ -0,0 +1,214 @@
1
+ import { type DiagnosticRedactor, redactDiagnosticText } from "../runtime/diagnostic-redactor.js";
2
+
3
+ /**
4
+ * Bounded, whitelist-shaped stack frames for masked (>=500) provider failures.
5
+ *
6
+ * `sanitizeDiagnosticText` is a blacklist scrubber for free text: a V8/Bun
7
+ * frame such as `at inner (/srv/app/probe.ts:3:30)` comes out as
8
+ * `at inner ([REDACTED])`, and even a bare `serve-implementation.ts:2039:12`
9
+ * trips the opaque-token heuristics. Stack frames therefore never go through
10
+ * it. Instead each frame is parsed against a strict grammar and re-emitted
11
+ * from its parts only: an optional `async`/`new` marker, the function name
12
+ * (identifier characters only), the file *basename* (never a filesystem
13
+ * directory, URL, or drive letter) and the numeric line:column. The sole
14
+ * exception is an engine-internal location (`native`, or a `node:` module id
15
+ * of identifier-shaped segments), which is kept verbatim because it names no
16
+ * deployment path. Frames that do not fit the grammar (eval frames, frames
17
+ * without a location, oversized lines) are dropped rather than passed
18
+ * through.
19
+ */
20
+ export const MAX_PROVIDER_ERROR_STACK_FRAMES = 5;
21
+ const MAX_STACK_SCAN_CHARS = 16 * 1024;
22
+ const MAX_STACK_SCAN_LINES = 64;
23
+ const MAX_STACK_LINE_CHARS = 1024;
24
+
25
+ const FRAME_LINE = /^\s+at\s+(\S.*)$/;
26
+ const FRAME_MODIFIER = /^(async|new)\s+(.*)$/;
27
+ const FRAME_FUNCTION_NAME = /^[\w$.<>[\] #]{1,80}$/;
28
+ const FRAME_LOCATION = /^(.*):(\d{1,7}):(\d{1,7})$/;
29
+ const MAX_NODE_MODULE_ID_CHARS = 128;
30
+ // Engine-internal module ids only: `node:` plus `/`-separated identifier-shaped
31
+ // segments (`node:async_hooks`, `node:internal/process/task_queues`). Every
32
+ // segment must start with a letter or `_`, so `..` traversal and dotfile
33
+ // segments cannot ride through this branch.
34
+ const FRAME_NODE_MODULE = /^node:[A-Za-z_][\w.-]{0,40}(?:\/[A-Za-z_][\w.-]{0,40}){0,7}$/;
35
+ const FRAME_BASENAME = /^[\w.$-]{1,120}$/;
36
+ // A basename of dots only (`.`, `..`, `...`) names no file.
37
+ const FRAME_BASENAME_HAS_NAME = /[^.]/;
38
+
39
+ function parseFunctionName(raw: string): { modifier?: string; name?: string } | undefined {
40
+ let rest = raw.trim();
41
+ let modifier: string | undefined;
42
+ const modified = FRAME_MODIFIER.exec(rest);
43
+ if (modified) {
44
+ modifier = modified[1];
45
+ rest = modified[2].trim();
46
+ }
47
+ if (rest.length === 0) return { modifier };
48
+ if (!FRAME_FUNCTION_NAME.test(rest)) return undefined;
49
+ return { modifier, name: rest };
50
+ }
51
+
52
+ function parseLocation(raw: string): string | undefined {
53
+ if (/[\s()]/.test(raw)) return undefined;
54
+ const located = FRAME_LOCATION.exec(raw);
55
+ if (!located) return undefined;
56
+ const [, path, line, column] = located;
57
+ let file: string;
58
+ if (
59
+ path === "native" ||
60
+ (path.length <= MAX_NODE_MODULE_ID_CHARS && FRAME_NODE_MODULE.test(path))
61
+ ) {
62
+ // Not filesystem paths: engine-internal module ids carry no deployment
63
+ // information. This is the one branch that keeps a `/`, and the grammar
64
+ // above bounds it to identifier segments.
65
+ file = path;
66
+ } else {
67
+ const separator = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
68
+ const basename = separator >= 0 ? path.slice(separator + 1) : path;
69
+ if (!FRAME_BASENAME.test(basename)) return undefined;
70
+ // `.` and `..` pass the basename character class but name no file: they
71
+ // are the traversal tokens the grammar exists to exclude, so a path such
72
+ // as `/srv/app/..:1:1` must not be emitted as `..:1:1`.
73
+ if (!FRAME_BASENAME_HAS_NAME.test(basename)) return undefined;
74
+ file = basename;
75
+ }
76
+ return `${file}:${line}:${column}`;
77
+ }
78
+
79
+ /** Parses one `at ...` line into `[async |new ][fn ](basename:line:col)`, or undefined. */
80
+ export function parseProviderErrorStackFrame(line: string): string | undefined {
81
+ if (line.length > MAX_STACK_LINE_CHARS) return undefined;
82
+ const frame = FRAME_LINE.exec(line);
83
+ if (!frame) return undefined;
84
+ const rest = frame[1].trimEnd();
85
+
86
+ let functionPart: string;
87
+ let locationPart: string;
88
+ if (rest.endsWith(")")) {
89
+ const open = rest.lastIndexOf("(");
90
+ if (open < 0) return undefined;
91
+ functionPart = rest.slice(0, open);
92
+ locationPart = rest.slice(open + 1, -1);
93
+ } else {
94
+ // A frame with no parenthesized location still carries its `async`/`new`
95
+ // modifier inline (`at async file:///app/index.mjs:12:3`, emitted by V8
96
+ // for an anonymous async frame such as a module's top-level await).
97
+ // Split it off here: `parseLocation` rejects anything containing
98
+ // whitespace, so leaving it attached dropped the whole frame.
99
+ const modified = FRAME_MODIFIER.exec(rest);
100
+ functionPart = modified ? modified[1] : "";
101
+ locationPart = modified ? modified[2].trim() : rest;
102
+ }
103
+ // Eval frames nest parentheses inside the function part; never pass them through.
104
+ if (/[()]/.test(functionPart)) return undefined;
105
+
106
+ const location = parseLocation(locationPart);
107
+ if (!location) return undefined;
108
+ const fn = parseFunctionName(functionPart);
109
+ if (!fn) return undefined;
110
+
111
+ const prefix = [fn.modifier, fn.name].filter((part) => part !== undefined).join(" ");
112
+ return prefix.length > 0 ? `${prefix} (${location})` : `(${location})`;
113
+ }
114
+
115
+ // V8 (Node >= 21) installs `stack` as a shared native accessor pair on every
116
+ // Error instance; JSC (Bun) installs a data property. Capture the engine's own
117
+ // accessor once so a provider-defined accessor (a different function object) is
118
+ // still rejected while the engine's is trusted. The engine accessor itself can
119
+ // call a process-global `Error.prepareStackTrace` hook, a process-wide trust
120
+ // boundary this module cannot narrow: a provider that installs one already
121
+ // reshapes `stack` for every consumer, and the whitelist grammar below still
122
+ // bounds what the resulting text can carry.
123
+ const NATIVE_STACK_GETTER = Object.getOwnPropertyDescriptor(new Error(), "stack")?.get;
124
+
125
+ function readOwnStackString(error: unknown): string | undefined {
126
+ // Only engine-captured stacks: a thrown plain object is provider-authored text.
127
+ if (!(error instanceof Error)) return undefined;
128
+ try {
129
+ const descriptor = Object.getOwnPropertyDescriptor(error, "stack");
130
+ if (descriptor === undefined) return undefined;
131
+ let stack: unknown;
132
+ if (Object.hasOwn(descriptor, "value")) {
133
+ stack = descriptor.value;
134
+ } else if (
135
+ typeof NATIVE_STACK_GETTER === "function" &&
136
+ descriptor.get === NATIVE_STACK_GETTER
137
+ ) {
138
+ stack = Reflect.apply(NATIVE_STACK_GETTER, error, []);
139
+ } else {
140
+ // A provider-defined accessor must never run inside the logger.
141
+ return undefined;
142
+ }
143
+ return typeof stack === "string" ? stack : undefined;
144
+ } catch {
145
+ return undefined;
146
+ }
147
+ }
148
+
149
+ // Header shapes ahead of the frame block: `Name: message` (V8/JSC),
150
+ // `Name [CODE]: message` (node:assert), `Name` (empty message), or the bare
151
+ // message (Playwright rewrites `stack` as message-first). The message itself
152
+ // is matched verbatim, so anything before it must be a short name-like prefix.
153
+ const HEADER_NAME_PREFIX = /^(?:[\w$][\w$ .[\]-]{0,119}: )?$/;
154
+ const HEADER_NAME_ONLY = /^[\w$][\w$ .[\]-]{0,119}$/;
155
+
156
+ /**
157
+ * The frame block that follows the header, or undefined when the header does
158
+ * not embed the current `message` followed by a newline. Fail closed there: if
159
+ * `message` was mutated after the stack was formatted, the original message
160
+ * lines are still in the string and could be parsed as forged frames. (A
161
+ * mutation that truncates exactly at one of the message's own newlines is
162
+ * indistinguishable from the string alone; the whitelist grammar and the
163
+ * redactor still bound what such a line can carry.)
164
+ */
165
+ function frameBlock(stack: string, error: object): string | undefined {
166
+ let message: unknown;
167
+ try {
168
+ message = (error as { message?: unknown }).message;
169
+ } catch {
170
+ return undefined;
171
+ }
172
+ if (typeof message !== "string") return undefined;
173
+ if (message.length === 0) {
174
+ const newline = stack.indexOf("\n");
175
+ if (newline < 0 || !HEADER_NAME_ONLY.test(stack.slice(0, newline))) return undefined;
176
+ return stack.slice(newline + 1);
177
+ }
178
+ const at = stack.indexOf(`${message}\n`);
179
+ if (at < 0 || !HEADER_NAME_PREFIX.test(stack.slice(0, at))) return undefined;
180
+ return stack.slice(at + message.length + 1);
181
+ }
182
+
183
+ /**
184
+ * Up to {@link MAX_PROVIDER_ERROR_STACK_FRAMES} whitelist-shaped frames from
185
+ * `error.stack`, or undefined when nothing parses. For an engine-formatted
186
+ * stack the header (which embeds the current `message`) is located and
187
+ * stripped first, so a provider-controlled *message* containing newlines
188
+ * cannot inject frame-shaped text (no header match, no frames). A provider
189
+ * that overwrites `stack` or `name` outright can still put grammar-shaped
190
+ * frames in the record: the field is a diagnostic hint, not an attested
191
+ * location, and the whitelist plus the redactor bound what it can carry.
192
+ * Each emitted frame still passes
193
+ * through the request redactor so a registered secret that happens to appear
194
+ * in a function or file name is masked (fail-closed to `[REDACTION_FAILED]`).
195
+ */
196
+ export function providerErrorStackFrames(
197
+ error: unknown,
198
+ redact?: DiagnosticRedactor,
199
+ ): string[] | undefined {
200
+ const stack = readOwnStackString(error);
201
+ if (stack === undefined) return undefined;
202
+
203
+ const body = frameBlock(stack, error as object);
204
+ if (body === undefined) return undefined;
205
+
206
+ const frames: string[] = [];
207
+ const lines = body.slice(0, MAX_STACK_SCAN_CHARS).split("\n", MAX_STACK_SCAN_LINES);
208
+ for (const line of lines) {
209
+ if (frames.length >= MAX_PROVIDER_ERROR_STACK_FRAMES) break;
210
+ const frame = parseProviderErrorStackFrame(line);
211
+ if (frame !== undefined) frames.push(redactDiagnosticText(frame, redact));
212
+ }
213
+ return frames.length > 0 ? frames : undefined;
214
+ }