cursedbelt-server 4.8.0 → 4.9.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.
@@ -35,7 +35,7 @@ export interface ChunkedUploadService {
35
35
  /**
36
36
  * Mint the direct-to-backend PUT URL for one chunk. The app supplies this so the module stays
37
37
  * backend-agnostic. For binary-server: `${base}/upload-chunk/${app}/${key}?token=` with a token
38
- * carrying { k, uid, sid, ci, tc }.
38
+ * carrying { k, uid, sid, ci, tc, sz }.
39
39
  */
40
40
  export type MintChunkUpload = (args: {
41
41
  key: string;
@@ -43,6 +43,17 @@ export type MintChunkUpload = (args: {
43
43
  ci: number;
44
44
  tc: number;
45
45
  owner: string;
46
+ /**
47
+ * The SOURCE byte length this session was opened with — pass it straight into the token's `sz`
48
+ * claim (`FileTokenClaims.sz`), which is the only end-to-end integrity check the upload path
49
+ * has: binary-server compares it to the assembled object and answers 422 instead of indexing a
50
+ * short one.
51
+ *
52
+ * 🔴 `undefined` when the caller could not state a positive length, and then the claim must be
53
+ * OMITTED rather than sent as `0` — a declared zero refuses every assemble. That is the same
54
+ * posture the claim itself has: optional, so that declaring it could never take an upload down.
55
+ */
56
+ sz?: number;
46
57
  }) => Promise<string>;
47
58
  export declare function createChunkedUploadService(cfg: {
48
59
  catalogue: FileCatalogue;
@@ -39,7 +39,12 @@ export function createChunkedUploadService(cfg) {
39
39
  tags: req.tags,
40
40
  createdAt: now(),
41
41
  });
42
- const chunkUploadUrls = await Promise.all(Array.from({ length: chunkCount }, (_, ci) => cfg.mintChunkUpload({ key, sid, ci, tc: chunkCount, owner })));
42
+ // 🔴 The source length rides along, because `chunkCount` above is derived from it and a
43
+ // count is not a length: a session that splits the file wrongly stores whatever it sent,
44
+ // faithfully, with a 201. Omitted rather than zeroed when the caller stated no positive
45
+ // size — `sz: 0` would refuse every assemble. See `MintChunkUpload.sz`.
46
+ const declaredSize = Number.isFinite(req.size) && req.size > 0 ? req.size : undefined;
47
+ const chunkUploadUrls = await Promise.all(Array.from({ length: chunkCount }, (_, ci) => cfg.mintChunkUpload({ key, sid, ci, tc: chunkCount, owner, sz: declaredSize })));
43
48
  return { fileId, sid, key, chunkSize, chunkCount, chunkUploadUrls };
44
49
  },
45
50
  async complete(sid, confirmed) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursedbelt-server",
3
- "version": "4.8.0",
3
+ "version": "4.9.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.",
@@ -39,7 +39,7 @@ describe('chunked upload', () => {
39
39
  now: () => ++clock,
40
40
  });
41
41
  const ledger = createStorageOpsLedger(db, () => ++clock);
42
- const minted: Array<{ key: string; sid: string; ci: number; tc: number }> = [];
42
+ const minted: Array<{ key: string; sid: string; ci: number; tc: number; sz?: number }> = [];
43
43
  const svc = createChunkedUploadService({
44
44
  catalogue,
45
45
  ledger,
@@ -67,6 +67,10 @@ describe('chunked upload', () => {
67
67
  expect(init.chunkUploadUrls).toHaveLength(3);
68
68
  expect(minted.map((m) => m.ci)).toEqual([0, 1, 2]);
69
69
  expect(minted.every((m) => m.tc === 3 && m.sid === init.sid)).toBe(true);
70
+ // 🔴 The SOURCE length, on every chunk. `tc` is derived from it and a count is not a length:
71
+ // without this the minter has nothing to put in the token's `sz` claim, and binary-server's
72
+ // only end-to-end check of the finished object against its source stays inert.
73
+ expect(minted.map((m) => m.sz)).toEqual([20 * 1024 * 1024, 20 * 1024 * 1024, 20 * 1024 * 1024]);
70
74
  // The mint targets exactly the key the catalogue will look for on complete.
71
75
  expect(init.key).toBe(catalogueUploadKey('u1', init.fileId, 'image/png'));
72
76
 
@@ -78,6 +82,16 @@ describe('chunked upload', () => {
78
82
  expect(svc.pending()).toBe(1);
79
83
  });
80
84
 
85
+ test('🔴 a size the caller could not state is OMITTED, never declared as zero', async () => {
86
+ // `sz: 0` would compare zero to the assembled length and refuse every assemble — so an
87
+ // unknown size degrades to exactly today's behaviour (no claim, recorded undeclared) rather
88
+ // than to a 422. `chunkCount` still clamps to one chunk, which is the old `req.size || 1`.
89
+ const { svc, minted } = build();
90
+ const init = await svc.init(CTX, { name: 'unknown.bin', mime: 'application/octet-stream', size: 0 });
91
+ expect(init.chunkCount).toBe(1);
92
+ expect(minted.map((m) => m.sz)).toEqual([undefined]);
93
+ });
94
+
81
95
  test('complete registers the catalogue row over the assembled bytes + marks the op done', async () => {
82
96
  const { catalogue, ledger, svc } = build();
83
97
  const init = await svc.init(CTX, {
@@ -64,7 +64,7 @@ export interface ChunkedUploadService {
64
64
  /**
65
65
  * Mint the direct-to-backend PUT URL for one chunk. The app supplies this so the module stays
66
66
  * backend-agnostic. For binary-server: `${base}/upload-chunk/${app}/${key}?token=` with a token
67
- * carrying { k, uid, sid, ci, tc }.
67
+ * carrying { k, uid, sid, ci, tc, sz }.
68
68
  */
69
69
  export type MintChunkUpload = (args: {
70
70
  key: string;
@@ -72,6 +72,17 @@ export type MintChunkUpload = (args: {
72
72
  ci: number;
73
73
  tc: number;
74
74
  owner: string;
75
+ /**
76
+ * The SOURCE byte length this session was opened with — pass it straight into the token's `sz`
77
+ * claim (`FileTokenClaims.sz`), which is the only end-to-end integrity check the upload path
78
+ * has: binary-server compares it to the assembled object and answers 422 instead of indexing a
79
+ * short one.
80
+ *
81
+ * 🔴 `undefined` when the caller could not state a positive length, and then the claim must be
82
+ * OMITTED rather than sent as `0` — a declared zero refuses every assemble. That is the same
83
+ * posture the claim itself has: optional, so that declaring it could never take an upload down.
84
+ */
85
+ sz?: number;
75
86
  }) => Promise<string>;
76
87
 
77
88
  export function createChunkedUploadService(cfg: {
@@ -110,9 +121,14 @@ export function createChunkedUploadService(cfg: {
110
121
  tags: req.tags,
111
122
  createdAt: now(),
112
123
  });
124
+ // 🔴 The source length rides along, because `chunkCount` above is derived from it and a
125
+ // count is not a length: a session that splits the file wrongly stores whatever it sent,
126
+ // faithfully, with a 201. Omitted rather than zeroed when the caller stated no positive
127
+ // size — `sz: 0` would refuse every assemble. See `MintChunkUpload.sz`.
128
+ const declaredSize = Number.isFinite(req.size) && req.size > 0 ? req.size : undefined;
113
129
  const chunkUploadUrls = await Promise.all(
114
130
  Array.from({ length: chunkCount }, (_, ci) =>
115
- cfg.mintChunkUpload({ key, sid, ci, tc: chunkCount, owner }),
131
+ cfg.mintChunkUpload({ key, sid, ci, tc: chunkCount, owner, sz: declaredSize }),
116
132
  ),
117
133
  );
118
134
  return { fileId, sid, key, chunkSize, chunkCount, chunkUploadUrls };