cursedbelt-server 4.7.0 β†’ 4.8.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.
@@ -454,7 +454,12 @@ export function createBinaryStore(cfg) {
454
454
  const titleQs = title ? `&title=${encodeURIComponent(title)}` : '';
455
455
  const immutableQs = opts?.immutable ? '&immutable=1' : '';
456
456
  for (let ci = 0; ci < total; ci++) {
457
- const token = await signToken({ k: appKey(key), sid, ci, tc: total }, UPLOAD_TOKEN_TTL_SECONDS);
457
+ // πŸ”΄ `sz` is the SOURCE length, and it is what makes `tc` above not load-bearing: bs
458
+ // checks it before and after assembly and answers 422 rather than indexing a short
459
+ // object (see `FileTokenClaims.sz` in `./types`). `total` is derived from `blob.size`
460
+ // two lines up, so the number is already in hand β€” declaring it costs nothing, and
461
+ // NOT declaring it is what left 579 of 579 fleet assembles unprovable on 2026-09-18.
462
+ const token = await signToken({ k: appKey(key), sid, ci, tc: total, sz: blob.size }, UPLOAD_TOKEN_TTL_SECONDS);
458
463
  const slice = new Uint8Array(await blob
459
464
  .slice(ci * chunkBytes, Math.min((ci + 1) * chunkBytes, blob.size))
460
465
  .arrayBuffer());
@@ -69,6 +69,32 @@ export type FileTokenClaims = {
69
69
  ci?: number;
70
70
  /** Total chunk count. */
71
71
  tc?: number;
72
+ /**
73
+ * The SOURCE file's exact byte length, declared by the minting client when the chunk session
74
+ * is created. binary-server checks it before and after assembly and answers 422 on a
75
+ * mismatch, indexing nothing.
76
+ *
77
+ * πŸ”΄ This is the only END-TO-END integrity claim the upload path has; everything else in it
78
+ * is an internal consistency check. `tc` comes from the client, the assemble fires when `tc`
79
+ * parts have been counted, and the checksum is computed over whatever was assembled β€” so a
80
+ * session that believes a 6-minute video is one 16 MiB part gets exactly that stored,
81
+ * faithfully, with a 201. That is what happened to `collections/file/u_fd668e6a-…` on
82
+ * 2026-08-13: nothing anywhere compared the finished object to the file it came from.
83
+ *
84
+ * With `sz`, `tc` stops being load-bearing. The part size is the client's choice by design
85
+ * (a browser picks its own slice), so `tc` and the chunk size are one degree of freedom and
86
+ * the server cannot derive either; declaring the length costs a client nothing, because
87
+ * every mint site already has the number in hand. A wrong `tc` can then only cost a REFUSED
88
+ * upload, never a short object indexed `active`.
89
+ *
90
+ * Optional, and it stays optional until a tenant is measured at zero undeclared assembles:
91
+ * an upload the owner cannot complete is worse than the hole. An absent `sz` is recorded on
92
+ * the assemble event (`resp_meta.declared` is `null`), so which tenants still do not declare
93
+ * is a query rather than an audit. `apps/binary-server/src/tokens.ts` is the mirror this
94
+ * shape is verified against, and `declaredSizeOf` in `apps/binary-server/src/server.ts` is
95
+ * what reads it.
96
+ */
97
+ sz?: number;
72
98
  /** Download filename β†’ Content-Disposition. */
73
99
  fn?: string;
74
100
  dl?: 'inline' | 'attachment';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursedbelt-server",
3
- "version": "4.7.0",
3
+ "version": "4.8.0",
4
4
  "license": "ISC",
5
5
  "type": "module",
6
6
  "description": "The app-facing Bun/Hono server tier of the cursedbelt split \u2014 storage, sharing, activity, guard, sync. React-free; cursedbelt-core below it.",
@@ -283,6 +283,26 @@ describe('putLarge', () => {
283
283
  expect(claims.every((p) => p.k === 'roms/video/x')).toBe(true);
284
284
  });
285
285
 
286
+ it('πŸ”΄ declares the SOURCE byte length as `sz` on every chunk, whatever the split', async () => {
287
+ // The claim that makes `tc` above not load-bearing. binary-server checks `sz` before and
288
+ // after assembly and answers 422 rather than indexing a short object β€” but only for a
289
+ // client that declares it, and on 2026-09-18 that was 0 of 579 fleet assembles. So: the
290
+ // number is the SOURCE's, it is identical on every part, and it does not move when the
291
+ // client picks a different slice size (`tc` and the chunk size are the client's one
292
+ // degree of freedom; the length is not).
293
+ const store = makeStore();
294
+ reply = () => ok(null, { status: 201 });
295
+
296
+ await store.putLarge('video/x', new Uint8Array(10), 'video/mp4', { chunkBytes: 4 });
297
+ expect(calls.map((c) => tokenOf(c.url).payload.sz)).toEqual([10, 10, 10]);
298
+
299
+ calls = [];
300
+ await store.putLarge('video/y', new Uint8Array(10), 'video/mp4', { chunkBytes: 7 });
301
+ const claims = calls.map((c) => tokenOf(c.url).payload);
302
+ expect(claims.map((p) => p.tc)).toEqual([2, 2]); // a different split…
303
+ expect(claims.map((p) => p.sz)).toEqual([10, 10]); // …and the same declared length
304
+ });
305
+
286
306
  it('πŸ”΄ sends the real BYTES of each slice, not a lazy view', async () => {
287
307
  // The 2026-08-13 hang: a Bun.file partial view handed to fetch never sends, and
288
308
  // 42 files / 35 GB failed 42/42 for a day with nothing in binary-server's log.
@@ -741,8 +741,13 @@ export function createBinaryStore(cfg: BinaryStoreConfig): BinaryStore {
741
741
  const titleQs = title ? `&title=${encodeURIComponent(title)}` : '';
742
742
  const immutableQs = opts?.immutable ? '&immutable=1' : '';
743
743
  for (let ci = 0; ci < total; ci++) {
744
+ // πŸ”΄ `sz` is the SOURCE length, and it is what makes `tc` above not load-bearing: bs
745
+ // checks it before and after assembly and answers 422 rather than indexing a short
746
+ // object (see `FileTokenClaims.sz` in `./types`). `total` is derived from `blob.size`
747
+ // two lines up, so the number is already in hand β€” declaring it costs nothing, and
748
+ // NOT declaring it is what left 579 of 579 fleet assembles unprovable on 2026-09-18.
744
749
  const token = await signToken(
745
- { k: appKey(key), sid, ci, tc: total },
750
+ { k: appKey(key), sid, ci, tc: total, sz: blob.size },
746
751
  UPLOAD_TOKEN_TTL_SECONDS,
747
752
  );
748
753
  const slice = new Uint8Array(
@@ -93,6 +93,32 @@ export type FileTokenClaims = {
93
93
  ci?: number;
94
94
  /** Total chunk count. */
95
95
  tc?: number;
96
+ /**
97
+ * The SOURCE file's exact byte length, declared by the minting client when the chunk session
98
+ * is created. binary-server checks it before and after assembly and answers 422 on a
99
+ * mismatch, indexing nothing.
100
+ *
101
+ * πŸ”΄ This is the only END-TO-END integrity claim the upload path has; everything else in it
102
+ * is an internal consistency check. `tc` comes from the client, the assemble fires when `tc`
103
+ * parts have been counted, and the checksum is computed over whatever was assembled β€” so a
104
+ * session that believes a 6-minute video is one 16 MiB part gets exactly that stored,
105
+ * faithfully, with a 201. That is what happened to `collections/file/u_fd668e6a-…` on
106
+ * 2026-08-13: nothing anywhere compared the finished object to the file it came from.
107
+ *
108
+ * With `sz`, `tc` stops being load-bearing. The part size is the client's choice by design
109
+ * (a browser picks its own slice), so `tc` and the chunk size are one degree of freedom and
110
+ * the server cannot derive either; declaring the length costs a client nothing, because
111
+ * every mint site already has the number in hand. A wrong `tc` can then only cost a REFUSED
112
+ * upload, never a short object indexed `active`.
113
+ *
114
+ * Optional, and it stays optional until a tenant is measured at zero undeclared assembles:
115
+ * an upload the owner cannot complete is worse than the hole. An absent `sz` is recorded on
116
+ * the assemble event (`resp_meta.declared` is `null`), so which tenants still do not declare
117
+ * is a query rather than an audit. `apps/binary-server/src/tokens.ts` is the mirror this
118
+ * shape is verified against, and `declaredSizeOf` in `apps/binary-server/src/server.ts` is
119
+ * what reads it.
120
+ */
121
+ sz?: number;
96
122
  /** Download filename β†’ Content-Disposition. */
97
123
  fn?: string;
98
124
  dl?: 'inline' | 'attachment';