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,156 @@
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
+ import type { CtgrDecoded, CtgrMethod } from './types';
15
+
16
+ const decodeBase64Utf8 = (str: string): string => new TextDecoder().decode(fromBase64(str));
17
+ const reverseString = (str: string): string => str.split('').reverse().join('');
18
+ const swapEnds = (str: string): string => {
19
+ if (str.length < 2) return str;
20
+ const arr = str.split('');
21
+ arr[0] = str[str.length - 1];
22
+ arr[str.length - 1] = str[0];
23
+ return arr.join('');
24
+ };
25
+
26
+ /** Reverse the v0 client pipeline (`encode→reverse→swap→encode→swap`) and parse JSON. */
27
+ export function decodeV0Payload(encoded: string): unknown {
28
+ const json = decodeBase64Utf8(reverseString(swapEnds(decodeBase64Utf8(swapEnds(encoded)))));
29
+ let body = JSON.parse(json);
30
+ // The legacy client occasionally double-encoded the body string; unwrap it.
31
+ if (typeof body === 'string') body = JSON.parse(body);
32
+ return body;
33
+ }
34
+
35
+ function toDecoded(body: unknown, method: CtgrMethod): CtgrDecoded {
36
+ const { body: rest, files } = extractFiles(body);
37
+ return files ? { method, body: rest, files } : { method, body: rest };
38
+ }
39
+
40
+ export interface V0ChunkStoreOpts {
41
+ maxInFlight?: number;
42
+ /** Hard cap on the joined encoded string length (chars). Default ~80 MB. */
43
+ maxPayloadChars?: number;
44
+ ttlMs?: number;
45
+ now?: () => number;
46
+ }
47
+
48
+ interface V0Entry {
49
+ parts: (string | undefined)[];
50
+ total: number;
51
+ received: number;
52
+ chars: number;
53
+ method: CtgrMethod;
54
+ updatedAt: number;
55
+ }
56
+
57
+ /**
58
+ * Bounded accumulator for v0 requests, mirroring the legacy `unGETifyRoute` (`p` for
59
+ * a single payload; `cid`/`ci`/`ct`/`p` for chunked). Same `add` contract as the v1
60
+ * {@link ChunkStore}: `false` for an intermediate chunk, {@link CtgrDecoded} when
61
+ * complete, `Error` on malformed input.
62
+ */
63
+ export class V0ChunkStore {
64
+ private readonly entries = new Map<string, V0Entry>();
65
+ private readonly maxInFlight: number;
66
+ private readonly maxPayloadChars: number;
67
+ private readonly ttlMs: number;
68
+ private readonly now: () => number;
69
+
70
+ constructor(opts: V0ChunkStoreOpts = {}) {
71
+ this.maxInFlight = opts.maxInFlight ?? 200;
72
+ this.maxPayloadChars = opts.maxPayloadChars ?? 80 * 1024 * 1024;
73
+ this.ttlMs = opts.ttlMs ?? 2 * 60 * 1000;
74
+ this.now = opts.now ?? Date.now;
75
+ }
76
+
77
+ get size(): number {
78
+ return this.entries.size;
79
+ }
80
+
81
+ add(params: Record<string, string>, method: CtgrMethod): CtgrDecoded | false | Error {
82
+ this.evictStale();
83
+
84
+ // Single payload (or a body-less converted request: no `p`).
85
+ if (params.cid === undefined) {
86
+ if (!params.p) return { method, body: undefined };
87
+ try {
88
+ return toDecoded(decodeV0Payload(params.p), method);
89
+ } catch (err) {
90
+ return asError(err);
91
+ }
92
+ }
93
+
94
+ // Chunked payload.
95
+ const cid = params.cid;
96
+ const total = Number(params.ct);
97
+ const index = Number(params.ci);
98
+ if (
99
+ !Number.isInteger(total) ||
100
+ total <= 0 ||
101
+ !Number.isInteger(index) ||
102
+ index < 0 ||
103
+ index >= total
104
+ ) {
105
+ return new Error('ctgr v0: invalid chunk metadata');
106
+ }
107
+
108
+ let entry = this.entries.get(cid);
109
+ if (!entry) {
110
+ if (this.entries.size >= this.maxInFlight) {
111
+ return new Error('ctgr v0: too many in-flight payloads');
112
+ }
113
+ entry = {
114
+ parts: new Array(total),
115
+ total,
116
+ received: 0,
117
+ chars: 0,
118
+ method,
119
+ updatedAt: this.now(),
120
+ };
121
+ this.entries.set(cid, entry);
122
+ }
123
+ entry.updatedAt = this.now();
124
+
125
+ const piece = params.p ?? '';
126
+ if (entry.parts[index] === undefined) {
127
+ entry.parts[index] = piece;
128
+ entry.received += 1;
129
+ entry.chars += piece.length;
130
+ if (entry.chars > this.maxPayloadChars) {
131
+ this.entries.delete(cid);
132
+ return new Error('ctgr v0: payload exceeds size cap');
133
+ }
134
+ }
135
+
136
+ if (entry.received < entry.total) return false;
137
+
138
+ this.entries.delete(cid);
139
+ try {
140
+ return toDecoded(decodeV0Payload(entry.parts.join('')), method);
141
+ } catch (err) {
142
+ return asError(err);
143
+ }
144
+ }
145
+
146
+ private evictStale(): void {
147
+ const cutoff = this.now() - this.ttlMs;
148
+ for (const [cid, entry] of this.entries) {
149
+ if (entry.updatedAt < cutoff) this.entries.delete(cid);
150
+ }
151
+ }
152
+ }
153
+
154
+ function asError(err: unknown): Error {
155
+ return err instanceof Error ? err : new Error(String(err));
156
+ }
@@ -0,0 +1,87 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import pkg from '../package.json';
3
+
4
+ /**
5
+ * Every declared dependency must be reachable from an import (2026-09-13).
6
+ *
7
+ * `dependencies` is not advice — a consumer installs all of it to render one
8
+ * button. `peerDependencies` is a promise that installing the peer lights a
9
+ * feature up. Both rot silently: a component gets deleted, its package stays
10
+ * declared, and nothing anywhere reds. At the time this was written the manifest
11
+ * carried three runtime deps and five optional peers that no file imported.
12
+ *
13
+ * 🔴 The reason this matches an IMPORT SPECIFIER and scans EVERY file type,
14
+ * rather than grepping .ts for the bare name:
15
+ *
16
+ * - A .ts-only grep says `tw-animate-css` is dead. It is not — it is pulled in
17
+ * from CSS (`@import` in src/styles.css), and deleting it would have shipped
18
+ * a package whose stylesheet cannot resolve. That is the exact mistake this
19
+ * session made on its first pass.
20
+ * - A bare-name grep says everything is alive, because names like `diff` and
21
+ * `marked` appear in prose in hundreds of files. A gate that cannot fail is
22
+ * not a gate.
23
+ *
24
+ * devDependencies are deliberately NOT checked: several are installed only so
25
+ * this repo can typecheck and test an OPTIONAL peer's integration, so "nothing
26
+ * imports it by name" is the expected state there, not a defect.
27
+ */
28
+ const ROOTS = ['src', 'scripts'] as const;
29
+
30
+ // Reading a font or a screenshot as text costs time and can never match.
31
+ const BINARY = /\.(png|jpe?g|gif|webp|avif|ico|mp4|webm|mp3|wav|pdf|zip|gz|woff2?|ttf|otf|eot|wasm)$/i;
32
+
33
+ /**
34
+ * `from 'x'` · `import 'x/y.css'` · `import('x')` · `require('x')` · `@import "x"`,
35
+ * with or without a subpath. Anchored on the specifier quote so a name mentioned
36
+ * in prose or in a comment does not count as a use.
37
+ */
38
+ const RE_SPECIAL = /[.*+?^${}()|[\]\\]/g;
39
+
40
+ const importsPackage = (name: string): RegExp =>
41
+ new RegExp(`(?:from|import|require|@import)\\s*\\(?\\s*['"]${name.replace(RE_SPECIAL, '\\$&')}(?:/[^'"]*)?['"]`);
42
+
43
+ // 🔴 Bun.Glob + Bun.file, NOT node:fs — this package's test preload virtualizes
44
+ // node:fs, so a readdirSync/readFileSync scan reads MOCK data and passes against
45
+ // files it never opened. Same trap as publishShape.spec.ts; see its header.
46
+ let cache: Promise<string[]> | undefined;
47
+ const sources = (): Promise<string[]> => {
48
+ cache ??= (async () => {
49
+ const repo = `${import.meta.dir}/..`;
50
+ const texts: string[] = [];
51
+ for (const root of ROOTS) {
52
+ const dir = `${repo}/${root}`;
53
+ for (const rel of new Bun.Glob('**/*').scanSync({ cwd: dir, onlyFiles: true })) {
54
+ if (BINARY.test(rel)) continue;
55
+ texts.push(await Bun.file(`${dir}/${rel}`).text());
56
+ }
57
+ }
58
+ return texts;
59
+ })();
60
+ return cache;
61
+ };
62
+
63
+ const unimported = async (block: Record<string, string> | undefined): Promise<string[]> => {
64
+ const texts = await sources();
65
+ return Object.keys(block ?? {}).filter((name) => {
66
+ const re = importsPackage(name);
67
+ return !texts.some((t) => re.test(t));
68
+ });
69
+ };
70
+
71
+ describe('declared dependencies are imported', () => {
72
+ it('has scanned a real source tree', async () => {
73
+ // Guards the whole file: if the glob or the fs mock ever returns nothing,
74
+ // every assertion below passes vacuously and the gate quietly dies.
75
+ expect((await sources()).length).toBeGreaterThan(100);
76
+ });
77
+
78
+ for (const field of ['dependencies', 'peerDependencies'] as const) {
79
+ it(`${field} names nothing the code never imports`, async () => {
80
+ const dead = await unimported((pkg as Record<string, unknown>)[field] as Record<string, string>);
81
+ expect(
82
+ dead,
83
+ `declared in ${field} but imported nowhere in ${ROOTS.join('/')} — import it or drop it:\n ${dead.join('\n ')}`,
84
+ ).toEqual([]);
85
+ });
86
+ }
87
+ });
package/src/index.ts ADDED
@@ -0,0 +1,25 @@
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
+
15
+ // The two root-level conveniences the pre-split "." barrel actually served:
16
+ // the WCAG readable-foreground picker and the baked marketing-copy contract.
17
+ export { contrastText } from './core/color';
18
+ export {
19
+ isSiteCopyKey,
20
+ SITE_COPY_BAKED_ONLY,
21
+ SITE_COPY_DEFAULTS,
22
+ SITE_COPY_KEYS,
23
+ SITE_COPY_MAX_VALUE_LENGTH,
24
+ type SiteCopyKey,
25
+ } from './core/site/siteCopy';
@@ -0,0 +1,287 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { readdirSync, rmSync, statSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+ import pkg from '../package.json';
5
+
6
+ /**
7
+ * A LEAF subpath's whole promise is what it does NOT pull in. Nothing in this repo
8
+ * measured that until now (2026-09-15), so the promise was kept by hand — and it was
9
+ * broken twice in two days, in the same shape, one level apart.
10
+ *
11
+ * ## The measurement this exists because of
12
+ *
13
+ * `apps/collections/src/kit/binaryStore.ts` needed exactly one thing from here:
14
+ *
15
+ * import type { FileTokenClaims } from "cursedbelt/server/storage";
16
+ *
17
+ * A TYPE — TypeScript erases it, so the app should have paid nothing at runtime. What it
18
+ * actually paid was:
19
+ *
20
+ * error: Cannot find package 'otplib' from
21
+ * …/node_modules/cursedbelt/src/server/auth/totp.ts
22
+ *
23
+ * …and four unrelated route suites died before anything was measured. `server/storage`'s
24
+ * index is a barrel; the barrel reached `streamToken.ts`, which imported the `../auth`
25
+ * INDEX for two jose functions, and that index reaches `totp.ts` → `otplib`, an optional
26
+ * peer. (`sessionStore.ts` takes its Kysely db as a TYPE, so the `kysely` half of the same
27
+ * trap arrives through the `./server` barrel's `db/kysely.ts` instead — measured, not
28
+ * assumed: bundling `./server/storage` externals before this change listed `otplib` and
29
+ * `hono/cookie`, never `kysely`.) The app declared `otplib` as a direct dependency to get
30
+ * its route suites back — a one-time-password library installed to satisfy a claim shape.
31
+ *
32
+ * That is the exact shape 2.5.1 shipped `./server/login-throttle` to fix, one level
33
+ * deeper, and the rule written down after it ("never import the `cursedbelt/server`
34
+ * barrel for one primitive") could not catch it, because `./server/storage` IS a leaf by
35
+ * that rule's standard and has a barrel of its own. A rule that only a human can apply
36
+ * gets re-broken by the next module that looks like a leaf. So: a check.
37
+ *
38
+ * ## What it does
39
+ *
40
+ * Assembles a throwaway consumer under `$TMPDIR` — a `node_modules/cursedbelt` holding
41
+ * this package's REAL package.json (so the real `exports` map is what resolves) and
42
+ * nothing else but the leaf source files themselves — then spawns bun in it and imports
43
+ * each leaf by its PUBLIC specifier. No peers are installed, no other file of this package
44
+ * is even on disk, so any import that survives transpilation fails the import and this
45
+ * spec with it. `import type` is erased and costs nothing, which is the whole point:
46
+ * `verbatimModuleSyntax` is on, so a type import that is not written `import type` is
47
+ * already a typecheck error, and source and `dist` therefore carry the same module graph.
48
+ *
49
+ * ## 🔴 The trap that makes the naive version of this check a lie
50
+ *
51
+ * Bun AUTO-INSTALLS a missing bare import out of `~/.bun/install/cache` when the walk up
52
+ * from the importing file finds NO `node_modules` at all. Measured while building this, in
53
+ * a scratch directory holding two files and nothing else: `import { authenticator } from
54
+ * 'otplib'` resolved — from
55
+ * `~/.bun/install/cache/otplib@13.5.0@@@1/dist/index.js` — and the only error was a missing
56
+ * named export. An "empty fixture" therefore proves NOTHING about what a leaf drags: every
57
+ * peer in the cache answers.
58
+ *
59
+ * Two things hold the line, and the second is the one that matters:
60
+ *
61
+ * 1. `bunfig.toml` with `[install] auto = "disable"` in the fixture root. Note what this
62
+ * is and is not: with a `node_modules/` present (as here) bun does not auto-install
63
+ * anyway — removing the line and re-running left the negative control failing exactly
64
+ * as before. It is the second lock, for the day this fixture is refactored into one
65
+ * that has no `node_modules` and the trap silently comes back.
66
+ * 2. The NEGATIVE CONTROL: a file importing `otplib` and nothing else is copied in beside
67
+ * the leaves, and this spec fails if importing it SUCCEEDS. Whatever makes peers
68
+ * resolvable — auto-install, a `node_modules` above the scratch root, a future bun
69
+ * default — the control sees it and reds, instead of letting every assertion below
70
+ * pass vacuously.
71
+ *
72
+ * ## 🔴 Verified failing, 2026-09-15, before it was trusted — every branch
73
+ *
74
+ * · `import 'otplib'` added to `src/server/storage/types.ts`
75
+ * → "cursedbelt/server/storage/types must import nothing at runtime … Cannot find
76
+ * package 'otplib' imported from …/node_modules/cursedbelt/src/server/storage/types.ts"
77
+ * · the same, but `import { createStreamTokenService } from './streamToken'` — a
78
+ * relative import of a module inside this package
79
+ * → red too: "Cannot find module './streamToken'". A leaf may not reach sideways
80
+ * either, which is why ONLY the leaf files are copied into the fixture.
81
+ * · `./server/storage/types` re-pointed at `src/server/storage/index.ts`, the barrel it
82
+ * was split out of
83
+ * → red: "Cannot find module './videoDerivativeKeys'". A barrel cannot pass this.
84
+ * · the negative control re-pointed at `loginThrottle.ts`, which imports nothing, so the
85
+ * fixture stopped demonstrating that peers are unreachable
86
+ * → red on the control itself: "the fixture resolved 'otplib' … Every assertion below
87
+ * is vacuous until that is fixed". The vacuity guard fires.
88
+ * · `src/server/private-media/unlock.ts` put back to `import … from '../auth'` — the
89
+ * second, two-hop edge, which grep for `../auth` inside `src/server/storage` cannot see
90
+ * → red: "importing cursedbelt/server/storage statically pulls these OPTIONAL peers
91
+ * … + otplib". This is the branch that found that edge in the first place.
92
+ */
93
+
94
+ const REPO = fileURLToPath(new URL('..', import.meta.url));
95
+
96
+ /**
97
+ * Every subpath that promises to cost a consumer NOTHING at runtime. A leaf earns its
98
+ * place here by being the answer to "I want one type / one primitive out of a module
99
+ * whose index is heavy" — the entry names the runtime symbol proving the module really
100
+ * evaluated, so an accidentally-empty module can never pass this vacuously.
101
+ *
102
+ * The file is read from `exports`, never hard-coded: re-target the subpath and the check
103
+ * follows it to the new file rather than going quietly green against the old one.
104
+ */
105
+ const LEAVES = [
106
+ {
107
+ subpath: './ctgr/types',
108
+ /** Wire-format types + the method⇄abbreviation maps. Trivially isomorphic on purpose. */
109
+ evaluates: 'METHOD_TO_ABBREV',
110
+ },
111
+ ] as const;
112
+
113
+ /**
114
+ * Imports `cwip/error` (a real dependency of this package), so the fixture proving it
115
+ * CANNOT resolve is the proof the fixture has no packages at all — not an assertion
116
+ * about `errors.ts` itself.
117
+ */
118
+ const CONTROL = 'src/shared/errors.ts';
119
+
120
+ type Outcome = { ok: true; keys: string[] } | { ok: false; error: string };
121
+
122
+ const exportsMap = pkg.exports as Record<string, { bun?: string } | string>;
123
+
124
+ /** `./src/server/storage/types.ts` → `src/server/storage/types.ts`. */
125
+ const sourceOf = (subpath: string): string => {
126
+ const entry = exportsMap[subpath];
127
+ const bun = typeof entry === 'object' && entry !== null ? entry.bun : undefined;
128
+ if (typeof bun !== 'string') {
129
+ throw new Error(`package.json exports has no \`bun\` target for "${subpath}"`);
130
+ }
131
+ return bun.replace(/^\.\//, '');
132
+ };
133
+
134
+ /**
135
+ * 🔴 The fixture must sit OUTSIDE this repo, and where it lands is not a detail.
136
+ *
137
+ * `scripts/testTmpRoot.ts` redirects `$TMPDIR` per process, and when `$FORGE_STATE`
138
+ * is unset — a bare checkout with no generation around it — its fallback is
139
+ * deliberately repo-local: `<repo>/.agent.noindex/tmp/<pid>`. For every other suite
140
+ * that is exactly right. For this one it is fatal: bun resolves a bare import by
141
+ * walking node_modules UP to the filesystem root, so a fixture under the repo finds
142
+ * `<repo>/node_modules/otplib` and every peer this check exists to prove unreachable
143
+ * becomes reachable.
144
+ *
145
+ * Measured 2026-09-15: with `$FORGE_STATE` set the negative control failed correctly;
146
+ * with it unset the SAME control resolved `otplib` out of this repo's own
147
+ * `node_modules` and the guard fired — *"the fixture resolved 'otplib' … Every
148
+ * assertion below is vacuous until that is fixed"*. So `bun run verify` was green
149
+ * under the runner and red in a plain shell, for a reason that has nothing to do with
150
+ * the code under test. The control did its job; the fixture's address was the bug.
151
+ *
152
+ * Resolution here is therefore explicit rather than inherited. The redirected root is
153
+ * used when it lies outside the repo (the normal case — the runner always sets
154
+ * `$FORGE_STATE`, and the preload's sweep then cleans up after a killed run). When it
155
+ * does not, this hops to `$HOME`, which `resolveTestTmpRoot` refuses for the general
156
+ * default and which this check genuinely requires: an isolated root is the whole
157
+ * measurement. Stale siblings are swept by mtime, the same way the preload does it,
158
+ * so the hop cannot accumulate.
159
+ */
160
+ const REPO_PREFIX = `${REPO.replace(/\/$/, '')}/`;
161
+
162
+ const fixtureRoot = (): string => {
163
+ const redirected = process.env.TMPDIR;
164
+ if (redirected !== undefined && !`${redirected}/`.startsWith(REPO_PREFIX)) {
165
+ return `${redirected}/leaf-subpath-fixture`;
166
+ }
167
+ const home = process.env.HOME?.trim();
168
+ if (home === undefined || home === '') {
169
+ throw new Error(
170
+ 'no isolated root available for the leaf fixture ($TMPDIR is inside this repo and $HOME is unset) — ' +
171
+ 'a check that cannot isolate itself is not a passing check',
172
+ );
173
+ }
174
+ const outside = `${home}/.${pkg.name}-leaf-fixture.noindex`;
175
+ try {
176
+ const cutoff = Date.now() - 6 * 60 * 60 * 1000;
177
+ for (const entry of readdirSync(outside)) {
178
+ const path = `${outside}/${entry}`;
179
+ try {
180
+ if (statSync(path).mtimeMs < cutoff) rmSync(path, { recursive: true, force: true });
181
+ } catch {
182
+ // racing another run's sweep is fine — whoever wins, it goes
183
+ }
184
+ }
185
+ } catch {
186
+ // no root yet: Bun.write creates it below
187
+ }
188
+ return `${outside}/${process.pid}/leaf-subpath-fixture`;
189
+ };
190
+
191
+ const run = async (): Promise<Record<string, Outcome>> => {
192
+ const root = fixtureRoot();
193
+ const pkgDir = `${root}/node_modules/${pkg.name}`;
194
+
195
+ // The second lock against bun's auto-install (see the header). The negative control is the
196
+ // first one, and the one that would catch this line being wrong.
197
+ await Bun.write(`${root}/bunfig.toml`, '[install]\nauto = "disable"\n');
198
+ // The REAL manifest: the export map under test, resolved exactly as a consumer resolves it.
199
+ await Bun.write(`${pkgDir}/package.json`, Bun.file(`${REPO}/package.json`));
200
+
201
+ const targets: Array<{ name: string; specifier: string }> = [];
202
+ for (const leaf of LEAVES) {
203
+ const source = sourceOf(leaf.subpath);
204
+ // 🔴 Bun.file/Bun.write, NOT node:fs — this repo's test preload virtualizes node:fs, so a
205
+ // copy made through it can land in a mock the spawned child cannot see. Same trap as
206
+ // publishShape.spec.ts.
207
+ await Bun.write(`${pkgDir}/${source}`, Bun.file(`${REPO}/${source}`));
208
+ targets.push({ name: leaf.subpath, specifier: `${pkg.name}/${leaf.subpath.slice(2)}` });
209
+ }
210
+ await Bun.write(`${pkgDir}/${CONTROL}`, Bun.file(`${REPO}/${CONTROL}`));
211
+ // By PATH, not by subpath: the control is deliberately not exported, and an
212
+ // ERR_PACKAGE_PATH_NOT_EXPORTED would prove nothing about peer resolution.
213
+ targets.push({ name: 'control', specifier: `./node_modules/${pkg.name}/${CONTROL}` });
214
+
215
+ await Bun.write(
216
+ `${root}/entry.ts`,
217
+ `const targets = ${JSON.stringify(targets)};
218
+ const out = {};
219
+ for (const t of targets) {
220
+ try {
221
+ const mod = await import(t.specifier);
222
+ out[t.name] = { ok: true, keys: Object.keys(mod) };
223
+ } catch (error) {
224
+ out[t.name] = { ok: false, error: String((error && error.message) || error) };
225
+ }
226
+ }
227
+ console.log(JSON.stringify(out));
228
+ `,
229
+ );
230
+
231
+ const child = Bun.spawnSync(['bun', 'run', 'entry.ts'], {
232
+ cwd: root,
233
+ stdout: 'pipe',
234
+ stderr: 'pipe',
235
+ });
236
+ const stdout = child.stdout.toString().trim();
237
+ if (child.exitCode !== 0 || stdout === '') {
238
+ throw new Error(
239
+ `the fixture could not run at all (exit ${child.exitCode}) — that is a broken check, not a passing one:\n${child.stderr.toString()}`,
240
+ );
241
+ }
242
+ return JSON.parse(stdout) as Record<string, Outcome>;
243
+ };
244
+
245
+ /*
246
+ * The pre-split copy of this file carried a second half here: "barrels reach no
247
+ * optional peer statically", bundling each heavy barrel and asserting no OPTIONAL
248
+ * peer is a static import. cursedbelt-core declares NO optional peers — its whole
249
+ * dependency surface is cwip + ufuzzy with a zod peer — so that half would measure
250
+ * an empty set and its own vacuity guard (`OPTIONAL_PEERS.size > 10`) would red.
251
+ * It lives on in `cursedbelt`, where the optional peers actually are. The day this
252
+ * package grows an optional peer, copy the half back from there rather than
253
+ * re-deriving it: the bundling trap notes in its header were paid for.
254
+ */
255
+
256
+ const outcomes = await run();
257
+
258
+ describe('leaf subpaths import nothing at runtime', () => {
259
+ it('has a fixture with no peers installed (negative control)', () => {
260
+ const control = outcomes.control;
261
+ expect(control, 'the control never ran — the fixture is not measuring anything').toBeDefined();
262
+ expect(
263
+ control?.ok === false,
264
+ `the fixture resolved 'cwip' — bun auto-installed it out of ~/.bun/install/cache, or a node_modules above the fixture answered. Every assertion below is vacuous until that is fixed; do not delete this test to make it pass.`,
265
+ ).toBe(true);
266
+ expect(
267
+ control?.ok === false ? control.error : '',
268
+ 'the control failed for the wrong reason — it must fail on the missing PACKAGE',
269
+ ).toContain("Cannot find package 'cwip");
270
+ });
271
+
272
+ for (const leaf of LEAVES) {
273
+ it(`${pkg.name}/${leaf.subpath.slice(2)} imports clean`, () => {
274
+ const outcome = outcomes[leaf.subpath];
275
+ expect(outcome, `${leaf.subpath} was never imported by the fixture`).toBeDefined();
276
+ expect(
277
+ outcome?.ok === true ? '' : outcome?.ok === false ? outcome.error : 'missing',
278
+ `${pkg.name}/${leaf.subpath.slice(2)} must import NOTHING at runtime — that is the entire reason the subpath exists. Move whatever it now reaches behind \`import type\`, or split the leaf again; do not add the dependency to a consumer.`,
279
+ ).toBe('');
280
+ // Non-vacuity: a module that resolved but exported nothing would otherwise pass.
281
+ expect(
282
+ outcome?.ok === true ? outcome.keys : [],
283
+ `${leaf.subpath} resolved but does not export ${leaf.evaluates} — the check is pointing at the wrong file`,
284
+ ).toContain(leaf.evaluates);
285
+ });
286
+ }
287
+ });
@@ -0,0 +1,50 @@
1
+ /**
2
+ * 🔴 Rule 2 of this generation: `bun install --frozen-lockfile` must pass on a
3
+ * CLEAN CHECKOUT — not merely in the tree an agent happens to be sitting in.
4
+ *
5
+ * ── What this cost, 2026-09-13 ──────────────────────────────────────────────
6
+ * `.gitignore` carried a `bun.lock` stanza with a comment explaining it was
7
+ * temporary: the only lockfile that could exist at the time recorded a
8
+ * proof-only local pack of `cwip`, and committing that would have reintroduced
9
+ * the `file:` dependency the package had just removed. The reasoning was sound
10
+ * and the expiry condition — "until cwip is on the registry" — was real. It was
11
+ * also written in a comment, which nothing can evaluate. `cwip@3.0.2` went to
12
+ * the registry and the stanza stayed, so rule 2 was true of every working tree
13
+ * and false of every fresh clone: exactly the case the rule exists for, and the
14
+ * one nobody hits until they are the person doing the clone.
15
+ *
16
+ * A temporary ignore needs a check that notices when it stopped being true.
17
+ * This is that check. `git check-ignore` is the whole mechanism, because the
18
+ * failure is not "the lockfile is missing" — it is "git has been told to throw
19
+ * it away", which is invisible in a tree that already has one.
20
+ *
21
+ * Deliberately NOT `git ls-files bun.lock`: a regenerated lockfile is untracked
22
+ * until it is staged, and a check that reds on work-in-progress teaches people
23
+ * to skip it. Ignored-vs-not is the property that actually regresses.
24
+ */
25
+ import { describe, expect, test } from 'bun:test'
26
+ import { spawnSync } from 'node:child_process'
27
+ import { existsSync } from 'node:fs'
28
+ import { fileURLToPath } from 'node:url'
29
+
30
+ const repoRoot = fileURLToPath(new URL('..', import.meta.url))
31
+
32
+ describe('bun.lock is part of the checkout', () => {
33
+ test('the lockfile exists', () => {
34
+ expect(
35
+ existsSync(new URL('../bun.lock', import.meta.url)),
36
+ 'no bun.lock — regenerate it with `bun install`, or `bun install --frozen-lockfile` cannot pass for anyone cloning this',
37
+ ).toBe(true)
38
+ })
39
+
40
+ test('git is not told to ignore it', () => {
41
+ const git = spawnSync('git', ['check-ignore', '-q', 'bun.lock'], { cwd: repoRoot })
42
+ if (git.error) return // no git on this host: nothing to assert about ignore rules
43
+
44
+ // exit 0 = the path IS ignored; 1 = it is not; anything else = git could not tell us.
45
+ expect(
46
+ git.status,
47
+ 'bun.lock is gitignored, so a fresh clone has no lockfile and `bun install --frozen-lockfile` cannot pass there. Delete the stanza from .gitignore rather than the assertion.',
48
+ ).not.toBe(0)
49
+ })
50
+ })