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
|
-
|
|
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.
|
|
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';
|