cursedbelt-core 1.0.0

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 (307) hide show
  1. package/LICENSE +15 -0
  2. package/dist/client/wsWire.d.ts +49 -0
  3. package/dist/client/wsWire.js +57 -0
  4. package/dist/config/appConfig.d.ts +361 -0
  5. package/dist/config/appConfig.js +308 -0
  6. package/dist/core/activity/index.d.ts +8 -0
  7. package/dist/core/activity/index.js +7 -0
  8. package/dist/core/activity/model.d.ts +115 -0
  9. package/dist/core/activity/model.js +63 -0
  10. package/dist/core/analytics/index.d.ts +18 -0
  11. package/dist/core/analytics/index.js +18 -0
  12. package/dist/core/analytics/range.d.ts +186 -0
  13. package/dist/core/analytics/range.js +271 -0
  14. package/dist/core/analytics/schedule.d.ts +301 -0
  15. package/dist/core/analytics/schedule.js +372 -0
  16. package/dist/core/category/index.d.ts +3 -0
  17. package/dist/core/category/index.js +6 -0
  18. package/dist/core/category/memoryStore.d.ts +21 -0
  19. package/dist/core/category/memoryStore.js +97 -0
  20. package/dist/core/category/tree.d.ts +26 -0
  21. package/dist/core/category/tree.js +113 -0
  22. package/dist/core/category/types.d.ts +48 -0
  23. package/dist/core/category/types.js +8 -0
  24. package/dist/core/color.d.ts +14 -0
  25. package/dist/core/color.js +32 -0
  26. package/dist/core/colorScheme/colorSchemeStore.d.ts +70 -0
  27. package/dist/core/colorScheme/colorSchemeStore.js +123 -0
  28. package/dist/core/colorScheme/index.d.ts +7 -0
  29. package/dist/core/colorScheme/index.js +7 -0
  30. package/dist/core/domainOwners.d.ts +19 -0
  31. package/dist/core/domainOwners.js +15 -0
  32. package/dist/core/download.d.ts +14 -0
  33. package/dist/core/download.js +37 -0
  34. package/dist/core/events/ccEventBus.d.ts +44 -0
  35. package/dist/core/events/ccEventBus.js +70 -0
  36. package/dist/core/file-tree/fileTreeModel.d.ts +276 -0
  37. package/dist/core/file-tree/fileTreeModel.js +312 -0
  38. package/dist/core/folder-tree/actions.d.ts +84 -0
  39. package/dist/core/folder-tree/actions.js +119 -0
  40. package/dist/core/folder-tree/index.d.ts +3 -0
  41. package/dist/core/folder-tree/index.js +20 -0
  42. package/dist/core/folder-tree/path.d.ts +76 -0
  43. package/dist/core/folder-tree/path.js +146 -0
  44. package/dist/core/folder-tree/tree.d.ts +86 -0
  45. package/dist/core/folder-tree/tree.js +184 -0
  46. package/dist/core/layout/index.d.ts +1 -0
  47. package/dist/core/layout/index.js +7 -0
  48. package/dist/core/master-lock/index.d.ts +14 -0
  49. package/dist/core/master-lock/index.js +14 -0
  50. package/dist/core/master-lock/kdf.d.ts +63 -0
  51. package/dist/core/master-lock/kdf.js +117 -0
  52. package/dist/core/master-lock/policy.d.ts +32 -0
  53. package/dist/core/master-lock/policy.js +52 -0
  54. package/dist/core/master-lock/presence.d.ts +52 -0
  55. package/dist/core/master-lock/presence.js +61 -0
  56. package/dist/core/master-lock/wire.d.ts +139 -0
  57. package/dist/core/master-lock/wire.js +81 -0
  58. package/dist/core/media/bgRemoval.d.ts +27 -0
  59. package/dist/core/media/bgRemoval.js +101 -0
  60. package/dist/core/media/ffmpeg.d.ts +57 -0
  61. package/dist/core/media/ffmpeg.js +126 -0
  62. package/dist/core/media/format.d.ts +20 -0
  63. package/dist/core/media/format.js +54 -0
  64. package/dist/core/media/index.d.ts +12 -0
  65. package/dist/core/media/index.js +18 -0
  66. package/dist/core/media/mediaItem.d.ts +15 -0
  67. package/dist/core/media/mediaItem.js +34 -0
  68. package/dist/core/media/renderPlan.d.ts +63 -0
  69. package/dist/core/media/renderPlan.js +232 -0
  70. package/dist/core/media/segment.d.ts +79 -0
  71. package/dist/core/media/segment.js +247 -0
  72. package/dist/core/media/size.d.ts +1 -0
  73. package/dist/core/media/size.js +3 -0
  74. package/dist/core/media/trim.d.ts +21 -0
  75. package/dist/core/media/trim.js +40 -0
  76. package/dist/core/media/types.d.ts +148 -0
  77. package/dist/core/media/types.js +11 -0
  78. package/dist/core/media/uploadLimits.d.ts +7 -0
  79. package/dist/core/media/uploadLimits.js +16 -0
  80. package/dist/core/media/validate.d.ts +9 -0
  81. package/dist/core/media/validate.js +121 -0
  82. package/dist/core/media/videoCuts.d.ts +152 -0
  83. package/dist/core/media/videoCuts.js +284 -0
  84. package/dist/core/navigation/breadcrumbCompat.d.ts +24 -0
  85. package/dist/core/navigation/breadcrumbCompat.js +21 -0
  86. package/dist/core/navigation/coverageTest.d.ts +24 -0
  87. package/dist/core/navigation/coverageTest.js +48 -0
  88. package/dist/core/navigation/index.d.ts +6 -0
  89. package/dist/core/navigation/index.js +10 -0
  90. package/dist/core/navigation/migrateNavPreferences.d.ts +55 -0
  91. package/dist/core/navigation/migrateNavPreferences.js +63 -0
  92. package/dist/core/navigation/navigationModel.d.ts +165 -0
  93. package/dist/core/navigation/navigationModel.js +14 -0
  94. package/dist/core/navigation/navigationSelectors.d.ts +63 -0
  95. package/dist/core/navigation/navigationSelectors.js +167 -0
  96. package/dist/core/navigation/searchCatalogue.d.ts +23 -0
  97. package/dist/core/navigation/searchCatalogue.js +76 -0
  98. package/dist/core/net/reconnectSchedule.d.ts +46 -0
  99. package/dist/core/net/reconnectSchedule.js +38 -0
  100. package/dist/core/palette/ShortcutRegistry.d.ts +33 -0
  101. package/dist/core/palette/ShortcutRegistry.js +52 -0
  102. package/dist/core/palette/fuzzyRank.d.ts +18 -0
  103. package/dist/core/palette/fuzzyRank.js +73 -0
  104. package/dist/core/palette/hotkeys.d.ts +28 -0
  105. package/dist/core/palette/hotkeys.js +118 -0
  106. package/dist/core/palette/index.d.ts +3 -0
  107. package/dist/core/palette/index.js +8 -0
  108. package/dist/core/schema/projection.d.ts +84 -0
  109. package/dist/core/schema/projection.js +184 -0
  110. package/dist/core/session/claims.d.ts +145 -0
  111. package/dist/core/session/claims.js +157 -0
  112. package/dist/core/sharing/index.d.ts +5 -0
  113. package/dist/core/sharing/index.js +4 -0
  114. package/dist/core/sharing/model.d.ts +195 -0
  115. package/dist/core/sharing/model.js +150 -0
  116. package/dist/core/site/siteCopy.d.ts +92 -0
  117. package/dist/core/site/siteCopy.js +91 -0
  118. package/dist/core/slots.d.ts +22 -0
  119. package/dist/core/slots.js +31 -0
  120. package/dist/core/styles/constructableSheet.d.ts +51 -0
  121. package/dist/core/styles/constructableSheet.js +80 -0
  122. package/dist/core/tokens/registry.d.ts +119 -0
  123. package/dist/core/tokens/registry.js +447 -0
  124. package/dist/core/uiScale/index.d.ts +8 -0
  125. package/dist/core/uiScale/index.js +8 -0
  126. package/dist/core/uiScale/scale.d.ts +36 -0
  127. package/dist/core/uiScale/scale.js +47 -0
  128. package/dist/core/uiScale/uiScaleStore.d.ts +47 -0
  129. package/dist/core/uiScale/uiScaleStore.js +63 -0
  130. package/dist/core/wire/__fixtures__/rowsMarshalWorker.d.ts +1 -0
  131. package/dist/core/wire/__fixtures__/rowsMarshalWorker.js +23 -0
  132. package/dist/core/wire/index.d.ts +17 -0
  133. package/dist/core/wire/index.js +17 -0
  134. package/dist/core/wire/ingress.d.ts +44 -0
  135. package/dist/core/wire/ingress.js +134 -0
  136. package/dist/core/wire/marshalWorker.d.ts +70 -0
  137. package/dist/core/wire/marshalWorker.js +273 -0
  138. package/dist/core/wire/marshalling.d.ts +122 -0
  139. package/dist/core/wire/marshalling.js +126 -0
  140. package/dist/core/wire/wireHub.d.ts +80 -0
  141. package/dist/core/wire/wireHub.js +116 -0
  142. package/dist/ctgr/base64url.d.ts +8 -0
  143. package/dist/ctgr/base64url.js +39 -0
  144. package/dist/ctgr/chunkAccumulator.d.ts +214 -0
  145. package/dist/ctgr/chunkAccumulator.js +307 -0
  146. package/dist/ctgr/chunkStore.d.ts +24 -0
  147. package/dist/ctgr/chunkStore.js +125 -0
  148. package/dist/ctgr/codec.d.ts +44 -0
  149. package/dist/ctgr/codec.js +153 -0
  150. package/dist/ctgr/compress.d.ts +2 -0
  151. package/dist/ctgr/compress.js +20 -0
  152. package/dist/ctgr/crc32.d.ts +2 -0
  153. package/dist/ctgr/crc32.js +23 -0
  154. package/dist/ctgr/digest.d.ts +1 -0
  155. package/dist/ctgr/digest.js +15 -0
  156. package/dist/ctgr/index.d.ts +9 -0
  157. package/dist/ctgr/index.js +17 -0
  158. package/dist/ctgr/types.d.ts +64 -0
  159. package/dist/ctgr/types.js +21 -0
  160. package/dist/ctgr/v0compat.d.ts +27 -0
  161. package/dist/ctgr/v0compat.js +128 -0
  162. package/dist/index.d.ts +15 -0
  163. package/dist/index.js +17 -0
  164. package/dist/shared/errors.d.ts +101 -0
  165. package/dist/shared/errors.js +99 -0
  166. package/dist/testing/bytes.d.ts +32 -0
  167. package/dist/testing/bytes.js +33 -0
  168. package/dist/testing/domInspect.d.ts +59 -0
  169. package/dist/testing/domInspect.js +115 -0
  170. package/dist/testing/fakeFetch.d.ts +23 -0
  171. package/dist/testing/fakeFetch.js +21 -0
  172. package/dist/testing/index.d.ts +3 -0
  173. package/dist/testing/index.js +11 -0
  174. package/package.json +373 -0
  175. package/src/client/wsWire.ts +83 -0
  176. package/src/config/appConfig.spec.ts +158 -0
  177. package/src/config/appConfig.ts +372 -0
  178. package/src/core/activity/index.ts +21 -0
  179. package/src/core/activity/model.ts +160 -0
  180. package/src/core/analytics/index.ts +18 -0
  181. package/src/core/analytics/range.spec.ts +280 -0
  182. package/src/core/analytics/range.ts +383 -0
  183. package/src/core/analytics/schedule.spec.ts +311 -0
  184. package/src/core/analytics/schedule.ts +624 -0
  185. package/src/core/category/category.spec.ts +144 -0
  186. package/src/core/category/index.ts +19 -0
  187. package/src/core/category/memoryStore.ts +104 -0
  188. package/src/core/category/tree.ts +118 -0
  189. package/src/core/category/types.ts +59 -0
  190. package/src/core/color.spec.ts +35 -0
  191. package/src/core/color.ts +35 -0
  192. package/src/core/colorScheme/colorScheme.spec.ts +127 -0
  193. package/src/core/colorScheme/colorSchemeStore.ts +184 -0
  194. package/src/core/colorScheme/index.ts +23 -0
  195. package/src/core/domainOwners.ts +32 -0
  196. package/src/core/download.spec.ts +38 -0
  197. package/src/core/download.ts +35 -0
  198. package/src/core/events/ccEventBus.spec.ts +90 -0
  199. package/src/core/events/ccEventBus.ts +120 -0
  200. package/src/core/file-tree/fileTreeModel.spec.ts +462 -0
  201. package/src/core/file-tree/fileTreeModel.ts +511 -0
  202. package/src/core/folder-tree/actions.spec.ts +103 -0
  203. package/src/core/folder-tree/actions.ts +153 -0
  204. package/src/core/folder-tree/index.ts +46 -0
  205. package/src/core/folder-tree/path.spec.ts +113 -0
  206. package/src/core/folder-tree/path.ts +148 -0
  207. package/src/core/folder-tree/tree.spec.ts +157 -0
  208. package/src/core/folder-tree/tree.ts +244 -0
  209. package/src/core/layout/index.ts +7 -0
  210. package/src/core/master-lock/index.ts +50 -0
  211. package/src/core/master-lock/kdf.spec.ts +95 -0
  212. package/src/core/master-lock/kdf.ts +145 -0
  213. package/src/core/master-lock/policy.ts +55 -0
  214. package/src/core/master-lock/presence.spec.ts +73 -0
  215. package/src/core/master-lock/presence.ts +71 -0
  216. package/src/core/master-lock/wire.ts +155 -0
  217. package/src/core/media/bgRemoval.spec.ts +120 -0
  218. package/src/core/media/bgRemoval.ts +123 -0
  219. package/src/core/media/ffmpeg.spec.ts +135 -0
  220. package/src/core/media/ffmpeg.ts +162 -0
  221. package/src/core/media/format.spec.ts +41 -0
  222. package/src/core/media/format.ts +58 -0
  223. package/src/core/media/index.ts +105 -0
  224. package/src/core/media/mediaItem.spec.ts +46 -0
  225. package/src/core/media/mediaItem.ts +40 -0
  226. package/src/core/media/renderPlan.spec.ts +140 -0
  227. package/src/core/media/renderPlan.ts +318 -0
  228. package/src/core/media/segment.spec.ts +159 -0
  229. package/src/core/media/segment.ts +352 -0
  230. package/src/core/media/size.spec.ts +19 -0
  231. package/src/core/media/size.ts +3 -0
  232. package/src/core/media/trim.spec.ts +75 -0
  233. package/src/core/media/trim.ts +60 -0
  234. package/src/core/media/types.ts +147 -0
  235. package/src/core/media/uploadLimits.ts +18 -0
  236. package/src/core/media/validate.spec.ts +106 -0
  237. package/src/core/media/validate.ts +131 -0
  238. package/src/core/media/videoCuts.spec.ts +327 -0
  239. package/src/core/media/videoCuts.ts +355 -0
  240. package/src/core/navigation/breadcrumbCompat.ts +44 -0
  241. package/src/core/navigation/coverageTest.ts +54 -0
  242. package/src/core/navigation/index.ts +50 -0
  243. package/src/core/navigation/migrateNavPreferences.ts +119 -0
  244. package/src/core/navigation/navigation.spec.ts +388 -0
  245. package/src/core/navigation/navigationModel.ts +185 -0
  246. package/src/core/navigation/navigationSelectors.ts +206 -0
  247. package/src/core/navigation/searchCatalogue.ts +89 -0
  248. package/src/core/net/reconnectSchedule.spec.ts +49 -0
  249. package/src/core/net/reconnectSchedule.ts +78 -0
  250. package/src/core/palette/ShortcutRegistry.ts +69 -0
  251. package/src/core/palette/fuzzyRank.ts +78 -0
  252. package/src/core/palette/hotkeys.ts +132 -0
  253. package/src/core/palette/index.ts +22 -0
  254. package/src/core/palette/palette.spec.ts +160 -0
  255. package/src/core/schema/projection.spec.ts +122 -0
  256. package/src/core/schema/projection.ts +225 -0
  257. package/src/core/session/claims.ts +181 -0
  258. package/src/core/sharing/index.ts +33 -0
  259. package/src/core/sharing/model.spec.ts +104 -0
  260. package/src/core/sharing/model.ts +296 -0
  261. package/src/core/site/siteCopy.ts +115 -0
  262. package/src/core/slots.ts +38 -0
  263. package/src/core/styles/constructableSheet.ts +86 -0
  264. package/src/core/tokens/registry.spec.ts +160 -0
  265. package/src/core/tokens/registry.ts +536 -0
  266. package/src/core/uiScale/index.ts +32 -0
  267. package/src/core/uiScale/scale.ts +63 -0
  268. package/src/core/uiScale/uiScale.spec.ts +77 -0
  269. package/src/core/uiScale/uiScaleStore.ts +100 -0
  270. package/src/core/wire/__fixtures__/rowsMarshalWorker.ts +24 -0
  271. package/src/core/wire/index.ts +56 -0
  272. package/src/core/wire/ingress.spec.ts +276 -0
  273. package/src/core/wire/ingress.ts +182 -0
  274. package/src/core/wire/marshalWorker.spec.ts +196 -0
  275. package/src/core/wire/marshalWorker.ts +361 -0
  276. package/src/core/wire/marshalling.spec.ts +139 -0
  277. package/src/core/wire/marshalling.ts +238 -0
  278. package/src/core/wire/wireHub.spec.ts +137 -0
  279. package/src/core/wire/wireHub.ts +213 -0
  280. package/src/ctgr/base64url.ts +44 -0
  281. package/src/ctgr/chunkAccumulator.ts +403 -0
  282. package/src/ctgr/chunkStore.spec.ts +101 -0
  283. package/src/ctgr/chunkStore.ts +168 -0
  284. package/src/ctgr/codec.spec.ts +208 -0
  285. package/src/ctgr/codec.ts +202 -0
  286. package/src/ctgr/compress.ts +21 -0
  287. package/src/ctgr/crc32.ts +23 -0
  288. package/src/ctgr/digest.ts +14 -0
  289. package/src/ctgr/index.ts +39 -0
  290. package/src/ctgr/types.ts +80 -0
  291. package/src/ctgr/v0compat.spec.ts +73 -0
  292. package/src/ctgr/v0compat.ts +156 -0
  293. package/src/declaredDepsAreImported.spec.ts +87 -0
  294. package/src/index.ts +25 -0
  295. package/src/leafSubpathsImportNothing.spec.ts +287 -0
  296. package/src/lockfileIsTracked.spec.ts +50 -0
  297. package/src/namedSubpathsResolve.spec.ts +122 -0
  298. package/src/noPathDeps.spec.ts +24 -0
  299. package/src/publishShape.spec.ts +89 -0
  300. package/src/shared/errors.ts +153 -0
  301. package/src/skippedTestsAreAnswered.spec.ts +221 -0
  302. package/src/testTmpRoot.spec.ts +102 -0
  303. package/src/testing/bytes.ts +38 -0
  304. package/src/testing/domInspect.spec.ts +79 -0
  305. package/src/testing/domInspect.ts +127 -0
  306. package/src/testing/fakeFetch.ts +26 -0
  307. package/src/testing/index.ts +11 -0
@@ -0,0 +1,214 @@
1
+ /**
2
+ * # ChunkAccumulator — The Facade Service for Multi-Chunk Payload Assembly
3
+ *
4
+ * **Purpose:** Provides a high-level, deterministic service for assembling and validating
5
+ * multi-chunk HTTP request payloads in a resource-bounded, DoS-resistant manner.
6
+ *
7
+ * **Why This Exists:** The ctgr (change-tracking-git-reconciliation) protocol encodes large
8
+ * mutating request bodies as multi-chunk GET twins to bypass client-side size limits. A
9
+ * large file upload might be split into 10 chunks, each arriving separately over potentially
10
+ * unreliable connections. This service:
11
+ *
12
+ * - **Buffers incomplete payloads** in a singleton per-request-handler, keyed by payload ID
13
+ * - **Enforces resource caps** (max concurrent payloads, max bytes per payload) to prevent DoS
14
+ * - **Evicts idle payloads** via TTL to free memory from incomplete or abandoned uploads
15
+ * - **Validates chunk integrity** by verifying crc32 per-chunk and sha256 at assembly time
16
+ * - **Handles partial uploads gracefully** — returns specific signals for intermediate/final chunks
17
+ * - **Never throws** — all errors are returned as `Error` objects for middleware to handle
18
+ *
19
+ * **Architecture:** Operates in three phases:
20
+ *
21
+ * 1. **Intake (add):** Parse raw query params, detect fast path (single chunk), or buffer
22
+ * 2. **Buffering:** Store chunks in order, track completion, evict stale entries
23
+ * 3. **Finalization:** On last chunk, assemble + verify + decode via codec
24
+ *
25
+ * **Invariants:**
26
+ * - All chunks for a payload (cid) must have identical framing (total count, byte length, gzip flag)
27
+ * - Chunk order matters; index 0..total-1 identifies each chunk's position in the final payload
28
+ * - sha256 is ONLY transmitted on the final chunk (optimization; verified at finalize time)
29
+ * - Size cap is enforced BOTH at chunk-add and at assembly time (defense-in-depth)
30
+ * - Eviction runs at every `add()` call to prevent memory bloat during connection failures
31
+ *
32
+ * **Resource Model:**
33
+ * - **Memory overhead:** ~30 bytes per in-flight entry + Uint8Array per buffered chunk
34
+ * - **Timing overhead:** O(n) scan for eviction on every add(); acceptable for n ≤ 200
35
+ * - **Max default memory:** 200 payloads * 10 MB = 2 GB worst-case; tunable at construction
36
+ *
37
+ * **Return Value Semantics:**
38
+ * - `CtgrDecoded` — success: full payload decoded and ready for handler
39
+ * - `false` — intermediate: chunk accepted, waiting for more; return HTTP 202
40
+ * - `Error` — failure: malformed, corrupt, or violating policy; return HTTP 400
41
+ *
42
+ * **Usage Example:**
43
+ *
44
+ * ```typescript
45
+ * const accumulator = new ChunkAccumulator({ maxInFlight: 200, maxPayloadBytes: 10 * 1024 * 1024 });
46
+ *
47
+ * // In Hono middleware:
48
+ * app.post('/ctgr*', async (c) => {
49
+ * const result = await accumulator.add(c.req.query());
50
+ * if (result instanceof Error) return c.json({ error: result.message }, 400);
51
+ * if (result === false) return c.text('', 202);
52
+ * // result is CtgrDecoded; process the full payload
53
+ * return c.json({ ok: true });
54
+ * });
55
+ * ```
56
+ *
57
+ * **For AI Agent Context:** When reading code that uses ChunkAccumulator, understand that:
58
+ * - The `add()` method is the sole entry point; it is NOT async in its return logic (uses fire-and-forget internals)
59
+ * - Every call to `add()` automatically runs eviction; this is O(1) amortized
60
+ * - A single-chunk payload (chunk.total === 1) bypasses buffering entirely (fast path)
61
+ * - Inconsistency errors (e.g., second chunk claims different total count) delete the entry immediately; recovery requires retransmitting all chunks from the client
62
+ * - The service itself is a singleton; do NOT create multiple instances per server or request thread
63
+ */
64
+ import type { CtgrDecoded } from './types';
65
+ /**
66
+ * Configuration for ChunkAccumulator resource limits and timing behavior.
67
+ *
68
+ * **defaults:** A conservative 200 concurrent payloads, 10 MB per payload, 30s idle TTL,
69
+ * real clock for production. These are tuned for typical web applications; adjust if your
70
+ * workload has sustained high-volume concurrent uploads or slower connections.
71
+ */
72
+ export interface ChunkAccumulatorConfig {
73
+ /**
74
+ * Max concurrent in-flight (incomplete) payloads before rejecting new ones.
75
+ * If a client has uploaded 20 chunks of payload A and starts uploading payload B,
76
+ * both are in-flight until one completes. Default: 200.
77
+ *
78
+ * **Tuning:** If you see "too many in-flight payloads" errors under normal load,
79
+ * increase this; if memory usage is high with idle connections, decrease or lower TTL.
80
+ */
81
+ maxInFlight?: number;
82
+ /**
83
+ * Hard cap on a single payload's assembled byte length (post-gzip, pre-decode).
84
+ * A client uploading a 15 MB file that compresses to 11 MB will be rejected.
85
+ * Default: 10 MB (10485760 bytes).
86
+ *
87
+ * **Security:** This is a DoS boundary; do not set arbitrarily high. If you need
88
+ * larger uploads, evaluate whether chunking is the right transport for your use case.
89
+ */
90
+ maxPayloadBytes?: number;
91
+ /**
92
+ * Idle TTL in milliseconds before an incomplete payload is evicted.
93
+ * If a chunk arrives at T=0 and the next at T=45s, and TTL=30s, the entry is
94
+ * discarded and reassembly fails. Default: 30000 (30 seconds).
95
+ *
96
+ * **Tuning:** For slow or unreliable connections, increase; for fast-paced local
97
+ * networks, decrease to free memory faster. Must be longer than your expected
98
+ * inter-chunk latency.
99
+ */
100
+ ttlMs?: number;
101
+ /**
102
+ * Injectable time source for testing. Default: `Date.now`. Use a fake clock
103
+ * in unit tests to control eviction timing deterministically.
104
+ *
105
+ * **Testing:** Pass a function that returns milliseconds-since-epoch.
106
+ */
107
+ now?: () => number;
108
+ }
109
+ /**
110
+ * ChunkAccumulator — The hardened service for multi-chunk payload assembly.
111
+ *
112
+ * **Thread safety:** NOT thread-safe. Instantiate ONE per server instance and protect
113
+ * with a mutex if used from concurrent handlers (Hono runs async handlers serially,
114
+ * so this is usually unnecessary).
115
+ *
116
+ * **Lifecycle:** Create once at server startup, use for the lifetime of the app.
117
+ * No explicit cleanup required (WeakMaps would be overkill for the typical 200-entry
118
+ * bound).
119
+ */
120
+ export declare class ChunkAccumulator {
121
+ private readonly entries;
122
+ private readonly maxInFlight;
123
+ private readonly maxPayloadBytes;
124
+ private readonly ttlMs;
125
+ private readonly now;
126
+ /**
127
+ * Construct a new ChunkAccumulator with the given resource policy.
128
+ *
129
+ * @param config — Optional configuration; all fields default to sensible production values.
130
+ *
131
+ * @example
132
+ * ```typescript
133
+ * const acc = new ChunkAccumulator({
134
+ * maxInFlight: 100,
135
+ * maxPayloadBytes: 5 * 1024 * 1024,
136
+ * ttlMs: 60_000,
137
+ * });
138
+ * ```
139
+ */
140
+ constructor(config?: ChunkAccumulatorConfig);
141
+ /**
142
+ * Current number of in-flight (incomplete) payloads.
143
+ *
144
+ * **Usage:** For observability; monitor this to detect connection storms or client bugs.
145
+ * If this climbs to maxInFlight, new uploads will be rejected.
146
+ */
147
+ get inFlightCount(): number;
148
+ /**
149
+ * Add a single chunk, returned from the client's ctgr-encoded request.
150
+ *
151
+ * **Call this once per incoming chunk.** The middleware parses `req.query()` or
152
+ * `req.url.searchParams` and passes it here.
153
+ *
154
+ * @param params — Raw query params from the HTTP request. Typically `c.req.query()`
155
+ * in Hono, or `url.searchParams` in Fetch.
156
+ *
157
+ * @returns
158
+ * - `CtgrDecoded` — the fully assembled and verified payload; ready for handler logic
159
+ * - `false` — intermediate chunk accepted; return HTTP 202 to client
160
+ * - `Error` — validation/policy failure (bad format, corrupt, over-limit); return HTTP 400
161
+ *
162
+ * **Never throws.** All failure cases return an `Error` object instead.
163
+ *
164
+ * **Performance:** Fast path (single-chunk payloads) is O(1) codec time. Multi-chunk:
165
+ * O(n) where n = number of in-flight payloads (for eviction scan). Acceptable for
166
+ * n ≤ 200.
167
+ *
168
+ * @example
169
+ * ```typescript
170
+ * app.post('/ctgr*', async (c) => {
171
+ * const result = await acc.add(c.req.query());
172
+ * if (result instanceof Error) {
173
+ * console.error('Upload error:', result.message);
174
+ * return c.json({ error: result.message }, 400);
175
+ * }
176
+ * if (result === false) {
177
+ * return c.text('', 202); // Chunk received, waiting for more
178
+ * }
179
+ * // result is CtgrDecoded; continue with your handler
180
+ * const { method, body, files } = result;
181
+ * return processUpload(body, files);
182
+ * });
183
+ * ```
184
+ */
185
+ add(params: Record<string, string> | URLSearchParams): Promise<CtgrDecoded | false | Error>;
186
+ /**
187
+ * Assemble, verify, and decode the complete payload.
188
+ *
189
+ * **Called once all chunks have arrived.** Handles gzip decompression,
190
+ * sha256 verification, and codec decoding.
191
+ *
192
+ * @param parts — Array of Uint8Array chunks in order
193
+ * @param meta — Metadata (gzip flag, HTTP method, sha256)
194
+ *
195
+ * @returns
196
+ * - `CtgrDecoded` — the payload with method and parsed body/files
197
+ * - `Error` — if sha256 mismatch, decompression fails, or codec error
198
+ *
199
+ * @internal — Called only from `add()`; do not invoke directly.
200
+ */
201
+ private finalize;
202
+ /**
203
+ * Remove entries that have not received a chunk in the last `ttlMs`.
204
+ *
205
+ * **Called at the start of every `add()` call.** Prevents memory bloat from
206
+ * abandoned or interrupted uploads (e.g., browser tab closes mid-upload).
207
+ *
208
+ * **Performance:** O(n) where n = current in-flight count. For n ≤ 200,
209
+ * this is < 1 ms; acceptable overhead per request.
210
+ *
211
+ * @internal — Called only from `add()`; do not invoke directly.
212
+ */
213
+ private evictStale;
214
+ }
@@ -0,0 +1,307 @@
1
+ /**
2
+ * # ChunkAccumulator — The Facade Service for Multi-Chunk Payload Assembly
3
+ *
4
+ * **Purpose:** Provides a high-level, deterministic service for assembling and validating
5
+ * multi-chunk HTTP request payloads in a resource-bounded, DoS-resistant manner.
6
+ *
7
+ * **Why This Exists:** The ctgr (change-tracking-git-reconciliation) protocol encodes large
8
+ * mutating request bodies as multi-chunk GET twins to bypass client-side size limits. A
9
+ * large file upload might be split into 10 chunks, each arriving separately over potentially
10
+ * unreliable connections. This service:
11
+ *
12
+ * - **Buffers incomplete payloads** in a singleton per-request-handler, keyed by payload ID
13
+ * - **Enforces resource caps** (max concurrent payloads, max bytes per payload) to prevent DoS
14
+ * - **Evicts idle payloads** via TTL to free memory from incomplete or abandoned uploads
15
+ * - **Validates chunk integrity** by verifying crc32 per-chunk and sha256 at assembly time
16
+ * - **Handles partial uploads gracefully** — returns specific signals for intermediate/final chunks
17
+ * - **Never throws** — all errors are returned as `Error` objects for middleware to handle
18
+ *
19
+ * **Architecture:** Operates in three phases:
20
+ *
21
+ * 1. **Intake (add):** Parse raw query params, detect fast path (single chunk), or buffer
22
+ * 2. **Buffering:** Store chunks in order, track completion, evict stale entries
23
+ * 3. **Finalization:** On last chunk, assemble + verify + decode via codec
24
+ *
25
+ * **Invariants:**
26
+ * - All chunks for a payload (cid) must have identical framing (total count, byte length, gzip flag)
27
+ * - Chunk order matters; index 0..total-1 identifies each chunk's position in the final payload
28
+ * - sha256 is ONLY transmitted on the final chunk (optimization; verified at finalize time)
29
+ * - Size cap is enforced BOTH at chunk-add and at assembly time (defense-in-depth)
30
+ * - Eviction runs at every `add()` call to prevent memory bloat during connection failures
31
+ *
32
+ * **Resource Model:**
33
+ * - **Memory overhead:** ~30 bytes per in-flight entry + Uint8Array per buffered chunk
34
+ * - **Timing overhead:** O(n) scan for eviction on every add(); acceptable for n ≤ 200
35
+ * - **Max default memory:** 200 payloads * 10 MB = 2 GB worst-case; tunable at construction
36
+ *
37
+ * **Return Value Semantics:**
38
+ * - `CtgrDecoded` — success: full payload decoded and ready for handler
39
+ * - `false` — intermediate: chunk accepted, waiting for more; return HTTP 202
40
+ * - `Error` — failure: malformed, corrupt, or violating policy; return HTTP 400
41
+ *
42
+ * **Usage Example:**
43
+ *
44
+ * ```typescript
45
+ * const accumulator = new ChunkAccumulator({ maxInFlight: 200, maxPayloadBytes: 10 * 1024 * 1024 });
46
+ *
47
+ * // In Hono middleware:
48
+ * app.post('/ctgr*', async (c) => {
49
+ * const result = await accumulator.add(c.req.query());
50
+ * if (result instanceof Error) return c.json({ error: result.message }, 400);
51
+ * if (result === false) return c.text('', 202);
52
+ * // result is CtgrDecoded; process the full payload
53
+ * return c.json({ ok: true });
54
+ * });
55
+ * ```
56
+ *
57
+ * **For AI Agent Context:** When reading code that uses ChunkAccumulator, understand that:
58
+ * - The `add()` method is the sole entry point; it is NOT async in its return logic (uses fire-and-forget internals)
59
+ * - Every call to `add()` automatically runs eviction; this is O(1) amortized
60
+ * - A single-chunk payload (chunk.total === 1) bypasses buffering entirely (fast path)
61
+ * - Inconsistency errors (e.g., second chunk claims different total count) delete the entry immediately; recovery requires retransmitting all chunks from the client
62
+ * - The service itself is a singleton; do NOT create multiple instances per server or request thread
63
+ */
64
+ import { decodePayload, parseChunk } from './codec';
65
+ import { sha256Hex } from './digest';
66
+ /**
67
+ * ChunkAccumulator — The hardened service for multi-chunk payload assembly.
68
+ *
69
+ * **Thread safety:** NOT thread-safe. Instantiate ONE per server instance and protect
70
+ * with a mutex if used from concurrent handlers (Hono runs async handlers serially,
71
+ * so this is usually unnecessary).
72
+ *
73
+ * **Lifecycle:** Create once at server startup, use for the lifetime of the app.
74
+ * No explicit cleanup required (WeakMaps would be overkill for the typical 200-entry
75
+ * bound).
76
+ */
77
+ export class ChunkAccumulator {
78
+ entries = new Map();
79
+ maxInFlight;
80
+ maxPayloadBytes;
81
+ ttlMs;
82
+ now;
83
+ /**
84
+ * Construct a new ChunkAccumulator with the given resource policy.
85
+ *
86
+ * @param config — Optional configuration; all fields default to sensible production values.
87
+ *
88
+ * @example
89
+ * ```typescript
90
+ * const acc = new ChunkAccumulator({
91
+ * maxInFlight: 100,
92
+ * maxPayloadBytes: 5 * 1024 * 1024,
93
+ * ttlMs: 60_000,
94
+ * });
95
+ * ```
96
+ */
97
+ constructor(config = {}) {
98
+ this.maxInFlight = config.maxInFlight ?? 200;
99
+ this.maxPayloadBytes = config.maxPayloadBytes ?? 10 * 1024 * 1024;
100
+ this.ttlMs = config.ttlMs ?? 30_000;
101
+ this.now = config.now ?? Date.now;
102
+ }
103
+ /**
104
+ * Current number of in-flight (incomplete) payloads.
105
+ *
106
+ * **Usage:** For observability; monitor this to detect connection storms or client bugs.
107
+ * If this climbs to maxInFlight, new uploads will be rejected.
108
+ */
109
+ get inFlightCount() {
110
+ return this.entries.size;
111
+ }
112
+ /**
113
+ * Add a single chunk, returned from the client's ctgr-encoded request.
114
+ *
115
+ * **Call this once per incoming chunk.** The middleware parses `req.query()` or
116
+ * `req.url.searchParams` and passes it here.
117
+ *
118
+ * @param params — Raw query params from the HTTP request. Typically `c.req.query()`
119
+ * in Hono, or `url.searchParams` in Fetch.
120
+ *
121
+ * @returns
122
+ * - `CtgrDecoded` — the fully assembled and verified payload; ready for handler logic
123
+ * - `false` — intermediate chunk accepted; return HTTP 202 to client
124
+ * - `Error` — validation/policy failure (bad format, corrupt, over-limit); return HTTP 400
125
+ *
126
+ * **Never throws.** All failure cases return an `Error` object instead.
127
+ *
128
+ * **Performance:** Fast path (single-chunk payloads) is O(1) codec time. Multi-chunk:
129
+ * O(n) where n = number of in-flight payloads (for eviction scan). Acceptable for
130
+ * n ≤ 200.
131
+ *
132
+ * @example
133
+ * ```typescript
134
+ * app.post('/ctgr*', async (c) => {
135
+ * const result = await acc.add(c.req.query());
136
+ * if (result instanceof Error) {
137
+ * console.error('Upload error:', result.message);
138
+ * return c.json({ error: result.message }, 400);
139
+ * }
140
+ * if (result === false) {
141
+ * return c.text('', 202); // Chunk received, waiting for more
142
+ * }
143
+ * // result is CtgrDecoded; continue with your handler
144
+ * const { method, body, files } = result;
145
+ * return processUpload(body, files);
146
+ * });
147
+ * ```
148
+ */
149
+ async add(params) {
150
+ // === Step 1: Parse the chunk ===
151
+ let chunk;
152
+ try {
153
+ chunk = parseChunk(params);
154
+ }
155
+ catch (err) {
156
+ return asError(err);
157
+ }
158
+ // === Step 2: Evict stale entries ===
159
+ // Run eviction on every add to prevent memory bloat if clients abandon uploads.
160
+ this.evictStale();
161
+ // === Step 3: Fast path for single-chunk payloads ===
162
+ // No buffering needed; verify integrity and decode immediately.
163
+ if (chunk.total === 1) {
164
+ return this.finalize([chunk.bytes], chunk);
165
+ }
166
+ // === Step 4: Enforce global size cap ===
167
+ // Reject payloads that exceed our size limit before buffering anything.
168
+ if (chunk.payloadLength > this.maxPayloadBytes) {
169
+ return new Error('ctgr: payload exceeds size cap');
170
+ }
171
+ // === Step 5: Get or create the buffer entry ===
172
+ let entry = this.entries.get(chunk.cid);
173
+ if (!entry) {
174
+ // First chunk of a new payload.
175
+ if (this.entries.size >= this.maxInFlight) {
176
+ return new Error('ctgr: too many in-flight payloads');
177
+ }
178
+ entry = {
179
+ parts: new Array(chunk.total),
180
+ received: 0,
181
+ total: chunk.total,
182
+ bytes: 0,
183
+ payloadLength: chunk.payloadLength,
184
+ gzip: chunk.gzip,
185
+ method: chunk.method,
186
+ updatedAt: this.now(),
187
+ };
188
+ this.entries.set(chunk.cid, entry);
189
+ }
190
+ else if (
191
+ // === Step 6: Validate consistent framing ===
192
+ // All chunks for the same payload must agree on total count, byte length, and flags.
193
+ // If a subsequent chunk declares different framing, it's a client bug or attack.
194
+ entry.total !== chunk.total ||
195
+ entry.payloadLength !== chunk.payloadLength ||
196
+ entry.gzip !== chunk.gzip ||
197
+ entry.method !== chunk.method) {
198
+ this.entries.delete(chunk.cid);
199
+ return new Error('ctgr: inconsistent chunk metadata');
200
+ }
201
+ // === Step 7: Buffer the chunk and track progress ===
202
+ entry.updatedAt = this.now();
203
+ if (chunk.sha)
204
+ entry.sha = chunk.sha;
205
+ // Only count a chunk if we haven't seen it yet (avoid double-counting retries).
206
+ if (entry.parts[chunk.index] === undefined) {
207
+ entry.parts[chunk.index] = chunk.bytes;
208
+ entry.received += 1;
209
+ entry.bytes += chunk.bytes.length;
210
+ // === Step 8: Re-check size cap after buffering ===
211
+ // A concurrent chunk might push us over the limit; catch it early.
212
+ if (entry.bytes > this.maxPayloadBytes) {
213
+ this.entries.delete(chunk.cid);
214
+ return new Error('ctgr: payload exceeds size cap');
215
+ }
216
+ }
217
+ // === Step 9: Detect completion and finalize ===
218
+ if (entry.received < entry.total) {
219
+ // More chunks expected; signal the client to continue (HTTP 202).
220
+ return false;
221
+ }
222
+ // All chunks received; proceed to finalization.
223
+ this.entries.delete(chunk.cid);
224
+ return this.finalize(entry.parts, entry);
225
+ }
226
+ /**
227
+ * Assemble, verify, and decode the complete payload.
228
+ *
229
+ * **Called once all chunks have arrived.** Handles gzip decompression,
230
+ * sha256 verification, and codec decoding.
231
+ *
232
+ * @param parts — Array of Uint8Array chunks in order
233
+ * @param meta — Metadata (gzip flag, HTTP method, sha256)
234
+ *
235
+ * @returns
236
+ * - `CtgrDecoded` — the payload with method and parsed body/files
237
+ * - `Error` — if sha256 mismatch, decompression fails, or codec error
238
+ *
239
+ * @internal — Called only from `add()`; do not invoke directly.
240
+ */
241
+ async finalize(parts, meta) {
242
+ // Concatenate all chunks into a single payload buffer.
243
+ const payload = concat(parts);
244
+ // Defensive size re-check (should be impossible, but belt-and-suspenders).
245
+ if (payload.length > this.maxPayloadBytes) {
246
+ return new Error('ctgr: payload exceeds size cap');
247
+ }
248
+ // If sha256 is present (always on last chunk), verify it.
249
+ // This catches bit-flips, corrupt gzip streams, or tampering.
250
+ if (meta.sha && (await sha256Hex(payload)) !== meta.sha) {
251
+ return new Error('ctgr: payload sha256 mismatch');
252
+ }
253
+ // Decode: decompress if gzipped, deserialize JSON, parse method-specific structure.
254
+ try {
255
+ return await decodePayload(payload, meta.gzip, meta.method);
256
+ }
257
+ catch (err) {
258
+ return asError(err);
259
+ }
260
+ }
261
+ /**
262
+ * Remove entries that have not received a chunk in the last `ttlMs`.
263
+ *
264
+ * **Called at the start of every `add()` call.** Prevents memory bloat from
265
+ * abandoned or interrupted uploads (e.g., browser tab closes mid-upload).
266
+ *
267
+ * **Performance:** O(n) where n = current in-flight count. For n ≤ 200,
268
+ * this is < 1 ms; acceptable overhead per request.
269
+ *
270
+ * @internal — Called only from `add()`; do not invoke directly.
271
+ */
272
+ evictStale() {
273
+ const cutoff = this.now() - this.ttlMs;
274
+ for (const [cid, entry] of this.entries) {
275
+ if (entry.updatedAt < cutoff) {
276
+ this.entries.delete(cid);
277
+ }
278
+ }
279
+ }
280
+ }
281
+ /**
282
+ * Convert any error to an Error object (never null or undefined).
283
+ *
284
+ * @internal
285
+ */
286
+ function asError(err) {
287
+ return err instanceof Error ? err : new Error(String(err));
288
+ }
289
+ /**
290
+ * Concatenate an array of Uint8Array into a single buffer.
291
+ *
292
+ * **Performance:** O(n) where n = total byte length; allocation is minimal (one pass).
293
+ *
294
+ * @internal
295
+ */
296
+ function concat(parts) {
297
+ let total = 0;
298
+ for (const part of parts)
299
+ total += part.length;
300
+ const out = new Uint8Array(total);
301
+ let offset = 0;
302
+ for (const part of parts) {
303
+ out.set(part, offset);
304
+ offset += part.length;
305
+ }
306
+ return out;
307
+ }
@@ -0,0 +1,24 @@
1
+ import type { CtgrDecoded } from './types';
2
+ export interface ChunkStoreOpts {
3
+ /** Max concurrent in-flight (incomplete) payloads. Default 200. */
4
+ maxInFlight?: number;
5
+ /** Hard cap on a single payload's assembled byte length. Default 10 MB. */
6
+ maxPayloadBytes?: number;
7
+ /** Idle TTL before an incomplete payload is evicted. Default 30 s. */
8
+ ttlMs?: number;
9
+ /** Injectable clock (tests). Default `Date.now`. */
10
+ now?: () => number;
11
+ }
12
+ export declare class ChunkStore {
13
+ private readonly entries;
14
+ private readonly maxInFlight;
15
+ private readonly maxPayloadBytes;
16
+ private readonly ttlMs;
17
+ private readonly now;
18
+ constructor(opts?: ChunkStoreOpts);
19
+ /** Number of in-flight (incomplete) payloads currently buffered. */
20
+ get size(): number;
21
+ add(params: Record<string, string> | URLSearchParams): Promise<CtgrDecoded | false | Error>;
22
+ private finalize;
23
+ private evictStale;
24
+ }
@@ -0,0 +1,125 @@
1
+ /*
2
+ * Server-side bounded accumulator for multi-chunk ctgr payloads. A singleton on the
3
+ * server (NOT per-request), keyed by the payload id `_cid`. Every `add`:
4
+ * - evicts stale entries (per-cid TTL),
5
+ * - enforces the max-in-flight + max-payload-bytes caps (DoS bounds),
6
+ * - buffers the chunk (its crc is already verified by parseChunk), and
7
+ * - on the final chunk, verifies the full-payload sha256 before decoding.
8
+ *
9
+ * It NEVER throws: a malformed/corrupt/oversized payload returns an `Error` the
10
+ * middleware maps to a 400; an intermediate chunk returns `false` (→ 202).
11
+ */
12
+ import { decodePayload, parseChunk } from './codec';
13
+ import { sha256Hex } from './digest';
14
+ export class ChunkStore {
15
+ entries = new Map();
16
+ maxInFlight;
17
+ maxPayloadBytes;
18
+ ttlMs;
19
+ now;
20
+ constructor(opts = {}) {
21
+ this.maxInFlight = opts.maxInFlight ?? 200;
22
+ this.maxPayloadBytes = opts.maxPayloadBytes ?? 10 * 1024 * 1024;
23
+ this.ttlMs = opts.ttlMs ?? 30_000;
24
+ this.now = opts.now ?? Date.now;
25
+ }
26
+ /** Number of in-flight (incomplete) payloads currently buffered. */
27
+ get size() {
28
+ return this.entries.size;
29
+ }
30
+ async add(params) {
31
+ let chunk;
32
+ try {
33
+ chunk = parseChunk(params);
34
+ }
35
+ catch (err) {
36
+ return asError(err);
37
+ }
38
+ this.evictStale();
39
+ // Single-chunk fast path: verify sha + decode immediately, no buffering.
40
+ if (chunk.total === 1) {
41
+ return this.finalize([chunk.bytes], chunk);
42
+ }
43
+ if (chunk.payloadLength > this.maxPayloadBytes) {
44
+ return new Error('ctgr: payload exceeds size cap');
45
+ }
46
+ let entry = this.entries.get(chunk.cid);
47
+ if (!entry) {
48
+ if (this.entries.size >= this.maxInFlight) {
49
+ return new Error('ctgr: too many in-flight payloads');
50
+ }
51
+ entry = {
52
+ parts: new Array(chunk.total),
53
+ received: 0,
54
+ total: chunk.total,
55
+ bytes: 0,
56
+ payloadLength: chunk.payloadLength,
57
+ gzip: chunk.gzip,
58
+ method: chunk.method,
59
+ updatedAt: this.now(),
60
+ };
61
+ this.entries.set(chunk.cid, entry);
62
+ }
63
+ else if (entry.total !== chunk.total ||
64
+ entry.payloadLength !== chunk.payloadLength ||
65
+ entry.gzip !== chunk.gzip ||
66
+ entry.method !== chunk.method) {
67
+ // A payload's framing must not change mid-stream.
68
+ this.entries.delete(chunk.cid);
69
+ return new Error('ctgr: inconsistent chunk metadata');
70
+ }
71
+ entry.updatedAt = this.now();
72
+ if (chunk.sha)
73
+ entry.sha = chunk.sha;
74
+ if (entry.parts[chunk.index] === undefined) {
75
+ entry.parts[chunk.index] = chunk.bytes;
76
+ entry.received += 1;
77
+ entry.bytes += chunk.bytes.length;
78
+ if (entry.bytes > this.maxPayloadBytes) {
79
+ this.entries.delete(chunk.cid);
80
+ return new Error('ctgr: payload exceeds size cap');
81
+ }
82
+ }
83
+ if (entry.received < entry.total)
84
+ return false;
85
+ this.entries.delete(chunk.cid);
86
+ return this.finalize(entry.parts, entry);
87
+ }
88
+ async finalize(parts, meta) {
89
+ const payload = concat(parts);
90
+ if (payload.length > this.maxPayloadBytes) {
91
+ return new Error('ctgr: payload exceeds size cap');
92
+ }
93
+ if (meta.sha && (await sha256Hex(payload)) !== meta.sha) {
94
+ return new Error('ctgr: payload sha256 mismatch');
95
+ }
96
+ try {
97
+ return await decodePayload(payload, meta.gzip, meta.method);
98
+ }
99
+ catch (err) {
100
+ return asError(err);
101
+ }
102
+ }
103
+ evictStale() {
104
+ const cutoff = this.now() - this.ttlMs;
105
+ for (const [cid, entry] of this.entries) {
106
+ if (entry.updatedAt < cutoff)
107
+ this.entries.delete(cid);
108
+ }
109
+ }
110
+ }
111
+ function asError(err) {
112
+ return err instanceof Error ? err : new Error(String(err));
113
+ }
114
+ function concat(parts) {
115
+ let total = 0;
116
+ for (const part of parts)
117
+ total += part.length;
118
+ const out = new Uint8Array(total);
119
+ let offset = 0;
120
+ for (const part of parts) {
121
+ out.set(part, offset);
122
+ offset += part.length;
123
+ }
124
+ return out;
125
+ }