@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,247 @@
1
+ import { Buffer } from "node:buffer";
2
+ import type { ProviderCacheLookupMeta } from "../types.js";
3
+ import {
4
+ closedEnum,
5
+ type ClosedEnum,
6
+ type SpanIndex,
7
+ type TelemetryContributor,
8
+ } from "./request-telemetry.js";
9
+ import { createTelemetryObserver } from "./http-telemetry-guard.js";
10
+
11
+ export type CacheTelemetrySource = "memory" | "redis" | "loader";
12
+
13
+ export type CacheTelemetrySample = {
14
+ key: string;
15
+ hit: boolean;
16
+ stale: boolean;
17
+ ageMs?: number;
18
+ source: CacheTelemetrySource;
19
+ };
20
+
21
+ export type CacheTelemetryLogPayload = {
22
+ telemetryFailed?: true;
23
+ lookups: number;
24
+ hits: number;
25
+ stale: number;
26
+ misses: number;
27
+ writes: number;
28
+ loaderErrors: number;
29
+ writeErrors: number;
30
+ source: { memory: number; redis: number; loader: number };
31
+ redisFallbacks: number;
32
+ redisMode: ClosedEnum<"not_configured" | "configured" | "degraded">;
33
+ /** Successful Redis commands, including reads, writes, and deletes; memory-only work counts zero. */
34
+ redisRoundTrips: number;
35
+ ageMs?: { p50: number; max: number };
36
+ samples: CacheTelemetrySample[];
37
+ dropped: number;
38
+ };
39
+
40
+ export type CacheTelemetryHeaderPayload = Omit<
41
+ CacheTelemetryLogPayload,
42
+ "samples" | "ageMs" | "telemetryFailed"
43
+ > & {
44
+ ageP50?: number;
45
+ ageMax?: number;
46
+ };
47
+
48
+ export interface CacheTelemetrySink {
49
+ markTelemetryFailed?(): void;
50
+ setRedisConfigured?(configured: boolean): void;
51
+ recordLookup(meta: ProviderCacheLookupMeta): void;
52
+ recordWrite(): void;
53
+ recordLoaderError?(): void;
54
+ recordWriteError?(): void;
55
+ recordRedisFallback(): void;
56
+ recordRedisRoundTrip?(): void;
57
+ }
58
+
59
+ const cacheTelemetryBindings = new WeakMap<object, CacheTelemetrySink>();
60
+
61
+ /** Internal construction binding; keeps sink plumbing off ProviderCacheOptions. */
62
+ export function bindCacheTelemetry<T extends object>(
63
+ options: T,
64
+ sink: CacheTelemetrySink | undefined,
65
+ ): T {
66
+ if (sink) cacheTelemetryBindings.set(options, sink);
67
+ return options;
68
+ }
69
+
70
+ /** Internal lookup used only by the cache factory. */
71
+ export function cacheTelemetryFor(options: object): CacheTelemetrySink | undefined {
72
+ return cacheTelemetryBindings.get(options);
73
+ }
74
+
75
+ export function guardCacheTelemetry(sink: CacheTelemetrySink | undefined): CacheTelemetrySink {
76
+ const observe = createTelemetryObserver(sink);
77
+ return {
78
+ setRedisConfigured: (configured) => observe(() => sink?.setRedisConfigured?.(configured)),
79
+ recordLookup: (meta) => observe(() => sink?.recordLookup(meta)),
80
+ recordWrite: () => observe(() => sink?.recordWrite()),
81
+ recordLoaderError: () => observe(() => sink?.recordLoaderError?.()),
82
+ recordWriteError: () => observe(() => sink?.recordWriteError?.()),
83
+ recordRedisFallback: () => observe(() => sink?.recordRedisFallback()),
84
+ recordRedisRoundTrip: () => observe(() => sink?.recordRedisRoundTrip?.()),
85
+ };
86
+ }
87
+
88
+ const MAX_SAMPLES = 24;
89
+ const MAX_TEXT = 300;
90
+
91
+ function integer(value: number): number {
92
+ return Number.isFinite(value)
93
+ ? Math.min(Number.MAX_SAFE_INTEGER, Math.max(0, Math.floor(value)))
94
+ : Number.MAX_SAFE_INTEGER;
95
+ }
96
+
97
+ function add(a: number, b: number): number {
98
+ return Math.min(Number.MAX_SAFE_INTEGER, integer(a) + integer(b));
99
+ }
100
+
101
+ function boundedText(value: string, redact?: (text: string) => string): string {
102
+ let result: string;
103
+ try {
104
+ result = redact?.(value) ?? value;
105
+ } catch {
106
+ result = "[REDACTION_FAILED]";
107
+ }
108
+ const length = Math.min(result.length, MAX_TEXT);
109
+ const units = Buffer.alloc(length * 2);
110
+ for (let index = 0; index < length; index += 1)
111
+ units.writeUInt16LE(result.charCodeAt(index), index * 2);
112
+ return units.toString("utf16le");
113
+ }
114
+
115
+ export class CacheTelemetryCollector
116
+ implements
117
+ TelemetryContributor<CacheTelemetryLogPayload, CacheTelemetryHeaderPayload>,
118
+ CacheTelemetrySink
119
+ {
120
+ readonly key = "cache" as const;
121
+ readonly #redact?: (text: string) => string;
122
+ #telemetryFailed = false;
123
+ #lookups = 0;
124
+ #hits = 0;
125
+ #stale = 0;
126
+ #misses = 0;
127
+ #writes = 0;
128
+ #loaderErrors = 0;
129
+ #writeErrors = 0;
130
+ #source = { memory: 0, redis: 0, loader: 0 };
131
+ #redisFallbacks = 0;
132
+ #configured: "not_configured" | "configured" | "degraded";
133
+ #redisRoundTrips = 0;
134
+ #ages: number[] = [];
135
+ #maxAge = 0;
136
+ #samples: CacheTelemetrySample[] = [];
137
+ #dropped = 0;
138
+
139
+ constructor(options: { redact?: (text: string) => string } = {}) {
140
+ this.#redact = options.redact;
141
+ this.#configured = "not_configured";
142
+ }
143
+ setRedisConfigured(configured: boolean): void {
144
+ if (configured && this.#configured === "not_configured") this.#configured = "configured";
145
+ }
146
+ markTelemetryFailed(): void {
147
+ this.#telemetryFailed = true;
148
+ }
149
+
150
+ recordLookup(meta: ProviderCacheLookupMeta): void {
151
+ if (!["memory", "redis", "loader"].includes(meta.source)) {
152
+ this.#telemetryFailed = true;
153
+ return;
154
+ }
155
+ this.#lookups = add(this.#lookups, 1);
156
+ if (meta.hit) this.#hits = add(this.#hits, 1);
157
+ else this.#misses = add(this.#misses, 1);
158
+ if (meta.stale) this.#stale = add(this.#stale, 1);
159
+ this.#source[meta.source] = add(this.#source[meta.source], 1);
160
+ if (meta.source === "redis" && this.#configured === "not_configured")
161
+ this.#configured = "configured";
162
+ if (meta.ageMs !== undefined) {
163
+ const age = integer(meta.ageMs);
164
+ if (this.#ages.length < MAX_SAMPLES) this.#ages.push(age);
165
+ this.#maxAge = Math.max(this.#maxAge, age);
166
+ }
167
+ if (this.#samples.length < MAX_SAMPLES) {
168
+ this.#samples.push({
169
+ key: boundedText(meta.key, this.#redact),
170
+ hit: meta.hit,
171
+ stale: meta.stale,
172
+ ...(meta.ageMs === undefined ? {} : { ageMs: integer(meta.ageMs) }),
173
+ source: meta.source,
174
+ });
175
+ } else this.#dropped = add(this.#dropped, 1);
176
+ }
177
+
178
+ recordWrite(): void {
179
+ this.#writes = add(this.#writes, 1);
180
+ }
181
+ recordLoaderError(): void {
182
+ this.#loaderErrors = add(this.#loaderErrors, 1);
183
+ }
184
+ recordWriteError(): void {
185
+ this.#writeErrors = add(this.#writeErrors, 1);
186
+ }
187
+
188
+ recordRedisFallback(): void {
189
+ this.#redisFallbacks = add(this.#redisFallbacks, 1);
190
+ this.#configured = "degraded";
191
+ }
192
+
193
+ recordRedisRoundTrip(): void {
194
+ this.#redisRoundTrips = add(this.#redisRoundTrips, 1);
195
+ }
196
+
197
+ toLogPayload(_spans: SpanIndex): CacheTelemetryLogPayload | undefined {
198
+ if (
199
+ this.#lookups === 0 &&
200
+ this.#writes === 0 &&
201
+ this.#redisFallbacks === 0 &&
202
+ !this.#telemetryFailed
203
+ )
204
+ return undefined;
205
+ const sorted = [...this.#ages].sort((a, b) => a - b);
206
+ const p50 = sorted.length ? sorted[Math.floor((sorted.length - 1) / 2)]! : undefined;
207
+ return {
208
+ ...(this.#telemetryFailed ? { telemetryFailed: true as const } : {}),
209
+ lookups: this.#lookups,
210
+ hits: this.#hits,
211
+ stale: this.#stale,
212
+ misses: this.#misses,
213
+ writes: this.#writes,
214
+ loaderErrors: this.#loaderErrors,
215
+ writeErrors: this.#writeErrors,
216
+ source: { ...this.#source },
217
+ redisFallbacks: this.#redisFallbacks,
218
+ redisMode: closedEnum(this.#configured),
219
+ redisRoundTrips: this.#redisRoundTrips,
220
+ ...(p50 === undefined ? {} : { ageMs: { p50, max: this.#maxAge } }),
221
+ samples: this.#samples.map((sample) => ({ ...sample })),
222
+ dropped: this.#dropped,
223
+ };
224
+ }
225
+
226
+ toHeaderPayload(log: CacheTelemetryLogPayload): CacheTelemetryHeaderPayload {
227
+ return {
228
+ lookups: log.lookups,
229
+ hits: log.hits,
230
+ stale: log.stale,
231
+ misses: log.misses,
232
+ writes: log.writes,
233
+ loaderErrors: log.loaderErrors,
234
+ writeErrors: log.writeErrors,
235
+ source: {
236
+ memory: log.source.memory,
237
+ redis: log.source.redis,
238
+ loader: log.source.loader,
239
+ },
240
+ redisFallbacks: log.redisFallbacks,
241
+ redisMode: log.redisMode,
242
+ redisRoundTrips: log.redisRoundTrips,
243
+ ...(log.ageMs ? { ageP50: log.ageMs.p50, ageMax: log.ageMs.max } : {}),
244
+ dropped: log.dropped,
245
+ };
246
+ }
247
+ }
@@ -11,6 +11,7 @@ import type {
11
11
  ProviderCacheResponseMeta,
12
12
  ProviderCacheResult,
13
13
  } from "../types.js";
14
+ import { cacheTelemetryFor, guardCacheTelemetry } from "./cache-telemetry.js";
14
15
  import {
15
16
  createProviderRedisClient,
16
17
  ensureRedisReady,
@@ -135,15 +136,13 @@ function assertJsonSafeSecretValue(
135
136
  if (typeof value === "number") {
136
137
  if (!Number.isFinite(value))
137
138
  unsupportedSecretValue(reportedPath, "non-finite numbers are unsupported");
138
- if (Object.is(value, -0))
139
- unsupportedSecretValue(reportedPath, "negative zero is unsupported");
139
+ if (Object.is(value, -0)) unsupportedSecretValue(reportedPath, "negative zero is unsupported");
140
140
  return;
141
141
  }
142
142
  if (typeof value !== "object") {
143
143
  unsupportedSecretValue(reportedPath, `${typeof value} values are unsupported`);
144
144
  }
145
- if (ancestors.has(value))
146
- unsupportedSecretValue(reportedPath, "cyclic values are unsupported");
145
+ if (ancestors.has(value)) unsupportedSecretValue(reportedPath, "cyclic values are unsupported");
147
146
 
148
147
  ancestors.add(value);
149
148
  try {
@@ -156,8 +155,7 @@ function assertJsonSafeSecretValue(
156
155
  const key = String(index);
157
156
  expectedNames.add(key);
158
157
  const descriptor = Object.getOwnPropertyDescriptor(value, key);
159
- if (!descriptor)
160
- unsupportedSecretValue(reportedPath, "sparse arrays are unsupported");
158
+ if (!descriptor) unsupportedSecretValue(reportedPath, "sparse arrays are unsupported");
161
159
  if (!descriptor.enumerable || !("value" in descriptor)) {
162
160
  unsupportedSecretValue(reportedPath, "array accessors are unsupported");
163
161
  }
@@ -275,10 +273,7 @@ function hashSecretValue(
275
273
  return { value: `hmac-sha256:${digest}`, secretScoped: true };
276
274
  }
277
275
 
278
- function metadataKeys(
279
- events: ProviderCacheLookupMeta[],
280
- secretScopedKeys: Set<string>,
281
- ): string[] {
276
+ function metadataKeys(events: ProviderCacheLookupMeta[], secretScopedKeys: Set<string>): string[] {
282
277
  let secretScopedIndex = 0;
283
278
  return Array.from(new Set(events.map((event) => event.key))).map((key) => {
284
279
  if (!secretScopedKeys.has(key)) return key;
@@ -369,10 +364,14 @@ export function createProviderCache(options: ProviderCacheOptions): ProviderCach
369
364
  const memoryMaxEntries = Math.max(1, options.memoryMaxEntries ?? DEFAULT_MEMORY_MAX_ENTRIES);
370
365
  const now = options.now ?? Date.now;
371
366
  const events: ProviderCacheLookupMeta[] = [];
367
+ const telemetry = guardCacheTelemetry(cacheTelemetryFor(options));
368
+ telemetry.setRedisConfigured?.(!!redisUrl);
369
+ let redisUnavailable = false;
372
370
  const secretScopedKeys = new Set<string>();
373
371
 
374
372
  function record(meta: ProviderCacheLookupMeta): void {
375
373
  events.push(meta);
374
+ telemetry?.recordLookup(meta);
376
375
  }
377
376
 
378
377
  function sweepMemory(currentTime: number): void {
@@ -416,13 +415,22 @@ export function createProviderCache(options: ProviderCacheOptions): ProviderCach
416
415
  } | null> {
417
416
  const redis = backend.redis;
418
417
  if (!redis || !(await ensureRedisReady(redis, DEFAULT_REDIS_TIMEOUT_MS))) {
418
+ if (redis) {
419
+ redisUnavailable = true;
420
+ telemetry?.recordRedisFallback();
421
+ }
419
422
  return null;
420
423
  }
421
424
 
422
425
  const raw = await withRedisFallback(async () => {
423
426
  return await redis.get(key);
424
427
  });
425
- if (typeof raw !== "string" && raw !== null) return null;
428
+ if (typeof raw !== "string" && raw !== null) {
429
+ redisUnavailable = true;
430
+ telemetry.recordRedisFallback();
431
+ return null;
432
+ }
433
+ telemetry?.recordRedisRoundTrip?.();
426
434
 
427
435
  const envelope = safeParseEnvelope(raw);
428
436
  if (!envelope) return null;
@@ -473,6 +481,7 @@ export function createProviderCache(options: ProviderCacheOptions): ProviderCach
473
481
  value: T,
474
482
  cacheOptions: ProviderCacheGetOrSetOptions,
475
483
  ): Promise<void> {
484
+ telemetry?.recordWrite();
476
485
  const currentTime = now();
477
486
  const freshTtlMs = jitteredTtlMs(cacheOptions.ttlMs, cacheOptions.jitterPct);
478
487
  const staleIfErrorMs = cacheOptions.staleIfErrorMs ?? 0;
@@ -487,9 +496,21 @@ export function createProviderCache(options: ProviderCacheOptions): ProviderCach
487
496
 
488
497
  const redis = backend.redis;
489
498
  if (!redis || !(await ensureRedisReady(redis, DEFAULT_REDIS_TIMEOUT_MS))) {
499
+ if (redis) {
500
+ redisUnavailable = true;
501
+ telemetry?.recordRedisFallback();
502
+ telemetry?.recordWriteError?.();
503
+ }
490
504
  return;
491
505
  }
492
- await withRedisFallback(() => redis.set(key, JSON.stringify(envelope), "PX", staleTtlMs));
506
+ const written = await withRedisFallback(() =>
507
+ redis.set(key, JSON.stringify(envelope), "PX", staleTtlMs),
508
+ );
509
+ if (written === undefined) {
510
+ redisUnavailable = true;
511
+ telemetry?.recordRedisFallback();
512
+ telemetry?.recordWriteError?.();
513
+ } else telemetry?.recordRedisRoundTrip?.();
493
514
  }
494
515
 
495
516
  async function loadAndStore<T>(
@@ -514,6 +535,13 @@ export function createProviderCache(options: ProviderCacheOptions): ProviderCach
514
535
  if (staleCandidate?.meta.stale) {
515
536
  return staleCandidate;
516
537
  }
538
+ telemetry?.recordLookup({
539
+ key,
540
+ hit: false,
541
+ stale: false,
542
+ source: "loader",
543
+ });
544
+ telemetry?.recordLoaderError?.();
517
545
  throw error;
518
546
  }
519
547
  }
@@ -530,18 +558,29 @@ export function createProviderCache(options: ProviderCacheOptions): ProviderCach
530
558
  async get<T = unknown>(key: string): Promise<ProviderCacheResult<T> | null> {
531
559
  const result = await read<T>(key);
532
560
  if (result) record(result.meta);
561
+ else
562
+ telemetry.recordLookup({
563
+ key,
564
+ hit: false,
565
+ stale: false,
566
+ source: backend.redis && !redisUnavailable ? "redis" : "memory",
567
+ });
533
568
  return result;
534
569
  },
535
570
 
536
571
  set: write,
537
572
 
538
573
  async delete(key: string): Promise<void> {
574
+ telemetry?.recordWrite();
539
575
  backend.memory.delete(key);
540
576
  const redis = backend.redis;
541
577
  if (!redis || !(await ensureRedisReady(redis, DEFAULT_REDIS_TIMEOUT_MS))) {
578
+ if (redis) telemetry?.recordRedisFallback();
542
579
  return;
543
580
  }
544
- await withRedisFallback(() => redis.del(key));
581
+ const deleted = await withRedisFallback(() => redis.del(key));
582
+ if (deleted === undefined) telemetry?.recordRedisFallback();
583
+ else telemetry?.recordRedisRoundTrip?.();
545
584
  },
546
585
 
547
586
  async getOrSet<T = unknown>(
@@ -557,10 +596,22 @@ export function createProviderCache(options: ProviderCacheOptions): ProviderCach
557
596
 
558
597
  const existingInflight = backend.inflight.get(key);
559
598
  if (existingInflight) {
560
- const inflightResult = await existingInflight;
561
- const result = resultWithValue<T>(inflightResult.value, inflightResult.meta);
562
- record(result.meta);
563
- return result;
599
+ try {
600
+ const inflightResult = await existingInflight;
601
+ const result = resultWithValue<T>(inflightResult.value, inflightResult.meta);
602
+ record(result.meta);
603
+ return result;
604
+ } catch (error) {
605
+ const meta: ProviderCacheLookupMeta = {
606
+ key,
607
+ hit: false,
608
+ stale: false,
609
+ source: "loader",
610
+ };
611
+ telemetry.recordLookup(meta);
612
+ telemetry?.recordLoaderError?.();
613
+ throw error;
614
+ }
564
615
  }
565
616
 
566
617
  const promise: Promise<ProviderCacheResult<unknown>> = loadAndStore(
@@ -0,0 +1,53 @@
1
+ export const CAPABILITY_BODY_UNAVAILABLE = "<body unavailable>";
2
+
3
+ /** Read optional diagnostic response text without letting a body stall the request failure. */
4
+ export async function readCapabilityErrorBody(
5
+ response: Response,
6
+ signal: AbortSignal,
7
+ ): Promise<string> {
8
+ if (signal.aborted) return CAPABILITY_BODY_UNAVAILABLE;
9
+
10
+ let timer: ReturnType<typeof setTimeout> | undefined;
11
+ let onAbort: (() => void) | undefined;
12
+ let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
13
+ let unavailableWon = false;
14
+ const cancelBody = (reason: unknown) => {
15
+ const cancellation = reader ? reader.cancel(reason) : response.body?.cancel(reason);
16
+ if (cancellation) void cancellation.catch(() => {});
17
+ };
18
+ const unavailable = new Promise<string>((resolve) => {
19
+ onAbort = () => {
20
+ unavailableWon = true;
21
+ resolve(CAPABILITY_BODY_UNAVAILABLE);
22
+ cancelBody(signal.reason);
23
+ };
24
+ signal.addEventListener("abort", onAbort, { once: true });
25
+ timer = setTimeout(() => {
26
+ unavailableWon = true;
27
+ resolve(CAPABILITY_BODY_UNAVAILABLE);
28
+ cancelBody(new Error("capability error body read timeout"));
29
+ }, 250);
30
+ });
31
+ const body = Promise.resolve()
32
+ .then(async () => {
33
+ if (unavailableWon) return CAPABILITY_BODY_UNAVAILABLE;
34
+ if (!response.body) return response.text();
35
+ reader = response.body.getReader();
36
+ const decoder = new TextDecoder();
37
+ let text = "";
38
+ while (true) {
39
+ if (unavailableWon) return CAPABILITY_BODY_UNAVAILABLE;
40
+ const chunk = await reader.read();
41
+ if (chunk.done) return text + decoder.decode();
42
+ text += decoder.decode(chunk.value, { stream: true });
43
+ }
44
+ })
45
+ .catch(() => CAPABILITY_BODY_UNAVAILABLE);
46
+
47
+ try {
48
+ return await Promise.race([body, unavailable]);
49
+ } finally {
50
+ if (timer !== undefined) clearTimeout(timer);
51
+ if (onAbort) signal.removeEventListener("abort", onAbort);
52
+ }
53
+ }
@@ -0,0 +1,101 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { Buffer } from "node:buffer";
3
+ import { registerDiagnosticValue } from "./diagnostic-env.js";
4
+ import { redactDiagnosticText } from "./diagnostic-redactor.js";
5
+ import { observeTelemetry } from "./http-telemetry-guard.js";
6
+ import { boundedText } from "./resolver-telemetry.js";
7
+
8
+ /** Internal capture facts, isolated across concurrent calls and never attached to caller results. */
9
+ export type CapabilityFacts = { status?: number; finishReason?: string; diagnostics?: string };
10
+ export const capabilityFacts = new AsyncLocalStorage<CapabilityFacts>();
11
+ const metadata = new WeakMap<object, { backend: string; engine: string; model?: string }>();
12
+ export function tagCapability<T extends object>(
13
+ context: T,
14
+ facts: { backend: string; engine: string; model?: string },
15
+ ): T {
16
+ metadata.set(context, facts);
17
+ return context;
18
+ }
19
+ export function capabilityMetadata(context: object) {
20
+ return metadata.get(context);
21
+ }
22
+ export function captureCapability(facts: CapabilityFacts): void {
23
+ const current = capabilityFacts.getStore();
24
+ if (current) Object.assign(current, facts);
25
+ }
26
+ export function registerCapabilityInput(input: {
27
+ kind: string;
28
+ data?: string;
29
+ url?: string;
30
+ }): void {
31
+ // Payloads are measurements, never redaction needles. Only bounded URLs belong
32
+ // in the request inventory; structural stripping covers oversized URL echoes.
33
+ if (input.kind === "url" && input.url && Buffer.byteLength(input.url) <= 2048)
34
+ registerDiagnosticValue(input.url.trim());
35
+ }
36
+ /**
37
+ * Formats capability diagnostics after credential redaction.
38
+ * When `stripPayload` is enabled, long base64-shaped runs are replaced as a
39
+ * defence in depth; this may replace an unregistered long identifier, while
40
+ * protecting against the more likely case of payload data in an upstream body.
41
+ */
42
+ export function capabilityText(
43
+ value: string,
44
+ redact?: (text: string) => string,
45
+ stripPayload = false,
46
+ ): string {
47
+ const stripped = value
48
+ .replace(
49
+ /(?:authorization|proxy-authorization|cookie|set-cookie)\s*["']?\s*[:=]\s*[^\r\n]+/gi,
50
+ "[REDACTED]",
51
+ )
52
+ .replace(/Bearer\s+[^\r\n]+/gi, "[REDACTED]")
53
+ .replace(/https?:\/\/[^\s"'<>]+/gi, "[REDACTED]")
54
+ .replace(/data:[^\s"']+/gi, "[BASE64_STRIPPED]");
55
+ return boundedText(
56
+ stripPayload ? stripped.replace(/[A-Za-z0-9+/=]{64,}/g, "[BASE64_STRIPPED]") : stripped,
57
+ (text) => redactDiagnosticText(text, redact),
58
+ );
59
+ }
60
+ export function capabilityErrorText(error: unknown): string {
61
+ if (!(error instanceof Error)) return String(error);
62
+ const details = "details" in error ? error.details : undefined;
63
+ const body =
64
+ details && typeof details === "object" && "body" in details ? details.body : undefined;
65
+ return `${error.message}${typeof body === "string" ? ` ${body}` : ""}`;
66
+ }
67
+ export const integer = (n: number | undefined) =>
68
+ Math.min(
69
+ Number.MAX_SAFE_INTEGER,
70
+ Math.max(0, Math.floor(Number.isFinite(n ?? 0) ? (n ?? 0) : 0)),
71
+ );
72
+ export const add = (a: number, b: number) =>
73
+ Math.min(Number.MAX_SAFE_INTEGER, integer(a) + integer(b));
74
+ export function inputBytes(input: { kind: string; data?: string }): number | undefined {
75
+ if (input.kind !== "base64" || input.data === undefined) return undefined;
76
+ return Buffer.byteLength(input.data.trim(), "base64");
77
+ }
78
+ /** All capability hooks use the HTTP observer guard, including failure reporting. */
79
+ export function observeCapability(
80
+ sink: { markTelemetryFailed?(): void },
81
+ callback: () => unknown,
82
+ ): void {
83
+ observeTelemetry(callback, () => {
84
+ observeTelemetry(
85
+ () => sink.markTelemetryFailed?.(),
86
+ () => {},
87
+ );
88
+ });
89
+ }
90
+
91
+ // Only server-enrolled capability spans use the request root; standalone instrumentation
92
+ // continues to use its caller's active span. No existing namespace changes parentage.
93
+ type RootRunner = <T>(callback: () => Promise<T>) => Promise<T>;
94
+ const roots = new WeakMap<object, RootRunner>();
95
+ export function bindCapabilityRoot<T extends object>(context: T, run: RootRunner): T {
96
+ roots.set(context, run);
97
+ return context;
98
+ }
99
+ export function runCapabilitySpan<T>(context: object, callback: () => Promise<T>): Promise<T> {
100
+ return roots.get(context)?.(callback) ?? callback();
101
+ }
@@ -1,9 +1,9 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
 
3
3
  import {
4
+ otlpHeaderCredentialValues,
4
5
  resolvedStringVariants,
5
6
  urlCredentialComponents,
6
- otlpHeaderCredentialValues,
7
7
  } from "./diagnostic-env-values.js";
8
8
  import {
9
9
  type CompiledDiagnosticSensitiveValues,
@@ -17,6 +17,7 @@ export interface DiagnosticEnvScope {
17
17
  observe: EnvObserver;
18
18
  finished: boolean;
19
19
  staticValues?: CompiledDiagnosticSensitiveValues;
20
+ register?: (values: readonly string[]) => void;
20
21
  }
21
22
  const observers = new AsyncLocalStorage<DiagnosticEnvScope>();
22
23
 
@@ -48,6 +49,15 @@ export function readDiagnosticEnv(
48
49
  return value;
49
50
  }
50
51
 
52
+ /** Register a runtime-discovered sensitive value in the active request inventory. */
53
+ export function registerDiagnosticValue(value: string): void {
54
+ if (value.length < 4) return;
55
+ const scope = observers.getStore();
56
+ if (scope && !scope.finished && scope.register) {
57
+ scope.register([value]);
58
+ }
59
+ }
60
+
51
61
  export function withDiagnosticEnv<T>(observer: EnvObserver | DiagnosticEnvScope, fn: () => T): T {
52
62
  const scope = typeof observer === "function" ? { observe: observer, finished: false } : observer;
53
63
  const run = () => observers.run(scope, fn);