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,44 @@
1
+ import { type CtgrDecoded, type CtgrMethod, type EncodedRequest, type EncodeOpts, type UploadedFile } from './types';
2
+ /**
3
+ * Encode a mutating request into one or more `/ctgr<XX>` GET-twin chunks. Small
4
+ * bodies become a single chunk; large bodies gzip (when over the threshold) and split
5
+ * into `chunkSizeBytes` pieces. Each chunk carries a crc32; the last carries the
6
+ * full-payload sha256.
7
+ */
8
+ export declare function encodeRequest(method: CtgrMethod, url: string, body: unknown, opts?: EncodeOpts): Promise<EncodedRequest>;
9
+ /** A validated, crc-verified single chunk — ready to buffer or decode. */
10
+ export interface ParsedChunk {
11
+ method: CtgrMethod;
12
+ gzip: boolean;
13
+ index: number;
14
+ total: number;
15
+ /** Declared full-payload length in bytes (post-gzip, pre-base64). */
16
+ payloadLength: number;
17
+ cid: string;
18
+ /** Full-payload sha256 (hex) — present on the last chunk only. */
19
+ sha?: string;
20
+ bytes: Uint8Array;
21
+ }
22
+ /**
23
+ * Parse + crc-verify ONE chunk's params. Throws on an unknown version, a bad method
24
+ * abbrev, malformed index/total/length, or a crc mismatch — callers that must not
25
+ * throw (the chunk store, the middleware) catch and map to an Error/400.
26
+ */
27
+ export declare function parseChunk(params: Record<string, string> | URLSearchParams): ParsedChunk;
28
+ /**
29
+ * Split an `__files` array out of a decoded body into multer-shaped uploads. The
30
+ * `__files` data is standard base64 (what the client's FormData serializer emits);
31
+ * everything else stays on the returned `body`.
32
+ */
33
+ export declare function extractFiles(parsed: unknown): {
34
+ body: unknown;
35
+ files?: UploadedFile[];
36
+ };
37
+ /** Decode an assembled payload (gunzip when flagged) → method + body (+ files). */
38
+ export declare function decodePayload(payload: Uint8Array, gzipped: boolean, method: CtgrMethod): Promise<CtgrDecoded>;
39
+ /**
40
+ * Decode a single set of chunk params. For the common single-chunk request this
41
+ * fully decodes (verifying the payload sha256); for one chunk of a multi-chunk
42
+ * payload it returns `'partial'` — those reassemble through {@link ChunkStore}.
43
+ */
44
+ export declare function decodeRequest(params: Record<string, string> | URLSearchParams): Promise<CtgrDecoded | 'partial'>;
@@ -0,0 +1,153 @@
1
+ /*
2
+ * ctgr v1 codec — pack ANY mutating request body into URL-safe GET-twin chunks, and
3
+ * the pure inverse. Isomorphic (browser + Bun), no runtime deps.
4
+ *
5
+ * Pipeline: JSON → UTF-8 bytes → gzip when > threshold → split into chunks → each
6
+ * chunk base64url-encoded with a crc32, the envelope carrying a self-describing
7
+ * header (version, gzip flag, method, index/total, payload length, payload id) and a
8
+ * full-payload sha256 on the last chunk. The decoder needs nothing but the params.
9
+ *
10
+ * This replaces the legacy `encode→reverse→swap→base64→swap` chain, which
11
+ * double-base64'd and whose swap/reverse was trivially-reversible obfuscation, not
12
+ * protection — the real protection (getOnly JWT claim + CSRF guard) is unchanged.
13
+ */
14
+ import { fromBase64, fromBase64url, toBase64url } from './base64url';
15
+ import { gunzip, gzip } from './compress';
16
+ import { crc32 } from './crc32';
17
+ import { sha256Hex } from './digest';
18
+ import { ABBREV_TO_METHOD, CTGR_WIRE_VERSION, METHOD_TO_ABBREV, } from './types';
19
+ const DEFAULT_CHUNK_SIZE = 1800;
20
+ const DEFAULT_GZIP_THRESHOLD = 1024;
21
+ const textEncoder = new TextEncoder();
22
+ const textDecoder = new TextDecoder();
23
+ /**
24
+ * Encode a mutating request into one or more `/ctgr<XX>` GET-twin chunks. Small
25
+ * bodies become a single chunk; large bodies gzip (when over the threshold) and split
26
+ * into `chunkSizeBytes` pieces. Each chunk carries a crc32; the last carries the
27
+ * full-payload sha256.
28
+ */
29
+ export async function encodeRequest(method, url, body, opts = {}) {
30
+ const chunkSize = opts.chunkSizeBytes ?? DEFAULT_CHUNK_SIZE;
31
+ const gzipThreshold = opts.gzipThresholdBytes ?? DEFAULT_GZIP_THRESHOLD;
32
+ // `JSON.stringify(undefined)` is `undefined`; coerce a missing body to `null` so a
33
+ // body-less mutation (e.g. DELETE) still rides the normal, single-versioned path.
34
+ const raw = textEncoder.encode(JSON.stringify(body ?? null));
35
+ const useGzip = raw.length > gzipThreshold;
36
+ const payload = useGzip ? await gzip(raw) : raw;
37
+ const sha = await sha256Hex(payload);
38
+ const cid = crypto.randomUUID();
39
+ const abbrev = METHOD_TO_ABBREV[method];
40
+ const totalChunks = Math.max(1, Math.ceil(payload.length / chunkSize));
41
+ const chunks = [];
42
+ for (let i = 0; i < totalChunks; i++) {
43
+ const slice = payload.subarray(i * chunkSize, (i + 1) * chunkSize);
44
+ const params = {
45
+ _cv: CTGR_WIRE_VERSION,
46
+ _cm: abbrev,
47
+ _cg: useGzip ? '1' : '0',
48
+ _ci: String(i),
49
+ _ct: String(totalChunks),
50
+ _cl: String(payload.length),
51
+ _cid: cid,
52
+ _crc: String(crc32(slice)),
53
+ _d: toBase64url(slice),
54
+ };
55
+ if (i === totalChunks - 1)
56
+ params._sha = sha;
57
+ const path = `/ctgr${abbrev}${url}`;
58
+ const separator = path.includes('?') ? '&' : '?';
59
+ const query = new URLSearchParams(params).toString();
60
+ chunks.push({ url: `${path}${separator}${query}`, params });
61
+ }
62
+ return { chunks };
63
+ }
64
+ function asRecord(params) {
65
+ return params instanceof URLSearchParams ? Object.fromEntries(params) : params;
66
+ }
67
+ /**
68
+ * Parse + crc-verify ONE chunk's params. Throws on an unknown version, a bad method
69
+ * abbrev, malformed index/total/length, or a crc mismatch — callers that must not
70
+ * throw (the chunk store, the middleware) catch and map to an Error/400.
71
+ */
72
+ export function parseChunk(params) {
73
+ const p = asRecord(params);
74
+ if (p._cv !== CTGR_WIRE_VERSION) {
75
+ throw new Error(`ctgr: unsupported wire version "${p._cv ?? ''}"`);
76
+ }
77
+ const method = ABBREV_TO_METHOD[p._cm];
78
+ if (!method)
79
+ throw new Error(`ctgr: unknown method abbreviation "${p._cm ?? ''}"`);
80
+ const index = Number(p._ci);
81
+ const total = Number(p._ct);
82
+ const payloadLength = Number(p._cl);
83
+ if (!Number.isInteger(total) || total <= 0)
84
+ throw new Error('ctgr: invalid chunk total');
85
+ if (!Number.isInteger(index) || index < 0 || index >= total) {
86
+ throw new Error('ctgr: chunk index out of range');
87
+ }
88
+ if (!Number.isInteger(payloadLength) || payloadLength < 0) {
89
+ throw new Error('ctgr: invalid payload length');
90
+ }
91
+ const bytes = fromBase64url(p._d ?? '');
92
+ const expectedCrc = Number(p._crc);
93
+ if (!Number.isInteger(expectedCrc) || crc32(bytes) !== expectedCrc) {
94
+ throw new Error('ctgr: chunk crc mismatch');
95
+ }
96
+ return {
97
+ method,
98
+ gzip: p._cg === '1',
99
+ index,
100
+ total,
101
+ payloadLength,
102
+ cid: p._cid ?? '',
103
+ sha: p._sha,
104
+ bytes,
105
+ };
106
+ }
107
+ /**
108
+ * Split an `__files` array out of a decoded body into multer-shaped uploads. The
109
+ * `__files` data is standard base64 (what the client's FormData serializer emits);
110
+ * everything else stays on the returned `body`.
111
+ */
112
+ export function extractFiles(parsed) {
113
+ if (parsed &&
114
+ typeof parsed === 'object' &&
115
+ Array.isArray(parsed.__files)) {
116
+ const source = parsed;
117
+ const files = source.__files.map((f) => {
118
+ const buffer = fromBase64(f.dataBase64 ?? '');
119
+ return {
120
+ fieldname: f.field ?? 'file',
121
+ originalname: f.name ?? 'file',
122
+ mimetype: f.type || 'application/octet-stream',
123
+ buffer,
124
+ size: buffer.length,
125
+ };
126
+ });
127
+ const { __files, ...body } = source;
128
+ return { body, files };
129
+ }
130
+ return { body: parsed };
131
+ }
132
+ /** Decode an assembled payload (gunzip when flagged) → method + body (+ files). */
133
+ export async function decodePayload(payload, gzipped, method) {
134
+ const raw = gzipped ? await gunzip(payload) : payload;
135
+ const json = textDecoder.decode(raw);
136
+ const parsed = json.length ? JSON.parse(json) : undefined;
137
+ const { body, files } = extractFiles(parsed);
138
+ return files ? { method, body, files } : { method, body };
139
+ }
140
+ /**
141
+ * Decode a single set of chunk params. For the common single-chunk request this
142
+ * fully decodes (verifying the payload sha256); for one chunk of a multi-chunk
143
+ * payload it returns `'partial'` — those reassemble through {@link ChunkStore}.
144
+ */
145
+ export async function decodeRequest(params) {
146
+ const chunk = parseChunk(params);
147
+ if (chunk.total !== 1)
148
+ return 'partial';
149
+ if (chunk.sha && (await sha256Hex(chunk.bytes)) !== chunk.sha) {
150
+ throw new Error('ctgr: payload sha256 mismatch');
151
+ }
152
+ return decodePayload(chunk.bytes, chunk.gzip, chunk.method);
153
+ }
@@ -0,0 +1,2 @@
1
+ export declare function gzip(bytes: Uint8Array): Promise<Uint8Array>;
2
+ export declare function gunzip(bytes: Uint8Array): Promise<Uint8Array>;
@@ -0,0 +1,20 @@
1
+ /*
2
+ * Isomorphic gzip via the web-standard CompressionStream / DecompressionStream —
3
+ * native on Bun and in the browser, so one implementation serves both the browser
4
+ * encoder and the Bun server decoder (no `node:zlib`, no `Bun.gzipSync` branch).
5
+ * Async on both ends, which is why the whole ctgr codec is async.
6
+ */
7
+ // `new Uint8Array(bytes)` re-backs the view on a plain ArrayBuffer — Blob/SubtleCrypto
8
+ // reject the `ArrayBufferLike` default (which admits SharedArrayBuffer) under TS 6.
9
+ export async function gzip(bytes) {
10
+ const stream = new Blob([new Uint8Array(bytes)])
11
+ .stream()
12
+ .pipeThrough(new CompressionStream('gzip'));
13
+ return new Uint8Array(await new Response(stream).arrayBuffer());
14
+ }
15
+ export async function gunzip(bytes) {
16
+ const stream = new Blob([new Uint8Array(bytes)])
17
+ .stream()
18
+ .pipeThrough(new DecompressionStream('gzip'));
19
+ return new Uint8Array(await new Response(stream).arrayBuffer());
20
+ }
@@ -0,0 +1,2 @@
1
+ /** CRC-32 of `bytes` as an unsigned 32-bit integer. */
2
+ export declare function crc32(bytes: Uint8Array): number;
@@ -0,0 +1,23 @@
1
+ /*
2
+ * Pure-JS CRC-32 (IEEE 802.3 / zlib polynomial) over a 256-entry lookup table — no
3
+ * native addon. Client and server share this exact implementation so a chunk's
4
+ * `_crc` integrity check computes the same value on both ends of the tunnel.
5
+ */
6
+ const TABLE = (() => {
7
+ const table = new Uint32Array(256);
8
+ for (let n = 0; n < 256; n++) {
9
+ let c = n;
10
+ for (let k = 0; k < 8; k++)
11
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
12
+ table[n] = c >>> 0;
13
+ }
14
+ return table;
15
+ })();
16
+ /** CRC-32 of `bytes` as an unsigned 32-bit integer. */
17
+ export function crc32(bytes) {
18
+ let crc = 0xffffffff;
19
+ for (let i = 0; i < bytes.length; i++) {
20
+ crc = (crc >>> 8) ^ TABLE[(crc ^ bytes[i]) & 0xff];
21
+ }
22
+ return (crc ^ 0xffffffff) >>> 0;
23
+ }
@@ -0,0 +1 @@
1
+ export declare function sha256Hex(bytes: Uint8Array): Promise<string>;
@@ -0,0 +1,15 @@
1
+ /*
2
+ * Hex SHA-256 via web-standard SubtleCrypto — isomorphic (Bun + browser both expose
3
+ * `crypto.subtle`). Used for the full-payload integrity check the wire format carries
4
+ * on the last chunk (`_sha`); per-chunk integrity is the cheaper {@link crc32}.
5
+ */
6
+ export async function sha256Hex(bytes) {
7
+ // `new Uint8Array(bytes)` re-backs the view on a plain ArrayBuffer — SubtleCrypto's
8
+ // BufferSource rejects the `ArrayBufferLike` default under TS 6.
9
+ const digest = await crypto.subtle.digest('SHA-256', new Uint8Array(bytes));
10
+ const view = new Uint8Array(digest);
11
+ let hex = '';
12
+ for (let i = 0; i < view.length; i++)
13
+ hex += view[i].toString(16).padStart(2, '0');
14
+ return hex;
15
+ }
@@ -0,0 +1,9 @@
1
+ export { fromBase64, fromBase64url, toBase64, toBase64url } from './base64url';
2
+ export { ChunkAccumulator, type ChunkAccumulatorConfig, } from './chunkAccumulator';
3
+ export { ChunkStore, type ChunkStoreOpts } from './chunkStore';
4
+ export { decodePayload, decodeRequest, encodeRequest, extractFiles, type ParsedChunk, parseChunk, } from './codec';
5
+ export { gunzip, gzip } from './compress';
6
+ export { crc32 } from './crc32';
7
+ export { sha256Hex } from './digest';
8
+ export { ABBREV_TO_METHOD, CTGR_WIRE_VERSION, type CtgrAbbrev, type CtgrDecoded, type CtgrMethod, type EncodedChunk, type EncodedRequest, type EncodeOpts, METHOD_TO_ABBREV, type SerializedFile, type UploadedFile, } from './types';
9
+ export { decodeV0Payload, V0ChunkStore, type V0ChunkStoreOpts } from './v0compat';
@@ -0,0 +1,17 @@
1
+ /*
2
+ * src/ctgr — the isomorphic ctgr codec, re-exported from `cursedbelt-server`; (browser + Bun, no runtime deps). The
3
+ * Hono transport binding (`unGETify` + `buildRoutes`) lives in `cursedbelt-server`;
4
+ * the browser guard + request converter live in `cursedbelt-cc/client`.
5
+ *
6
+ * The ChunkAccumulator is the primary abstraction for multi-chunk payload assembly;
7
+ * ChunkStore is kept for backward compatibility (they are identical implementations).
8
+ */
9
+ export { fromBase64, fromBase64url, toBase64, toBase64url } from './base64url';
10
+ export { ChunkAccumulator, } from './chunkAccumulator';
11
+ export { ChunkStore } from './chunkStore';
12
+ export { decodePayload, decodeRequest, encodeRequest, extractFiles, parseChunk, } from './codec';
13
+ export { gunzip, gzip } from './compress';
14
+ export { crc32 } from './crc32';
15
+ export { sha256Hex } from './digest';
16
+ export { ABBREV_TO_METHOD, CTGR_WIRE_VERSION, METHOD_TO_ABBREV, } from './types';
17
+ export { decodeV0Payload, V0ChunkStore } from './v0compat';
@@ -0,0 +1,64 @@
1
+ /** The mutating HTTP methods ctgr tunnels through a `/ctgr<XX>` GET twin. */
2
+ export type CtgrMethod = 'POST' | 'PATCH' | 'PUT' | 'DELETE';
3
+ /** Method → its 2-char abbreviation (the `/ctgr<XX>` path segment + the `_cm` param). */
4
+ export declare const METHOD_TO_ABBREV: {
5
+ readonly POST: "po";
6
+ readonly PATCH: "pa";
7
+ readonly PUT: "pu";
8
+ readonly DELETE: "de";
9
+ };
10
+ /** The inverse of {@link METHOD_TO_ABBREV}. */
11
+ export declare const ABBREV_TO_METHOD: {
12
+ readonly po: "POST";
13
+ readonly pa: "PATCH";
14
+ readonly pu: "PUT";
15
+ readonly de: "DELETE";
16
+ };
17
+ export type CtgrAbbrev = keyof typeof ABBREV_TO_METHOD;
18
+ /** Current wire-format version. v0 (the legacy `_cv`-absent codec) lives in `v0compat.ts`. */
19
+ export declare const CTGR_WIRE_VERSION = "1";
20
+ /**
21
+ * A reconstructed multipart upload, multer-shaped so the file came over the ctgr
22
+ * GET-tunnel the same way it would over multipart. `buffer` is raw isomorphic bytes
23
+ * (a `Uint8Array`, not a Node `Buffer`) — wrap it where a `Buffer` is required.
24
+ */
25
+ export interface UploadedFile {
26
+ fieldname: string;
27
+ originalname: string;
28
+ mimetype: string;
29
+ buffer: Uint8Array;
30
+ size: number;
31
+ }
32
+ /** The fully-decoded request a downstream handler / validator consumes. */
33
+ export interface CtgrDecoded {
34
+ /** The original HTTP method the GET twin stands in for. */
35
+ method: CtgrMethod;
36
+ /** The parsed JSON body, with any `__files` array extracted into {@link files}. */
37
+ body: unknown;
38
+ /** Reconstructed uploads — present only when the body carried an `__files` array. */
39
+ files?: UploadedFile[];
40
+ }
41
+ /** One encoded chunk: the GET-twin URL plus the query params it carries (same data). */
42
+ export interface EncodedChunk {
43
+ /** `/ctgr<XX><path>?<params>` — a path (no origin); the caller adds any prefix. */
44
+ url: string;
45
+ /** The `_c*` params, also serialized into {@link url}. Handy for tests + reassembly. */
46
+ params: Record<string, string>;
47
+ }
48
+ /** The output of `encodeRequest`: one chunk for small bodies, N for large ones. */
49
+ export interface EncodedRequest {
50
+ chunks: EncodedChunk[];
51
+ }
52
+ export interface EncodeOpts {
53
+ /** Max payload bytes per chunk (pre-base64url). Default 1800 → ~2400 URL chars. */
54
+ chunkSizeBytes?: number;
55
+ /** Gzip the payload when it exceeds this many bytes. Default 1024. */
56
+ gzipThresholdBytes?: number;
57
+ }
58
+ /** The `__files` entry shape the client serializes an upload into (base64 data). */
59
+ export interface SerializedFile {
60
+ field?: string;
61
+ name?: string;
62
+ type?: string;
63
+ dataBase64?: string;
64
+ }
@@ -0,0 +1,21 @@
1
+ /*
2
+ * ctgr wire-format types + the method⇄abbreviation maps shared by the codec, the
3
+ * chunk store, the Hono transport, and the browser client. Pure types + literal
4
+ * const maps only (no runtime APIs) so this module is trivially isomorphic.
5
+ */
6
+ /** Method → its 2-char abbreviation (the `/ctgr<XX>` path segment + the `_cm` param). */
7
+ export const METHOD_TO_ABBREV = {
8
+ POST: 'po',
9
+ PATCH: 'pa',
10
+ PUT: 'pu',
11
+ DELETE: 'de',
12
+ };
13
+ /** The inverse of {@link METHOD_TO_ABBREV}. */
14
+ export const ABBREV_TO_METHOD = {
15
+ po: 'POST',
16
+ pa: 'PATCH',
17
+ pu: 'PUT',
18
+ de: 'DELETE',
19
+ };
20
+ /** Current wire-format version. v0 (the legacy `_cv`-absent codec) lives in `v0compat.ts`. */
21
+ export const CTGR_WIRE_VERSION = '1';
@@ -0,0 +1,27 @@
1
+ import type { CtgrDecoded, CtgrMethod } from './types';
2
+ /** Reverse the v0 client pipeline (`encode→reverse→swap→encode→swap`) and parse JSON. */
3
+ export declare function decodeV0Payload(encoded: string): unknown;
4
+ export interface V0ChunkStoreOpts {
5
+ maxInFlight?: number;
6
+ /** Hard cap on the joined encoded string length (chars). Default ~80 MB. */
7
+ maxPayloadChars?: number;
8
+ ttlMs?: number;
9
+ now?: () => number;
10
+ }
11
+ /**
12
+ * Bounded accumulator for v0 requests, mirroring the legacy `unGETifyRoute` (`p` for
13
+ * a single payload; `cid`/`ci`/`ct`/`p` for chunked). Same `add` contract as the v1
14
+ * {@link ChunkStore}: `false` for an intermediate chunk, {@link CtgrDecoded} when
15
+ * complete, `Error` on malformed input.
16
+ */
17
+ export declare class V0ChunkStore {
18
+ private readonly entries;
19
+ private readonly maxInFlight;
20
+ private readonly maxPayloadChars;
21
+ private readonly ttlMs;
22
+ private readonly now;
23
+ constructor(opts?: V0ChunkStoreOpts);
24
+ get size(): number;
25
+ add(params: Record<string, string>, method: CtgrMethod): CtgrDecoded | false | Error;
26
+ private evictStale;
27
+ }
@@ -0,0 +1,128 @@
1
+ /*
2
+ * Legacy (v0) ctgr decoder — kept ONLY for the rollout window so a server running v1
3
+ * still understands requests from not-yet-updated clients (which send no `_cv`). The
4
+ * v0 codec was `encode→reverse→swap→base64→swap`: a trivially reversible transform,
5
+ * never a security mechanism (the real protection is the getOnly JWT claim + the CSRF
6
+ * guard, both unchanged). Delete this file once all clients ship v1.
7
+ *
8
+ * Detect v0 by the ABSENCE of `_cv` (the old client never sent it) — the safest
9
+ * heuristic, since v1 always sets `_cv=1`. v0 carried the method only in the path
10
+ * (`/ctgr<XX>`), so the middleware passes the path-derived method in.
11
+ */
12
+ import { fromBase64 } from './base64url';
13
+ import { extractFiles } from './codec';
14
+ const decodeBase64Utf8 = (str) => new TextDecoder().decode(fromBase64(str));
15
+ const reverseString = (str) => str.split('').reverse().join('');
16
+ const swapEnds = (str) => {
17
+ if (str.length < 2)
18
+ return str;
19
+ const arr = str.split('');
20
+ arr[0] = str[str.length - 1];
21
+ arr[str.length - 1] = str[0];
22
+ return arr.join('');
23
+ };
24
+ /** Reverse the v0 client pipeline (`encode→reverse→swap→encode→swap`) and parse JSON. */
25
+ export function decodeV0Payload(encoded) {
26
+ const json = decodeBase64Utf8(reverseString(swapEnds(decodeBase64Utf8(swapEnds(encoded)))));
27
+ let body = JSON.parse(json);
28
+ // The legacy client occasionally double-encoded the body string; unwrap it.
29
+ if (typeof body === 'string')
30
+ body = JSON.parse(body);
31
+ return body;
32
+ }
33
+ function toDecoded(body, method) {
34
+ const { body: rest, files } = extractFiles(body);
35
+ return files ? { method, body: rest, files } : { method, body: rest };
36
+ }
37
+ /**
38
+ * Bounded accumulator for v0 requests, mirroring the legacy `unGETifyRoute` (`p` for
39
+ * a single payload; `cid`/`ci`/`ct`/`p` for chunked). Same `add` contract as the v1
40
+ * {@link ChunkStore}: `false` for an intermediate chunk, {@link CtgrDecoded} when
41
+ * complete, `Error` on malformed input.
42
+ */
43
+ export class V0ChunkStore {
44
+ entries = new Map();
45
+ maxInFlight;
46
+ maxPayloadChars;
47
+ ttlMs;
48
+ now;
49
+ constructor(opts = {}) {
50
+ this.maxInFlight = opts.maxInFlight ?? 200;
51
+ this.maxPayloadChars = opts.maxPayloadChars ?? 80 * 1024 * 1024;
52
+ this.ttlMs = opts.ttlMs ?? 2 * 60 * 1000;
53
+ this.now = opts.now ?? Date.now;
54
+ }
55
+ get size() {
56
+ return this.entries.size;
57
+ }
58
+ add(params, method) {
59
+ this.evictStale();
60
+ // Single payload (or a body-less converted request: no `p`).
61
+ if (params.cid === undefined) {
62
+ if (!params.p)
63
+ return { method, body: undefined };
64
+ try {
65
+ return toDecoded(decodeV0Payload(params.p), method);
66
+ }
67
+ catch (err) {
68
+ return asError(err);
69
+ }
70
+ }
71
+ // Chunked payload.
72
+ const cid = params.cid;
73
+ const total = Number(params.ct);
74
+ const index = Number(params.ci);
75
+ if (!Number.isInteger(total) ||
76
+ total <= 0 ||
77
+ !Number.isInteger(index) ||
78
+ index < 0 ||
79
+ index >= total) {
80
+ return new Error('ctgr v0: invalid chunk metadata');
81
+ }
82
+ let entry = this.entries.get(cid);
83
+ if (!entry) {
84
+ if (this.entries.size >= this.maxInFlight) {
85
+ return new Error('ctgr v0: too many in-flight payloads');
86
+ }
87
+ entry = {
88
+ parts: new Array(total),
89
+ total,
90
+ received: 0,
91
+ chars: 0,
92
+ method,
93
+ updatedAt: this.now(),
94
+ };
95
+ this.entries.set(cid, entry);
96
+ }
97
+ entry.updatedAt = this.now();
98
+ const piece = params.p ?? '';
99
+ if (entry.parts[index] === undefined) {
100
+ entry.parts[index] = piece;
101
+ entry.received += 1;
102
+ entry.chars += piece.length;
103
+ if (entry.chars > this.maxPayloadChars) {
104
+ this.entries.delete(cid);
105
+ return new Error('ctgr v0: payload exceeds size cap');
106
+ }
107
+ }
108
+ if (entry.received < entry.total)
109
+ return false;
110
+ this.entries.delete(cid);
111
+ try {
112
+ return toDecoded(decodeV0Payload(entry.parts.join('')), method);
113
+ }
114
+ catch (err) {
115
+ return asError(err);
116
+ }
117
+ }
118
+ evictStale() {
119
+ const cutoff = this.now() - this.ttlMs;
120
+ for (const [cid, entry] of this.entries) {
121
+ if (entry.updatedAt < cutoff)
122
+ this.entries.delete(cid);
123
+ }
124
+ }
125
+ }
126
+ function asError(err) {
127
+ return err instanceof Error ? err : new Error(String(err));
128
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * cursedbelt-core — the framework-agnostic bottom of the cursedbelt split
3
+ * (task 148, 2026-09-15). Isomorphic domain logic only: no react, no hono,
4
+ * no DOM requirement at runtime. `cwip` sits below this package;
5
+ * `cursedbelt-server` and `cursedbelt` (the React design system) sit above it
6
+ * and share their domain models through it by published version.
7
+ *
8
+ * The real surface is the subpath exports (`cursedbelt-core/sharing`,
9
+ * `cursedbelt-core/master-lock`, …) — this root barrel stays deliberately
10
+ * tiny because a fat root barrel is what made the pre-split `cursedbelt`
11
+ * drag 47 dependencies into every consumer.
12
+ */
13
+ export declare const VERSION = "1.0.0";
14
+ export { contrastText } from './core/color';
15
+ export { isSiteCopyKey, SITE_COPY_BAKED_ONLY, SITE_COPY_DEFAULTS, SITE_COPY_KEYS, SITE_COPY_MAX_VALUE_LENGTH, type SiteCopyKey, } from './core/site/siteCopy';
package/dist/index.js ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * cursedbelt-core — the framework-agnostic bottom of the cursedbelt split
3
+ * (task 148, 2026-09-15). Isomorphic domain logic only: no react, no hono,
4
+ * no DOM requirement at runtime. `cwip` sits below this package;
5
+ * `cursedbelt-server` and `cursedbelt` (the React design system) sit above it
6
+ * and share their domain models through it by published version.
7
+ *
8
+ * The real surface is the subpath exports (`cursedbelt-core/sharing`,
9
+ * `cursedbelt-core/master-lock`, …) — this root barrel stays deliberately
10
+ * tiny because a fat root barrel is what made the pre-split `cursedbelt`
11
+ * drag 47 dependencies into every consumer.
12
+ */
13
+ export const VERSION = '1.0.0';
14
+ // The two root-level conveniences the pre-split "." barrel actually served:
15
+ // the WCAG readable-foreground picker and the baked marketing-copy contract.
16
+ export { contrastText } from './core/color';
17
+ export { isSiteCopyKey, SITE_COPY_BAKED_ONLY, SITE_COPY_DEFAULTS, SITE_COPY_KEYS, SITE_COPY_MAX_VALUE_LENGTH, } from './core/site/siteCopy';