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,101 @@
1
+ /**
2
+ * The canonical server error class + error/validation envelope for the whole
3
+ * monorepo — cursedbelt apps AND cursedshell plugins.
4
+ *
5
+ * Every API failure — thrown deep in a handler, surfaced by middleware, or raised
6
+ * by a plugin service — is shaped into ONE wire contract, the **rich** envelope
7
+ * defined once in cwip (`cwip/error`):
8
+ *
9
+ * { "error": { "name", "message", "code?", "status?", "category?", "context?",
10
+ * "isOperational", "timestamp" }, "correlationId?": "…" }
11
+ *
12
+ * Clients parse exactly this shape, so there is no per-route error drift — and no
13
+ * drift between a cwip-based server (which already emits it via `cwip/server`'s
14
+ * `errorHandler`) and a cursedshell one. The extra fields are additive:
15
+ * a client that only reads `message`/`code`/`context` keeps working unchanged.
16
+ *
17
+ * `ApiError` is the ergonomic throwable: `throw new ApiError(404, 'not found')`.
18
+ * It is a thin subclass of cwip's `AppError` with the status-first constructor
19
+ * shape every plugin already uses, so a single `getSummary()`/`toErrorEnvelope`
20
+ * produces the rich body. The `errorEnvelope` Hono `onError` handler
21
+ * (cursedbelt-server) turns it (and any other thrown value) into the envelope +
22
+ * the right HTTP status; {@link ApiError.fromEnvelope} rebuilds one on the CLIENT
23
+ * from a parsed error body.
24
+ *
25
+ * This module is **framework-free** (no Hono, no DOM, no server deps — it imports
26
+ * only cwip's framework-free `cwip/error` and a type-only `zod` for the validation
27
+ * builder), so it is safe to import from browser bundles and from cursedshell's
28
+ * plugin-pure layer via the `cursedbelt-core/errors` subpath. Server code reaches the
29
+ * same symbols through `cursedbelt-server` (which re-exports this module).
30
+ */
31
+ import { AppError, type AppErrorSummary, type ErrorEnvelope, isAppError, type ToErrorEnvelopeOptions, toErrorEnvelope } from 'cwip/error';
32
+ import type { ZodError } from 'zod';
33
+ export { AppError, type AppErrorSummary, type ErrorEnvelope, isAppError, type ToErrorEnvelopeOptions, toErrorEnvelope };
34
+ export interface ApiErrorOptions {
35
+ /** Stable machine code carried into the envelope. */
36
+ code?: string;
37
+ /** Structured detail carried into the envelope (e.g. `{ issues }`). */
38
+ context?: Record<string, unknown>;
39
+ /** Underlying cause, preserved for server-side logging (never serialized). */
40
+ cause?: unknown;
41
+ }
42
+ /**
43
+ * A throwable HTTP error carrying its status + canonical-envelope fields. A thin,
44
+ * status-first subclass of cwip's {@link AppError} — `new ApiError(404, 'nope',
45
+ * { code, context })` is the ergonomic form every handler/middleware/plugin
46
+ * service uses; the inherited `getSummary()` yields the rich `AppErrorSummary` so
47
+ * `toErrorEnvelope` maps it to the response with no extra work. On the client,
48
+ * {@link ApiError.fromEnvelope} rebuilds it from a parsed error body so callers
49
+ * can branch on `.status`/`.code`/`.context`.
50
+ */
51
+ export declare class ApiError extends AppError {
52
+ /**
53
+ * Always set — narrows the base's optional `status` (an `ApiError` is defined
54
+ * by its HTTP status). The value is assigned by `AppError`'s constructor from
55
+ * the `status` we pass through; `declare` just re-types it without re-emitting
56
+ * the field (so the base's assignment isn't shadowed under class-field semantics).
57
+ */
58
+ readonly status: number;
59
+ constructor(status: number, message: string, options?: ApiErrorOptions);
60
+ /**
61
+ * Rebuild an `ApiError` from a parsed error envelope's `error` object — the
62
+ * client half of the contract (e.g. `createServiceClient`'s response parser).
63
+ * Reads only `message`/`code`/`context`, so it works against both the rich
64
+ * envelope and any legacy minimal body. Missing/absent envelope falls back to a
65
+ * status-derived message.
66
+ */
67
+ static fromEnvelope(status: number, envelope?: Partial<AppErrorSummary>): ApiError;
68
+ }
69
+ /** Type guard for {@link ApiError} (the status-first server throwable). */
70
+ export declare const isApiError: (value: unknown) => value is ApiError;
71
+ /**
72
+ * One field-level validation error on the wire. `path` is the dotted Zod path to
73
+ * the offending field (`''` for a top-level/whole-body issue) — it mirrors
74
+ * `z.ZodIssue.path`, so server code maps 1:1 and clients read a stable name.
75
+ */
76
+ export interface ValidationIssue {
77
+ /** Dotted path to the offending field (`''` for a top-level issue). */
78
+ path: string;
79
+ /** Human-readable, zod-authored message. */
80
+ message: string;
81
+ }
82
+ /**
83
+ * The canonical machine code carried in a validation envelope's `error.code`, so a
84
+ * client can branch on it without string-matching the (localizable) message.
85
+ */
86
+ export declare const VALIDATION_ERROR_CODE = "validation";
87
+ /** The HTTP status every validation failure maps to (both emitters respond 400). */
88
+ export declare const VALIDATION_STATUS = 400;
89
+ /** Map a {@link https://zod.dev | zod} error's issues to the wire {@link ValidationIssue}s. */
90
+ export declare function toValidationIssues(error: ZodError): ValidationIssue[];
91
+ /**
92
+ * Build the canonical validation envelope from a zod error (or pre-mapped issues).
93
+ * It constructs an {@link ApiError} (status {@link VALIDATION_STATUS}, code
94
+ * {@link VALIDATION_ERROR_CODE}, `context.issues`) and runs it through the shared
95
+ * {@link toErrorEnvelope}, so a validation failure is shaped identically to every
96
+ * other error on the wire — `{ error: { …, code: 'validation', status: 400,
97
+ * context: { issues } } }`. This is the single builder — both cursedbelt's
98
+ * `zValidator` middleware and cursedshell's `buildServiceRouter` call it, so a
99
+ * client parses validation failures identically no matter which layer emitted them.
100
+ */
101
+ export declare function validationEnvelope(errorOrIssues: ZodError | ValidationIssue[], message?: string): ErrorEnvelope;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * The canonical server error class + error/validation envelope for the whole
3
+ * monorepo — cursedbelt apps AND cursedshell plugins.
4
+ *
5
+ * Every API failure — thrown deep in a handler, surfaced by middleware, or raised
6
+ * by a plugin service — is shaped into ONE wire contract, the **rich** envelope
7
+ * defined once in cwip (`cwip/error`):
8
+ *
9
+ * { "error": { "name", "message", "code?", "status?", "category?", "context?",
10
+ * "isOperational", "timestamp" }, "correlationId?": "…" }
11
+ *
12
+ * Clients parse exactly this shape, so there is no per-route error drift — and no
13
+ * drift between a cwip-based server (which already emits it via `cwip/server`'s
14
+ * `errorHandler`) and a cursedshell one. The extra fields are additive:
15
+ * a client that only reads `message`/`code`/`context` keeps working unchanged.
16
+ *
17
+ * `ApiError` is the ergonomic throwable: `throw new ApiError(404, 'not found')`.
18
+ * It is a thin subclass of cwip's `AppError` with the status-first constructor
19
+ * shape every plugin already uses, so a single `getSummary()`/`toErrorEnvelope`
20
+ * produces the rich body. The `errorEnvelope` Hono `onError` handler
21
+ * (cursedbelt-server) turns it (and any other thrown value) into the envelope +
22
+ * the right HTTP status; {@link ApiError.fromEnvelope} rebuilds one on the CLIENT
23
+ * from a parsed error body.
24
+ *
25
+ * This module is **framework-free** (no Hono, no DOM, no server deps — it imports
26
+ * only cwip's framework-free `cwip/error` and a type-only `zod` for the validation
27
+ * builder), so it is safe to import from browser bundles and from cursedshell's
28
+ * plugin-pure layer via the `cursedbelt-core/errors` subpath. Server code reaches the
29
+ * same symbols through `cursedbelt-server` (which re-exports this module).
30
+ */
31
+ import { AppError, isAppError, toErrorEnvelope, } from 'cwip/error';
32
+ // ── Re-export the ONE canonical envelope + formatter (defined in cwip) ────────
33
+ // So `cursedbelt-core/errors` and `cwip/error` speak the exact same wire contract.
34
+ export { AppError, isAppError, toErrorEnvelope };
35
+ /**
36
+ * A throwable HTTP error carrying its status + canonical-envelope fields. A thin,
37
+ * status-first subclass of cwip's {@link AppError} — `new ApiError(404, 'nope',
38
+ * { code, context })` is the ergonomic form every handler/middleware/plugin
39
+ * service uses; the inherited `getSummary()` yields the rich `AppErrorSummary` so
40
+ * `toErrorEnvelope` maps it to the response with no extra work. On the client,
41
+ * {@link ApiError.fromEnvelope} rebuilds it from a parsed error body so callers
42
+ * can branch on `.status`/`.code`/`.context`.
43
+ */
44
+ export class ApiError extends AppError {
45
+ constructor(status, message, options = {}) {
46
+ super(message, {
47
+ status,
48
+ code: options.code,
49
+ context: options.context,
50
+ cause: options.cause,
51
+ });
52
+ // `AppError` sets `this.name = new.target.name` → 'ApiError' already.
53
+ }
54
+ /**
55
+ * Rebuild an `ApiError` from a parsed error envelope's `error` object — the
56
+ * client half of the contract (e.g. `createServiceClient`'s response parser).
57
+ * Reads only `message`/`code`/`context`, so it works against both the rich
58
+ * envelope and any legacy minimal body. Missing/absent envelope falls back to a
59
+ * status-derived message.
60
+ */
61
+ static fromEnvelope(status, envelope) {
62
+ return new ApiError(status, envelope?.message ?? `Request failed with status ${status}`, {
63
+ code: envelope?.code,
64
+ context: envelope?.context,
65
+ });
66
+ }
67
+ }
68
+ /** Type guard for {@link ApiError} (the status-first server throwable). */
69
+ export const isApiError = (value) => value instanceof ApiError;
70
+ /**
71
+ * The canonical machine code carried in a validation envelope's `error.code`, so a
72
+ * client can branch on it without string-matching the (localizable) message.
73
+ */
74
+ export const VALIDATION_ERROR_CODE = 'validation';
75
+ /** The HTTP status every validation failure maps to (both emitters respond 400). */
76
+ export const VALIDATION_STATUS = 400;
77
+ /** The default client-facing message when validation fails. */
78
+ const DEFAULT_VALIDATION_MESSAGE = 'Validation failed';
79
+ /** Map a {@link https://zod.dev | zod} error's issues to the wire {@link ValidationIssue}s. */
80
+ export function toValidationIssues(error) {
81
+ return error.issues.map((issue) => ({
82
+ path: issue.path.map((segment) => String(segment)).join('.'),
83
+ message: issue.message,
84
+ }));
85
+ }
86
+ /**
87
+ * Build the canonical validation envelope from a zod error (or pre-mapped issues).
88
+ * It constructs an {@link ApiError} (status {@link VALIDATION_STATUS}, code
89
+ * {@link VALIDATION_ERROR_CODE}, `context.issues`) and runs it through the shared
90
+ * {@link toErrorEnvelope}, so a validation failure is shaped identically to every
91
+ * other error on the wire — `{ error: { …, code: 'validation', status: 400,
92
+ * context: { issues } } }`. This is the single builder — both cursedbelt's
93
+ * `zValidator` middleware and cursedshell's `buildServiceRouter` call it, so a
94
+ * client parses validation failures identically no matter which layer emitted them.
95
+ */
96
+ export function validationEnvelope(errorOrIssues, message = DEFAULT_VALIDATION_MESSAGE) {
97
+ const issues = Array.isArray(errorOrIssues) ? errorOrIssues : toValidationIssues(errorOrIssues);
98
+ return toErrorEnvelope(new ApiError(VALIDATION_STATUS, message, { code: VALIDATION_ERROR_CODE, context: { issues } }));
99
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * `Uint8Array` → a body the DOM types accept, in ONE place.
3
+ *
4
+ * ## The problem this exists to stop being solved five different ways
5
+ *
6
+ * TypeScript 5.7 made `Uint8Array` generic in its buffer: `Uint8Array<ArrayBufferLike>`.
7
+ * `BodyInit` accepts `ArrayBufferView<ArrayBuffer>` — an ArrayBuffer view, NOT a
8
+ * possibly-shared one — so `new Response(bytes)` and `fetch(url, { body: bytes })`
9
+ * stopped compiling for every plain `new Uint8Array([…])` in the repo. Nothing
10
+ * about the runtime changed; the types got more precise about `SharedArrayBuffer`.
11
+ *
12
+ * Two fixes suggest themselves and one of them is a bug:
13
+ *
14
+ * * `bytes.buffer as ArrayBuffer` — **wrong in general.** `.buffer` is the
15
+ * WHOLE underlying buffer, so for any view that does not span it (`subarray`,
16
+ * a slice of a decoder's output, a chunk read off a stream) this silently
17
+ * sends more bytes than the caller holds. It happens to be right for a
18
+ * literal `new Uint8Array([1,2,3,4])` and that is exactly what makes it a
19
+ * trap: it works everywhere it is first tried.
20
+ * * `bytes as Uint8Array<ArrayBuffer>` — correct, and what this does. It
21
+ * asserts the one thing the caller actually knows (this buffer is not
22
+ * shared), changes no lengths, and keeps the view's own offset.
23
+ *
24
+ * Test-only by intent: production code that needs to send bytes should hold a
25
+ * correctly-typed buffer rather than assert one.
26
+ */
27
+ /** A `Uint8Array` as a `BodyInit`, preserving byte offset and length. */
28
+ export declare const asBody: (bytes: Uint8Array) => Uint8Array<ArrayBuffer>;
29
+ /** `new Response(bytes)` with the assertion applied — the common case. */
30
+ export declare const bytesResponse: (bytes: Uint8Array, init?: ResponseInit) => Response;
31
+ /** A readable stream of `bytes` — what the media catalogue's ingest paths take. */
32
+ export declare const bytesStream: (bytes: Uint8Array | string) => ReadableStream<Uint8Array>;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * `Uint8Array` → a body the DOM types accept, in ONE place.
3
+ *
4
+ * ## The problem this exists to stop being solved five different ways
5
+ *
6
+ * TypeScript 5.7 made `Uint8Array` generic in its buffer: `Uint8Array<ArrayBufferLike>`.
7
+ * `BodyInit` accepts `ArrayBufferView<ArrayBuffer>` — an ArrayBuffer view, NOT a
8
+ * possibly-shared one — so `new Response(bytes)` and `fetch(url, { body: bytes })`
9
+ * stopped compiling for every plain `new Uint8Array([…])` in the repo. Nothing
10
+ * about the runtime changed; the types got more precise about `SharedArrayBuffer`.
11
+ *
12
+ * Two fixes suggest themselves and one of them is a bug:
13
+ *
14
+ * * `bytes.buffer as ArrayBuffer` — **wrong in general.** `.buffer` is the
15
+ * WHOLE underlying buffer, so for any view that does not span it (`subarray`,
16
+ * a slice of a decoder's output, a chunk read off a stream) this silently
17
+ * sends more bytes than the caller holds. It happens to be right for a
18
+ * literal `new Uint8Array([1,2,3,4])` and that is exactly what makes it a
19
+ * trap: it works everywhere it is first tried.
20
+ * * `bytes as Uint8Array<ArrayBuffer>` — correct, and what this does. It
21
+ * asserts the one thing the caller actually knows (this buffer is not
22
+ * shared), changes no lengths, and keeps the view's own offset.
23
+ *
24
+ * Test-only by intent: production code that needs to send bytes should hold a
25
+ * correctly-typed buffer rather than assert one.
26
+ */
27
+ /** A `Uint8Array` as a `BodyInit`, preserving byte offset and length. */
28
+ export const asBody = (bytes) => bytes;
29
+ /** `new Response(bytes)` with the assertion applied — the common case. */
30
+ export const bytesResponse = (bytes, init) => new Response(asBody(bytes), init);
31
+ /** A readable stream of `bytes` — what the media catalogue's ingest paths take. */
32
+ export const bytesStream = (bytes) => new Response(typeof bytes === 'string' ? asBody(new TextEncoder().encode(bytes)) : asBody(bytes))
33
+ .body;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Make a happy-dom node CHEAP to serialize, for every `bun test` process in this
3
+ * repo. Imported by `packages/cursedbelt/happydom.ts` and
4
+ * `packages/cursedshell/happydom.ts` — the two test preloads — so it is the ONE
5
+ * place this behavior is defined.
6
+ *
7
+ * ## The bug this exists to not have
8
+ *
9
+ * `expect(received)` builds its failure message EAGERLY, before anything decides
10
+ * whether that message will ever be read. When `received` is a happy-dom node,
11
+ * Bun's inspector walks the node's own object graph — and a happy-dom node holds
12
+ * `[Symbol(parentNode)]`, so the walk goes UPWARD through the parent chain into
13
+ * `HTMLBodyElement` → `HTMLHtmlElement` → `HTMLDocument` → the window, and back
14
+ * down through every sibling subtree it meets on the way.
15
+ *
16
+ * Measured on this repo's own studio spec (2026-07-26):
17
+ *
18
+ * | received value | cost of ONE failing `expect` |
19
+ * |---------------------------------------|------------------------------|
20
+ * | `true` | 0.1 ms |
21
+ * | a plain object | 0.0 ms |
22
+ * | a detached empty `<div>` | 9.8 ms |
23
+ * | an ATTACHED empty `<div>` | 29.8 ms |
24
+ * | an attached `<div>` with 50 children | **1,576 ms** |
25
+ *
26
+ * That cost is paid on assertions that FAIL, which sounds rare until you notice
27
+ * that `waitFor(() => expect(el).toBeNull())` fails on purpose on every poll
28
+ * until the condition comes true. The studio spec's `settled()` helper spent
29
+ * ~790 ms per mount building error strings that `waitFor` then threw away — 12
30
+ * mounts, ~9.5 s of a 10.5 s file, and on the CI runner the same two assertions
31
+ * blew through Bun's 5,000 ms per-test timeout and turned `main` red.
32
+ *
33
+ * ## The fix
34
+ *
35
+ * Install `nodejs.util.inspect.custom` on happy-dom's `Node.prototype` (the
36
+ * common ancestor of every element, text node, fragment and document), returning
37
+ * TRUNCATED `outerHTML`. Bun's inspector honors that hook and stops walking.
38
+ * Same measurement, after: **1,576 ms → 0.1 ms.**
39
+ *
40
+ * The message gets BETTER as well as faster. What it replaces is not an HTML
41
+ * dump; it is a `[Symbol(listeners)] / [Symbol(parentNode)]` object graph that
42
+ * scrolls for thousands of lines and never shows the markup. `outerHTML` is what
43
+ * someone reading a failure actually wants.
44
+ *
45
+ * ## What this deliberately does NOT do
46
+ *
47
+ * It does not touch `prettyDOM`, `screen.debug()` or any Testing Library output
48
+ * — those have their own serializer and are called only when someone asks for
49
+ * them, so they were never the cost. Only the implicit `expect`/`console.log`
50
+ * path changes.
51
+ */
52
+ /**
53
+ * Install the cheap inspector. Idempotent, and a no-op when happy-dom has not
54
+ * registered its globals (a pure-logic preload has no `Node`).
55
+ *
56
+ * Call AFTER `GlobalRegistrator.register()` — before it, `globalThis.Node` is
57
+ * whatever Bun ships and patching it would miss the class the specs actually get.
58
+ */
59
+ export declare function installCheapDomInspect(): void;
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Make a happy-dom node CHEAP to serialize, for every `bun test` process in this
3
+ * repo. Imported by `packages/cursedbelt/happydom.ts` and
4
+ * `packages/cursedshell/happydom.ts` — the two test preloads — so it is the ONE
5
+ * place this behavior is defined.
6
+ *
7
+ * ## The bug this exists to not have
8
+ *
9
+ * `expect(received)` builds its failure message EAGERLY, before anything decides
10
+ * whether that message will ever be read. When `received` is a happy-dom node,
11
+ * Bun's inspector walks the node's own object graph — and a happy-dom node holds
12
+ * `[Symbol(parentNode)]`, so the walk goes UPWARD through the parent chain into
13
+ * `HTMLBodyElement` → `HTMLHtmlElement` → `HTMLDocument` → the window, and back
14
+ * down through every sibling subtree it meets on the way.
15
+ *
16
+ * Measured on this repo's own studio spec (2026-07-26):
17
+ *
18
+ * | received value | cost of ONE failing `expect` |
19
+ * |---------------------------------------|------------------------------|
20
+ * | `true` | 0.1 ms |
21
+ * | a plain object | 0.0 ms |
22
+ * | a detached empty `<div>` | 9.8 ms |
23
+ * | an ATTACHED empty `<div>` | 29.8 ms |
24
+ * | an attached `<div>` with 50 children | **1,576 ms** |
25
+ *
26
+ * That cost is paid on assertions that FAIL, which sounds rare until you notice
27
+ * that `waitFor(() => expect(el).toBeNull())` fails on purpose on every poll
28
+ * until the condition comes true. The studio spec's `settled()` helper spent
29
+ * ~790 ms per mount building error strings that `waitFor` then threw away — 12
30
+ * mounts, ~9.5 s of a 10.5 s file, and on the CI runner the same two assertions
31
+ * blew through Bun's 5,000 ms per-test timeout and turned `main` red.
32
+ *
33
+ * ## The fix
34
+ *
35
+ * Install `nodejs.util.inspect.custom` on happy-dom's `Node.prototype` (the
36
+ * common ancestor of every element, text node, fragment and document), returning
37
+ * TRUNCATED `outerHTML`. Bun's inspector honors that hook and stops walking.
38
+ * Same measurement, after: **1,576 ms → 0.1 ms.**
39
+ *
40
+ * The message gets BETTER as well as faster. What it replaces is not an HTML
41
+ * dump; it is a `[Symbol(listeners)] / [Symbol(parentNode)]` object graph that
42
+ * scrolls for thousands of lines and never shows the markup. `outerHTML` is what
43
+ * someone reading a failure actually wants.
44
+ *
45
+ * ## What this deliberately does NOT do
46
+ *
47
+ * It does not touch `prettyDOM`, `screen.debug()` or any Testing Library output
48
+ * — those have their own serializer and are called only when someone asks for
49
+ * them, so they were never the cost. Only the implicit `expect`/`console.log`
50
+ * path changes.
51
+ */
52
+ /** Longest markup we will put in a failure message before eliding the middle. */
53
+ const MAX_MARKUP = 800;
54
+ /** Node.nodeType constants, spelled out so this file needs no DOM lib types. */
55
+ const ELEMENT_NODE = 1;
56
+ const TEXT_NODE = 3;
57
+ const COMMENT_NODE = 8;
58
+ const DOCUMENT_NODE = 9;
59
+ const DOCUMENT_FRAGMENT_NODE = 11;
60
+ const INSPECT = Symbol.for('nodejs.util.inspect.custom');
61
+ /** Keep the head and the tail: the opening tag says what it is, the closing tag
62
+ * says where it ended, and the middle of a 40 KB body is never the useful part. */
63
+ function elide(markup) {
64
+ if (markup.length <= MAX_MARKUP)
65
+ return markup;
66
+ const head = markup.slice(0, MAX_MARKUP - 120);
67
+ const tail = markup.slice(-100);
68
+ return `${head}\n … ${markup.length - MAX_MARKUP + 20} more chars …\n${tail}`;
69
+ }
70
+ function describe(node) {
71
+ switch (node.nodeType) {
72
+ case ELEMENT_NODE:
73
+ return elide(node.outerHTML ?? `<${(node.nodeName ?? 'element').toLowerCase()} />`);
74
+ case TEXT_NODE:
75
+ return `#text ${JSON.stringify(node.data ?? '')}`;
76
+ case COMMENT_NODE:
77
+ return `<!--${node.data ?? ''}-->`;
78
+ case DOCUMENT_NODE:
79
+ return '#document';
80
+ case DOCUMENT_FRAGMENT_NODE:
81
+ return elide(`#document-fragment ${node.innerHTML ?? `(${node.childNodes?.length ?? 0} children)`}`);
82
+ default:
83
+ return `${node.nodeName ?? 'Node'}(nodeType ${node.nodeType})`;
84
+ }
85
+ }
86
+ /**
87
+ * Install the cheap inspector. Idempotent, and a no-op when happy-dom has not
88
+ * registered its globals (a pure-logic preload has no `Node`).
89
+ *
90
+ * Call AFTER `GlobalRegistrator.register()` — before it, `globalThis.Node` is
91
+ * whatever Bun ships and patching it would miss the class the specs actually get.
92
+ */
93
+ export function installCheapDomInspect() {
94
+ const NodeCtor = globalThis.Node;
95
+ const proto = NodeCtor?.prototype;
96
+ if (!proto)
97
+ return;
98
+ if (Object.hasOwn(proto, INSPECT))
99
+ return; // already installed
100
+ Object.defineProperty(proto, INSPECT, {
101
+ value() {
102
+ // An inspector that throws turns a readable assertion failure into an
103
+ // unreadable one, so every branch here is best-effort.
104
+ try {
105
+ return describe(this);
106
+ }
107
+ catch {
108
+ return '[unserializable DOM node]';
109
+ }
110
+ },
111
+ configurable: true,
112
+ enumerable: false,
113
+ writable: true,
114
+ });
115
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A `fetch` stand-in that actually satisfies `typeof fetch`.
3
+ *
4
+ * `typeof fetch` is not just a call signature: the DOM lib declares `preconnect`
5
+ * on it, so a bare `async (input) => new Response(…)` is NOT assignable and
6
+ * `as typeof fetch` is refused outright ("neither type sufficiently overlaps").
7
+ * The reflexive workaround is `as unknown as typeof fetch`, and that is the
8
+ * pattern worth stopping: a double assertion silences THIS mismatch and every
9
+ * future one, including a genuine argument-shape error in the fake.
10
+ *
11
+ * This attaches the missing member instead. The result is a real value of the
12
+ * type, so the fake's own `(input, init)` signature stays checked — which is the
13
+ * whole reason to type a fake at all.
14
+ */
15
+ /** The part of `fetch` a test actually implements. */
16
+ export type FetchImpl = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
17
+ /**
18
+ * Wrap a fake so it is a complete `typeof fetch`.
19
+ *
20
+ * `preconnect` is a no-op: it is a resource HINT with no observable result, so a
21
+ * test that triggered one and got nothing has lost nothing.
22
+ */
23
+ export declare const fakeFetch: (impl: FetchImpl) => typeof fetch;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * A `fetch` stand-in that actually satisfies `typeof fetch`.
3
+ *
4
+ * `typeof fetch` is not just a call signature: the DOM lib declares `preconnect`
5
+ * on it, so a bare `async (input) => new Response(…)` is NOT assignable and
6
+ * `as typeof fetch` is refused outright ("neither type sufficiently overlaps").
7
+ * The reflexive workaround is `as unknown as typeof fetch`, and that is the
8
+ * pattern worth stopping: a double assertion silences THIS mismatch and every
9
+ * future one, including a genuine argument-shape error in the fake.
10
+ *
11
+ * This attaches the missing member instead. The result is a real value of the
12
+ * type, so the fake's own `(input, init)` signature stays checked — which is the
13
+ * whole reason to type a fake at all.
14
+ */
15
+ /**
16
+ * Wrap a fake so it is a complete `typeof fetch`.
17
+ *
18
+ * `preconnect` is a no-op: it is a resource HINT with no observable result, so a
19
+ * test that triggered one and got nothing has lost nothing.
20
+ */
21
+ export const fakeFetch = (impl) => Object.assign(impl, { preconnect: () => undefined });
@@ -0,0 +1,3 @@
1
+ export { installCheapDomInspect } from "./domInspect";
2
+ export { asBody, bytesResponse, bytesStream } from "./bytes";
3
+ export { type FetchImpl, fakeFetch } from "./fakeFetch";
@@ -0,0 +1,11 @@
1
+ // cursedbelt-core/testing — test-only helpers shared across every `bun test` process
2
+ // in the fleet. Its own subpath so nothing test-related is reachable from the
3
+ // runtime entrypoints.
4
+ //
5
+ // Exposed on 2026-08-02 so consumers (e.g. cursed-satellites' happydom preload)
6
+ // can `import { installCheapDomInspect } from "cursedbelt-core/testing"` instead of
7
+ // keeping a ~60-line hand-port of domInspect.ts — the file already lived here,
8
+ // it just had no export.
9
+ export { installCheapDomInspect } from "./domInspect";
10
+ export { asBody, bytesResponse, bytesStream } from "./bytes";
11
+ export { fakeFetch } from "./fakeFetch";