@rpcbase/server 0.601.0 → 0.602.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 (38) hide show
  1. package/dist/fileReferences-Ca0tivFd.js +101 -0
  2. package/dist/fileReferences-Ca0tivFd.js.map +1 -0
  3. package/dist/{handler-Fk-5fOSm.js → handler-BUTqP40B.js} +2 -2
  4. package/dist/{handler-Fk-5fOSm.js.map → handler-BUTqP40B.js.map} +1 -1
  5. package/dist/{handler-C92_gJkX.js → handler-BgcIWWMc.js} +2 -2
  6. package/dist/{handler-C92_gJkX.js.map → handler-BgcIWWMc.js.map} +1 -1
  7. package/dist/{handler-CapJTGzc.js → handler-CqSlSCdd.js} +215 -57
  8. package/dist/handler-CqSlSCdd.js.map +1 -0
  9. package/dist/{handler-BqlcQ9sE.js → handler-LRLmxrsg.js} +69 -24
  10. package/dist/handler-LRLmxrsg.js.map +1 -0
  11. package/dist/notifications.js +1 -1
  12. package/dist/postProcessors-iffHvje4.js.map +1 -1
  13. package/dist/rts/index.js +1 -1
  14. package/dist/{schemas-B2fUvgYo.js → schemas-POKnVn0c.js} +58 -2
  15. package/dist/schemas-POKnVn0c.js.map +1 -0
  16. package/dist/{shared-DfrVDSp0.js → shared-Zt17vuAT.js} +54 -2
  17. package/dist/shared-Zt17vuAT.js.map +1 -0
  18. package/dist/uploads/api/file-uploads/fileReferences.d.ts +42 -0
  19. package/dist/uploads/api/file-uploads/fileReferences.d.ts.map +1 -0
  20. package/dist/uploads/api/file-uploads/handlers/completeUpload.d.ts.map +1 -1
  21. package/dist/uploads/api/file-uploads/handlers/initUpload.d.ts.map +1 -1
  22. package/dist/uploads/api/file-uploads/imageVariantsPostProcessor.d.ts.map +1 -1
  23. package/dist/uploads/api/file-uploads/index.d.ts +23 -0
  24. package/dist/uploads/api/file-uploads/index.d.ts.map +1 -1
  25. package/dist/uploads/api/file-uploads/postProcessors.d.ts +2 -0
  26. package/dist/uploads/api/file-uploads/postProcessors.d.ts.map +1 -1
  27. package/dist/uploads/api/file-uploads/shared.d.ts +21 -0
  28. package/dist/uploads/api/file-uploads/shared.d.ts.map +1 -1
  29. package/dist/uploads/api/files/handlers/deleteFile.d.ts.map +1 -1
  30. package/dist/uploads/api/files/handlers/getFile.d.ts.map +1 -1
  31. package/dist/uploads/worker.js +3 -2
  32. package/dist/uploads/worker.js.map +1 -1
  33. package/dist/uploads.js +2 -2
  34. package/package.json +1 -1
  35. package/dist/handler-BqlcQ9sE.js.map +0 -1
  36. package/dist/handler-CapJTGzc.js.map +0 -1
  37. package/dist/schemas-B2fUvgYo.js.map +0 -1
  38. package/dist/shared-DfrVDSp0.js.map +0 -1
@@ -1,10 +1,11 @@
1
1
  import { r as enqueueUploadPostProcessors } from "./postProcessors-iffHvje4.js";
2
- import { a as getChunkSizeBytes, c as getRawBodyLimitBytes, f as getUploadSessionAccessQuery, h as toBufferPayload, i as getBucketName, l as getSessionTtlMs, m as normalizeSha256Hex, n as computeSha256Hex, o as getMaxClientUploadBytesPerSecond, p as getUserId, r as ensureUploadIndexes, s as getModelCtx, t as buildUploadsAbility, u as getTenantId } from "./shared-DfrVDSp0.js";
2
+ import { _ as getUserId, a as ensureFilesystemUploadIndexes, b as resolveUploadAccessScope, c as getChunkSizeBytes, d as getModelCtx, f as getRawBodyLimitBytes, g as getUploadSessionAccessQuery, i as computeSha256Hex, m as getTenantId, n as buildUploadDeduplication, o as ensureUploadIndexes, p as getSessionTtlMs, r as buildUploadsAbility, s as getBucketName, t as buildUploadAccessMetadata, u as getMaxClientUploadBytesPerSecond, v as normalizeSha256Hex, x as toBufferPayload } from "./shared-Zt17vuAT.js";
3
3
  import { t as convertHeifToWebpProcessor } from "./convertHeifToWebp-BT1n72DQ.js";
4
- import { a as object, i as number, n as array, r as boolean, s as string, t as _enum } from "./schemas-B2fUvgYo.js";
4
+ import { a as number, c as string, i as literal, n as array, o as object, r as boolean, t as _enum } from "./schemas-POKnVn0c.js";
5
+ import { n as createUploadFileReference, t as claimUploadFileReference } from "./fileReferences-Ca0tivFd.js";
5
6
  import { GridFSBucket, ObjectId } from "mongodb";
6
7
  import { getTenantFilesystemDb, models } from "@rpcbase/db";
7
- import { randomBytes } from "node:crypto";
8
+ import { createHash, randomBytes } from "node:crypto";
8
9
  import { JSDOM } from "jsdom";
9
10
  import createDOMPurify from "dompurify";
10
11
  //#region src/uploads/api/file-uploads/processors/sanitizeSvg.ts
@@ -109,6 +110,7 @@ var abortUploadStream = async (stream) => {
109
110
  stream.destroy?.();
110
111
  } catch {}
111
112
  };
113
+ var isDuplicateKeyError = (error) => error !== null && typeof error === "object" && "code" in error && error.code === 11e3;
112
114
  var completeUpload = async (_payload, ctx) => {
113
115
  const tenantId = getTenantId(ctx);
114
116
  if (!tenantId) {
@@ -146,7 +148,8 @@ var completeUpload = async (_payload, ctx) => {
146
148
  }
147
149
  if (existing.status === "done" && existing.fileId) return {
148
150
  ok: true,
149
- fileId: existing.fileId
151
+ fileId: existing.fileId,
152
+ ...existing.deduplicated ? { deduplicated: true } : {}
150
153
  };
151
154
  const locked = await UploadSession.findOneAndUpdate({ $and: [
152
155
  { _id: uploadId },
@@ -176,8 +179,26 @@ var completeUpload = async (_payload, ctx) => {
176
179
  error: "assembly_failed"
177
180
  };
178
181
  }
179
- const bucket = new GridFSBucket(nativeDb, { bucketName: getBucketName() });
182
+ const bucketName = getBucketName();
183
+ const bucket = new GridFSBucket(nativeDb, { bucketName });
184
+ if (locked.deduplication) await ensureFilesystemUploadIndexes(nativeDb, bucketName);
180
185
  const lockedUserId = typeof locked.userId === "string" ? locked.userId : void 0;
186
+ const accessMetadata = buildUploadAccessMetadata(locked);
187
+ const unclaimedDeduplication = locked.deduplication ? { deduplication: {
188
+ namespace: locked.deduplication.namespace,
189
+ algorithm: locked.deduplication.algorithm,
190
+ hash: locked.deduplication.hash,
191
+ pipelineVersion: locked.deduplication.pipelineVersion
192
+ } } : {};
193
+ const createFileMetadata = (storedMimeType, storedSize, additional = {}) => ({
194
+ uploadId,
195
+ tenantId,
196
+ mimeType: storedMimeType,
197
+ totalSize: storedSize,
198
+ ...additional,
199
+ ...accessMetadata,
200
+ ...unclaimedDeduplication
201
+ });
181
202
  const maxProcessorBytes = getMaxUploadProcessorBytes();
182
203
  const shouldBufferForProcessing = locked.totalSize <= maxProcessorBytes;
183
204
  const declaredSvg = locked.mimeType.trim().toLowerCase() === "image/svg+xml" || locked.filename.trim().toLowerCase().endsWith(".svg");
@@ -186,19 +207,13 @@ var completeUpload = async (_payload, ctx) => {
186
207
  let finalMimeType = locked.mimeType;
187
208
  let finalSize = locked.totalSize;
188
209
  let inlineProcessors = [];
189
- let finalMetadata = {
190
- uploadId,
191
- tenantId,
192
- mimeType: locked.mimeType,
193
- totalSize: locked.totalSize,
194
- ...typeof locked.isPublic === "boolean" ? { isPublic: locked.isPublic } : {},
195
- ...typeof locked.ownerKeyHash === "string" ? { ownerKeyHash: locked.ownerKeyHash } : {},
196
- ...lockedUserId ? { userId: lockedUserId } : {}
197
- };
210
+ let finalMetadata = createFileMetadata(locked.mimeType, locked.totalSize);
198
211
  try {
199
212
  if (!shouldBufferForProcessing && declaredSvg) throw new Error("svg_too_large");
200
213
  const cursor = UploadChunk.find({ uploadId }).sort({ index: 1 }).cursor();
201
214
  let expectedIndex = 0;
215
+ let sourceBytes = 0;
216
+ const sourceHasher = locked.deduplication ? createHash("sha256") : null;
202
217
  const chunks = [];
203
218
  let bufferedBytes = 0;
204
219
  const pendingChunks = [];
@@ -208,6 +223,8 @@ var completeUpload = async (_payload, ctx) => {
208
223
  for await (const chunkDoc of cursor) {
209
224
  if (chunkDoc.index !== expectedIndex) throw new Error("missing_chunks");
210
225
  const chunk = chunkDoc.data;
226
+ sourceBytes += chunk.length;
227
+ sourceHasher?.update(chunk);
211
228
  if (shouldBufferForProcessing) {
212
229
  chunks.push(chunk);
213
230
  bufferedBytes += chunk.length;
@@ -229,15 +246,7 @@ var completeUpload = async (_payload, ctx) => {
229
246
  sniff
230
247
  });
231
248
  if (processors.length) throw new Error(processors.some((processor) => processor.id === "sanitize-svg") ? "svg_too_large" : "processor_input_too_large");
232
- finalMetadata = {
233
- uploadId,
234
- tenantId,
235
- mimeType: locked.mimeType,
236
- totalSize: locked.totalSize,
237
- ...typeof locked.isPublic === "boolean" ? { isPublic: locked.isPublic } : {},
238
- ...typeof locked.ownerKeyHash === "string" ? { ownerKeyHash: locked.ownerKeyHash } : {},
239
- ...lockedUserId ? { userId: lockedUserId } : {}
240
- };
249
+ finalMetadata = createFileMetadata(locked.mimeType, locked.totalSize);
241
250
  uploadStream = bucket.openUploadStream(locked.filename, { metadata: finalMetadata });
242
251
  for (const pending of pendingChunks) await writeToStream(uploadStream, pending);
243
252
  pendingChunks.length = 0;
@@ -251,6 +260,8 @@ var completeUpload = async (_payload, ctx) => {
251
260
  } catch {}
252
261
  }
253
262
  if (expectedIndex !== locked.chunksTotal) throw new Error("missing_chunks");
263
+ if (sourceBytes !== locked.totalSize) throw new Error("invalid_source_size");
264
+ if (sourceHasher && sourceHasher.digest("hex") !== locked.deduplication?.hash) throw new Error("content_hash_mismatch");
254
265
  if (shouldBufferForProcessing) {
255
266
  const { data: processed, filename: processedFilename, mimeType: processedMimeType, applied, metadata: processedMetadata } = await applyUploadProcessors(Buffer.concat(chunks, bufferedBytes), {
256
267
  filename: locked.filename,
@@ -260,20 +271,13 @@ var completeUpload = async (_payload, ctx) => {
260
271
  finalMimeType = processedMimeType;
261
272
  finalSize = processed.length;
262
273
  inlineProcessors = applied;
263
- finalMetadata = {
264
- uploadId,
265
- tenantId,
266
- mimeType: processedMimeType,
267
- totalSize: processed.length,
274
+ finalMetadata = createFileMetadata(processedMimeType, processed.length, {
268
275
  ...applied.length ? {
269
276
  processors: applied,
270
277
  sourceTotalSize: locked.totalSize
271
278
  } : {},
272
- ...processedMetadata,
273
- ...typeof locked.isPublic === "boolean" ? { isPublic: locked.isPublic } : {},
274
- ...typeof locked.ownerKeyHash === "string" ? { ownerKeyHash: locked.ownerKeyHash } : {},
275
- ...lockedUserId ? { userId: lockedUserId } : {}
276
- };
279
+ ...processedMetadata
280
+ });
277
281
  uploadStream = bucket.openUploadStream(finalFilename, { metadata: finalMetadata });
278
282
  const finished = waitForStreamFinished(uploadStream);
279
283
  uploadStream.end(processed);
@@ -288,15 +292,7 @@ var completeUpload = async (_payload, ctx) => {
288
292
  sniff
289
293
  });
290
294
  if (processors.length) throw new Error(processors.some((processor) => processor.id === "sanitize-svg") ? "svg_too_large" : "processor_input_too_large");
291
- finalMetadata = {
292
- uploadId,
293
- tenantId,
294
- mimeType: locked.mimeType,
295
- totalSize: locked.totalSize,
296
- ...typeof locked.isPublic === "boolean" ? { isPublic: locked.isPublic } : {},
297
- ...typeof locked.ownerKeyHash === "string" ? { ownerKeyHash: locked.ownerKeyHash } : {},
298
- ...lockedUserId ? { userId: lockedUserId } : {}
299
- };
295
+ finalMetadata = createFileMetadata(locked.mimeType, locked.totalSize);
300
296
  uploadStream = bucket.openUploadStream(locked.filename, { metadata: finalMetadata });
301
297
  for (const pending of pendingChunks) await writeToStream(uploadStream, pending);
302
298
  pendingChunks.length = 0;
@@ -305,27 +301,85 @@ var completeUpload = async (_payload, ctx) => {
305
301
  uploadStream.end();
306
302
  await finished;
307
303
  }
308
- const fileId = String(uploadStream.id ?? "");
309
- if (!fileId) throw new Error("missing_file_id");
304
+ const uploadedBlobId = String(uploadStream.id ?? "");
305
+ if (!uploadedBlobId) throw new Error("missing_file_id");
306
+ let resolvedBlobId = uploadedBlobId;
307
+ let deduplicated = false;
308
+ let returnedFileId = locked.deduplication ? null : uploadedBlobId;
309
+ if (locked.deduplication) {
310
+ const filesCollection = nativeDb.collection(`${bucketName}.files`);
311
+ const uploadedBlobObjectId = new ObjectId(uploadedBlobId);
312
+ const referenceOptions = {
313
+ tenantId,
314
+ bucketName,
315
+ filename: finalFilename,
316
+ mimeType: finalMimeType,
317
+ totalSize: finalSize,
318
+ accessScope: resolveUploadAccessScope({
319
+ accessScope: locked.accessScope,
320
+ isPublic: locked.isPublic
321
+ }),
322
+ ...lockedUserId ? { userId: lockedUserId } : {},
323
+ ...typeof locked.ownerKeyHash === "string" ? { ownerKeyHash: locked.ownerKeyHash } : {}
324
+ };
325
+ let lastDuplicateKeyError = null;
326
+ for (let attempt = 0; attempt < 3 && !returnedFileId; attempt += 1) {
327
+ try {
328
+ returnedFileId = await claimUploadFileReference({
329
+ ...referenceOptions,
330
+ blobId: uploadedBlobObjectId,
331
+ deduplication: locked.deduplication
332
+ });
333
+ if (!returnedFileId) throw new Error("missing_file_id");
334
+ finalMetadata = {
335
+ ...finalMetadata,
336
+ deduplication: locked.deduplication
337
+ };
338
+ break;
339
+ } catch (error) {
340
+ if (!isDuplicateKeyError(error)) throw error;
341
+ lastDuplicateKeyError = error;
342
+ }
343
+ const canonicalFile = await filesCollection.findOne({ "metadata.deduplication.key": locked.deduplication.key }, { projection: { _id: 1 } });
344
+ if (!(canonicalFile?._id instanceof ObjectId)) continue;
345
+ const canonicalReferenceId = await createUploadFileReference({
346
+ ...referenceOptions,
347
+ blobId: canonicalFile._id,
348
+ deduplicationKey: locked.deduplication.key
349
+ });
350
+ if (!canonicalReferenceId) continue;
351
+ await bucket.delete(uploadedBlobObjectId);
352
+ resolvedBlobId = canonicalFile._id.toHexString();
353
+ returnedFileId = canonicalReferenceId;
354
+ deduplicated = true;
355
+ }
356
+ if (!returnedFileId) {
357
+ await bucket.delete(uploadedBlobObjectId);
358
+ if (lastDuplicateKeyError) throw lastDuplicateKeyError;
359
+ }
360
+ }
361
+ if (!returnedFileId) throw new Error("canonical_file_missing");
310
362
  await UploadSession.updateOne({ $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, "update")] }, {
311
363
  $set: {
312
364
  status: "done",
313
- fileId,
365
+ fileId: returnedFileId,
314
366
  filename: finalFilename,
315
367
  mimeType: finalMimeType,
316
- totalSize: finalSize
368
+ totalSize: finalSize,
369
+ ...deduplicated ? { deduplicated: true } : {}
317
370
  },
318
371
  $unset: { error: "" }
319
372
  });
320
- await enqueueUploadPostProcessors({
373
+ if (!deduplicated) await enqueueUploadPostProcessors({
321
374
  tenantId,
322
375
  uploadId,
323
- fileId,
376
+ fileId: resolvedBlobId,
324
377
  filename: finalFilename,
325
378
  mimeType: finalMimeType,
326
379
  clientMimeType: locked.mimeType,
327
380
  totalSize: finalSize,
328
381
  ...typeof locked.isPublic === "boolean" ? { isPublic: locked.isPublic } : {},
382
+ ...locked.accessScope ? { accessScope: locked.accessScope } : {},
329
383
  ...typeof locked.ownerKeyHash === "string" ? { ownerKeyHash: locked.ownerKeyHash } : {},
330
384
  ...lockedUserId ? { userId: lockedUserId } : {},
331
385
  inlineProcessors,
@@ -334,7 +388,7 @@ var completeUpload = async (_payload, ctx) => {
334
388
  console.error("Upload post processor enqueue failed", {
335
389
  tenantId,
336
390
  uploadId,
337
- fileId,
391
+ fileId: resolvedBlobId,
338
392
  error
339
393
  });
340
394
  });
@@ -343,7 +397,8 @@ var completeUpload = async (_payload, ctx) => {
343
397
  } catch {}
344
398
  return {
345
399
  ok: true,
346
- fileId
400
+ fileId: returnedFileId,
401
+ ...deduplicated ? { deduplicated: true } : {}
347
402
  };
348
403
  } catch (error) {
349
404
  const message = error instanceof Error ? error.message : String(error);
@@ -356,6 +411,17 @@ var completeUpload = async (_payload, ctx) => {
356
411
  error: "missing_chunks"
357
412
  };
358
413
  }
414
+ if (message === "content_hash_mismatch") {
415
+ await UploadSession.updateOne({ $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, "update")] }, { $set: {
416
+ status: "error",
417
+ error: message
418
+ } });
419
+ ctx.res.status(400);
420
+ return {
421
+ ok: false,
422
+ error: message
423
+ };
424
+ }
359
425
  if (message === "svg_too_large") {
360
426
  await UploadSession.updateOne({ $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, "update")] }, { $set: {
361
427
  status: "error",
@@ -467,11 +533,29 @@ var InitRoute = "/api/rb/file-uploads";
467
533
  var ChunkRoute = "/api/rb/file-uploads/:uploadId/chunks/:index";
468
534
  var StatusRoute = "/api/rb/file-uploads/:uploadId/status";
469
535
  var CompleteRoute = "/api/rb/file-uploads/:uploadId/complete";
536
+ var uploadAccessScopeSchema = _enum([
537
+ "owner",
538
+ "tenant",
539
+ "public"
540
+ ]);
541
+ var uploadDeduplicationSchema = object({
542
+ namespace: string().trim().min(1).max(128).regex(/^[a-z0-9][a-z0-9._:-]*$/i),
543
+ algorithm: literal("sha256"),
544
+ hash: string().trim().regex(/^[a-f0-9]{64}$/i)
545
+ });
470
546
  var initRequestSchema = object({
471
547
  filename: string().min(1),
472
548
  mimeType: string().min(1),
473
549
  isPublic: boolean().optional(),
550
+ accessScope: uploadAccessScopeSchema.optional(),
551
+ deduplication: uploadDeduplicationSchema.optional(),
474
552
  totalSize: number().int().min(1)
553
+ }).superRefine((value, ctx) => {
554
+ if (value.accessScope !== void 0 && value.isPublic !== void 0) ctx.addIssue({
555
+ code: "custom",
556
+ message: "accessScope and isPublic are mutually exclusive",
557
+ path: ["accessScope"]
558
+ });
475
559
  });
476
560
  object({
477
561
  ok: boolean(),
@@ -479,7 +563,9 @@ object({
479
563
  uploadId: string().optional(),
480
564
  uploadKey: string().optional(),
481
565
  chunkSize: number().int().optional(),
482
- chunksTotal: number().int().optional()
566
+ chunksTotal: number().int().optional(),
567
+ fileId: string().optional(),
568
+ deduplicated: boolean().optional()
483
569
  });
484
570
  object({
485
571
  ok: boolean(),
@@ -498,7 +584,8 @@ object({
498
584
  object({
499
585
  ok: boolean(),
500
586
  error: string().optional(),
501
- fileId: string().optional()
587
+ fileId: string().optional(),
588
+ deduplicated: boolean().optional()
502
589
  });
503
590
  //#endregion
504
591
  //#region src/uploads/api/file-uploads/handlers/initUpload.ts
@@ -520,8 +607,79 @@ var initUpload = async (payload, ctx) => {
520
607
  error: "invalid_payload"
521
608
  };
522
609
  }
610
+ const { filename, mimeType, totalSize, isPublic, deduplication: requestedDeduplication } = parsed.data;
611
+ const accessScope = resolveUploadAccessScope({
612
+ accessScope: parsed.data.accessScope,
613
+ isPublic
614
+ });
615
+ if (accessScope === "tenant" && !userId) {
616
+ ctx.res.status(401);
617
+ return {
618
+ ok: false,
619
+ error: "authentication_required"
620
+ };
621
+ }
622
+ const uploadKey = userId ? null : randomBytes(32).toString("base64url");
623
+ const ownerKeyHash = uploadKey ? computeSha256Hex(Buffer.from(uploadKey)) : void 0;
624
+ const deduplication = requestedDeduplication ? buildUploadDeduplication({
625
+ namespace: requestedDeduplication.namespace,
626
+ hash: requestedDeduplication.hash,
627
+ filename,
628
+ mimeType,
629
+ totalSize,
630
+ accessScope,
631
+ ...userId ? { userId } : {},
632
+ ...ownerKeyHash ? { ownerKeyHash } : {}
633
+ }) : null;
634
+ if (requestedDeduplication && !deduplication) {
635
+ ctx.res.status(400);
636
+ return {
637
+ ok: false,
638
+ error: "deduplication_unavailable"
639
+ };
640
+ }
641
+ if (deduplication) {
642
+ const nativeDb = (await getTenantFilesystemDb(tenantId)).db;
643
+ if (!nativeDb) {
644
+ ctx.res.status(500);
645
+ return {
646
+ ok: false,
647
+ error: "filesystem_db_unavailable"
648
+ };
649
+ }
650
+ const bucketName = getBucketName();
651
+ await ensureFilesystemUploadIndexes(nativeDb, bucketName);
652
+ const existingFile = await nativeDb.collection(`${bucketName}.files`).findOne({ "metadata.deduplication.key": deduplication.key }, { projection: {
653
+ _id: 1,
654
+ filename: 1,
655
+ length: 1,
656
+ metadata: 1
657
+ } });
658
+ if (existingFile?._id instanceof ObjectId) {
659
+ const existingMetadata = existingFile.metadata && typeof existingFile.metadata === "object" ? existingFile.metadata : {};
660
+ const existingMimeType = typeof existingMetadata.mimeType === "string" ? existingMetadata.mimeType : mimeType;
661
+ const existingSize = typeof existingFile.length === "number" ? existingFile.length : totalSize;
662
+ const fileId = await createUploadFileReference({
663
+ tenantId,
664
+ bucketName,
665
+ blobId: existingFile._id,
666
+ filename,
667
+ mimeType: existingMimeType,
668
+ totalSize: existingSize,
669
+ accessScope,
670
+ ...userId ? { userId } : {},
671
+ ...ownerKeyHash ? { ownerKeyHash } : {},
672
+ deduplicationKey: deduplication.key
673
+ });
674
+ if (fileId) return {
675
+ ok: true,
676
+ fileId,
677
+ deduplicated: true,
678
+ ...uploadKey ? { uploadKey } : {}
679
+ };
680
+ }
681
+ }
523
682
  const chunkSize = getChunkSizeBytes();
524
- const { filename, mimeType, totalSize, isPublic } = parsed.data;
525
683
  const chunksTotal = Math.ceil(totalSize / chunkSize);
526
684
  const modelCtx = getModelCtx(ctx, tenantId, buildUploadsAbility(ctx, tenantId));
527
685
  const [UploadSession, UploadChunk] = await Promise.all([models.get("RBUploadSession", modelCtx), models.get("RBUploadChunk", modelCtx)]);
@@ -529,15 +687,15 @@ var initUpload = async (payload, ctx) => {
529
687
  const uploadId = new ObjectId().toString();
530
688
  const now = Date.now();
531
689
  const expiresAt = new Date(now + getSessionTtlMs());
532
- const uploadKey = userId ? null : randomBytes(32).toString("base64url");
533
- const ownerKeyHash = uploadKey ? computeSha256Hex(Buffer.from(uploadKey)) : void 0;
534
690
  await UploadSession.create({
535
691
  _id: uploadId,
536
692
  ...userId ? { userId } : {},
537
693
  ...ownerKeyHash ? { ownerKeyHash } : {},
538
694
  filename,
539
695
  mimeType,
540
- ...typeof isPublic === "boolean" ? { isPublic } : {},
696
+ isPublic: accessScope === "public",
697
+ accessScope,
698
+ ...deduplication ? { deduplication } : {},
541
699
  totalSize,
542
700
  chunkSize,
543
701
  chunksTotal,
@@ -792,4 +950,4 @@ var handler_default = (api) => {
792
950
  //#endregion
793
951
  export { handler_default as default };
794
952
 
795
- //# sourceMappingURL=handler-CapJTGzc.js.map
953
+ //# sourceMappingURL=handler-CqSlSCdd.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler-CqSlSCdd.js","names":["JSDOM","createDOMPurify","UploadFileProcessor","MAX_SVG_BYTES","window","DOMPurify","normalizeForSniff","raw","replace","trimStart","looksLikeSvgText","text","normalized","startsWith","test","looksLikeSvg","sniff","Buffer","toString","sanitizeSvg","svg","sanitize","USE_PROFILES","svgFilters","sanitizeSvgProcessor","id","maxBytes","match","process","data","mimeType","length","Error","svgText","sanitized","trim","sanitizedBuffer","from","convertHeifToWebpProcessor","sanitizeSvgProcessor","UploadFileProcessorContext","filename","clientMimeType","totalSize","sniff","Buffer","UploadFileProcessorResult","data","mimeType","metadata","Record","UploadFileProcessor","id","maxBytes","match","ctx","process","Promise","uploadProcessors","Object","freeze","getMaxUploadProcessorBytes","reduce","max","processor","Math","selectUploadProcessors","filter","applyUploadProcessors","Omit","applied","currentData","currentFilename","currentMimeType","processorCtx","length","Error","result","trim","assign","push","createHash","ApiHandler","getTenantFilesystemDb","models","GridFSBucket","ObjectId","Model","Uploads","claimUploadFileReference","createUploadFileReference","enqueueUploadPostProcessors","applyUploadProcessors","getMaxUploadProcessorBytes","selectUploadProcessors","SessionUser","UploadChunkDoc","UploadSessionDoc","buildUploadAccessMetadata","buildUploadsAbility","ensureFilesystemUploadIndexes","ensureUploadIndexes","getBucketName","getModelCtx","getUploadSessionAccessQuery","getTenantId","resolveUploadAccessScope","waitForStreamFinished","stream","NodeJS","WritableStream","Promise","resolve","reject","once","writeToStream","chunk","Buffer","ok","write","onDrain","cleanup","onError","error","off","on","abortUploadStream","abort","destroy","isDuplicateKeyError","code","completeUpload","Record","CompleteResponsePayload","_payload","ctx","tenantId","res","status","uploadId","String","req","params","trim","ability","modelCtx","UploadSession","UploadChunk","all","get","can","existing","findOne","$and","_id","lean","fileId","deduplicated","locked","findOneAndUpdate","$set","$unset","returnDocument","fsDb","nativeDb","db","updateOne","bucketName","bucket","deduplication","lockedUserId","userId","undefined","accessMetadata","unclaimedDeduplication","namespace","algorithm","hash","pipelineVersion","createFileMetadata","storedMimeType","storedSize","additional","mimeType","totalSize","maxProcessorBytes","shouldBufferForProcessing","declaredMimeType","toLowerCase","declaredSvg","filename","endsWith","uploadStream","finalFilename","finalMimeType","finalSize","inlineProcessors","finalMetadata","Error","cursor","find","sort","index","AsyncIterable","close","expectedIndex","sourceBytes","sourceHasher","chunks","bufferedBytes","pendingChunks","sniffParts","sniffBytes","chunkDoc","data","length","update","push","slice","subarray","Math","min","sniff","concat","processors","clientMimeType","some","processor","id","openUploadStream","metadata","pending","chunksTotal","digest","assembled","processed","processedFilename","processedMimeType","applied","processedMetadata","sourceTotalSize","finished","end","uploadedBlobId","resolvedBlobId","returnedFileId","filesCollection","collection","uploadedBlobObjectId","referenceOptions","accessScope","isPublic","ownerKeyHash","lastDuplicateKeyError","attempt","blobId","canonicalFile","key","projection","canonicalReferenceId","deduplicationKey","delete","toHexString","catch","console","deleteMany","message","startsWith","ApiHandler","models","Model","Uploads","SessionUser","UploadChunkDoc","UploadSessionDoc","buildUploadsAbility","getModelCtx","getUploadSessionAccessQuery","getTenantId","getStatus","Record","StatusResponsePayload","_payload","ctx","Promise","tenantId","res","status","ok","error","uploadId","String","req","params","trim","ability","modelCtx","UploadSession","UploadChunk","all","get","can","session","findOne","$and","_id","lean","receivedDocs","find","index","sort","received","Array","map","doc","filter","n","Number","isInteger","chunkSize","chunksTotal","fileId","z","InitRoute","ChunkRoute","StatusRoute","CompleteRoute","uploadAccessScopeSchema","enum","uploadDeduplicationSchema","object","namespace","string","trim","min","max","regex","algorithm","literal","hash","initRequestSchema","filename","mimeType","isPublic","boolean","optional","accessScope","deduplication","totalSize","number","int","superRefine","value","ctx","undefined","addIssue","code","message","path","InitRequestPayload","infer","initResponseSchema","ok","error","uploadId","uploadKey","chunkSize","chunksTotal","fileId","deduplicated","InitResponsePayload","statusResponseSchema","status","received","array","StatusResponsePayload","completeResponseSchema","CompleteResponsePayload","randomBytes","ApiHandler","getTenantFilesystemDb","models","ObjectId","Model","Uploads","createUploadFileReference","SessionUser","UploadChunkDoc","UploadSessionDoc","buildUploadDeduplication","buildUploadsAbility","computeSha256Hex","ensureFilesystemUploadIndexes","ensureUploadIndexes","getBucketName","getChunkSizeBytes","getModelCtx","getSessionTtlMs","getTenantId","getUserId","resolveUploadAccessScope","initUpload","InitRequestPayload","InitResponsePayload","payload","ctx","Promise","tenantId","res","status","ok","error","userId","parsed","initRequestSchema","safeParse","success","filename","mimeType","totalSize","isPublic","deduplication","requestedDeduplication","data","accessScope","uploadKey","toString","ownerKeyHash","Buffer","from","undefined","namespace","hash","fsDb","nativeDb","db","bucketName","existingFile","collection","findOne","key","projection","_id","length","metadata","existingMetadata","Record","existingMimeType","existingSize","fileId","blobId","deduplicationKey","deduplicated","chunkSize","chunksTotal","Math","ceil","ability","modelCtx","UploadSession","UploadChunk","all","get","uploadId","now","Date","expiresAt","create","createdAt","ApiHandler","models","Model","SessionUser","UploadChunkDoc","UploadSessionDoc","buildUploadsAbility","computeSha256Hex","ensureUploadIndexes","getModelCtx","getUploadSessionAccessQuery","getTenantId","normalizeSha256Hex","toBufferPayload","ChunkResponsePayload","ok","error","uploadChunk","Buffer","payload","ctx","Promise","tenantId","res","status","uploadId","String","req","params","trim","indexRaw","index","Number","isInteger","ability","modelCtx","UploadSession","UploadChunk","all","get","can","session","findOne","$and","_id","lean","chunksTotal","data","expectedSize","totalSize","chunkSize","length","checksumHeader","sha256","undefined","expectedSha256","updateOne","$set","size","expiresAt","$setOnInsert","createdAt","Date","upsert","rawBodyParser","limitBytes","maxClientBytesPerSecond","req","res","next","contentType","headers","String","includes","total","chunks","Buffer","done","paused","throttleTimeout","ReturnType","setTimeout","rateBytesPerSecond","cleanup","off","onData","onEnd","onError","onAborted","clearTimeout","finish","error","body","concat","chunk","buffer","isBuffer","from","length","destroy","status","json","ok","push","now","Date","clientKey","getClientKey","state","getClientRateState","waitMs","consumeRateBudget","pause","resume","err","Error","on","ClientRateState","tokens","lastRefillMs","lastSeenMs","MAX_BURST_SECONDS","STALE_CLIENT_MS","clientRateStates","Map","lastCleanupMs","rawClientIp","clientIp","trim","rawIp","ip","maybeCleanupStates","size","key","delete","capacity","existing","get","Math","min","set","bytes","elapsedMs","max","ceil","Api","completeUpload","getStatus","initUpload","uploadChunk","rawBodyParser","getChunkSizeBytes","getMaxClientUploadBytesPerSecond","getRawBodyLimitBytes","SessionUser","Uploads","api","chunkSizeBytes","use","InitRoute","limitBytes","maxClientBytesPerSecond","post","put","ChunkRoute","get","StatusRoute","CompleteRoute"],"sources":["../src/uploads/api/file-uploads/processors/sanitizeSvg.ts","../src/uploads/api/file-uploads/processors/index.ts","../src/uploads/api/file-uploads/handlers/completeUpload.ts","../src/uploads/api/file-uploads/handlers/getStatus.ts","../src/uploads/api/file-uploads/index.ts","../src/uploads/api/file-uploads/handlers/initUpload.ts","../src/uploads/api/file-uploads/handlers/uploadChunk.ts","../src/uploads/api/file-uploads/middleware/rawBodyParser.ts","../src/uploads/api/file-uploads/handler.ts"],"sourcesContent":["import { JSDOM } from \"jsdom\"\nimport createDOMPurify from \"dompurify\"\n\nimport type { UploadFileProcessor } from \"./index\"\n\n\nconst MAX_SVG_BYTES = 128 * 1024\n\nconst window = new JSDOM(\"\").window\nconst DOMPurify = createDOMPurify(window)\n\nconst normalizeForSniff = (raw: string): string => raw.replace(/^\\uFEFF/, \"\").trimStart()\n\nconst looksLikeSvgText = (text: string): boolean => {\n const normalized = normalizeForSniff(text)\n if (!normalized.startsWith(\"<\")) return false\n return /<svg(?:\\s|>)/i.test(normalized)\n}\n\nexport const looksLikeSvg = (sniff: Buffer): boolean => looksLikeSvgText(sniff.toString(\"utf8\"))\n\nexport const sanitizeSvg = (svg: string): string =>\n DOMPurify.sanitize(svg, {\n USE_PROFILES: { svg: true, svgFilters: true },\n })\n\nexport const sanitizeSvgProcessor: UploadFileProcessor = {\n id: \"sanitize-svg\",\n maxBytes: MAX_SVG_BYTES,\n match: ({ sniff }) => looksLikeSvg(sniff),\n process: (data): { data: Buffer; mimeType: string } => {\n if (data.length > MAX_SVG_BYTES) {\n throw new Error(\"svg_too_large\")\n }\n\n const svgText = data.toString(\"utf8\")\n if (!looksLikeSvgText(svgText)) {\n throw new Error(\"svg_invalid\")\n }\n\n const sanitized = sanitizeSvg(svgText)\n if (!sanitized.trim() || !looksLikeSvgText(sanitized)) {\n throw new Error(\"svg_sanitize_failed\")\n }\n\n const sanitizedBuffer = Buffer.from(sanitized, \"utf8\")\n if (sanitizedBuffer.length > MAX_SVG_BYTES) {\n throw new Error(\"svg_too_large\")\n }\n\n return { data: sanitizedBuffer, mimeType: \"image/svg+xml\" }\n },\n}\n\n","import { convertHeifToWebpProcessor } from \"./convertHeifToWebp\"\nimport { sanitizeSvgProcessor } from \"./sanitizeSvg\"\n\n\nexport type UploadFileProcessorContext = {\n filename: string\n clientMimeType: string\n totalSize: number\n sniff: Buffer\n}\n\nexport type UploadFileProcessorResult = {\n data: Buffer\n filename?: string\n mimeType?: string\n metadata?: Record<string, unknown>\n}\n\nexport type UploadFileProcessor = {\n id: string\n maxBytes: number\n match: (ctx: UploadFileProcessorContext) => boolean\n process: (data: Buffer, ctx: UploadFileProcessorContext) => Promise<UploadFileProcessorResult> | UploadFileProcessorResult\n}\n\nexport const uploadProcessors = Object.freeze([sanitizeSvgProcessor, convertHeifToWebpProcessor] satisfies UploadFileProcessor[])\n\nexport const getMaxUploadProcessorBytes = (): number =>\n uploadProcessors.reduce((max, processor) => Math.max(max, processor.maxBytes), 0)\n\nexport const selectUploadProcessors = (ctx: UploadFileProcessorContext): UploadFileProcessor[] =>\n uploadProcessors.filter((processor) => processor.match(ctx))\n\nexport const applyUploadProcessors = async (\n data: Buffer,\n ctx: Omit<UploadFileProcessorContext, \"sniff\" | \"totalSize\">,\n): Promise<{ data: Buffer; filename: string; mimeType: string; applied: string[]; metadata: Record<string, unknown> }> => {\n let currentData = data\n let currentFilename = ctx.filename\n let currentMimeType = ctx.clientMimeType\n const applied: string[] = []\n const metadata: Record<string, unknown> = {}\n\n for (const processor of uploadProcessors) {\n const processorCtx: UploadFileProcessorContext = {\n filename: currentFilename,\n clientMimeType: currentMimeType,\n totalSize: currentData.length,\n sniff: currentData,\n }\n\n if (!processor.match(processorCtx)) continue\n\n if (currentData.length > processor.maxBytes) {\n throw new Error(\"processor_input_too_large\")\n }\n\n const result = await processor.process(currentData, processorCtx)\n currentData = result.data\n if (typeof result.filename === \"string\" && result.filename.trim()) {\n currentFilename = result.filename.trim()\n }\n if (typeof result.mimeType === \"string\" && result.mimeType.trim()) {\n currentMimeType = result.mimeType.trim()\n }\n if (result.metadata && typeof result.metadata === \"object\") {\n Object.assign(metadata, result.metadata)\n }\n applied.push(processor.id)\n }\n\n return {\n data: currentData,\n filename: currentFilename,\n mimeType: currentMimeType,\n applied,\n metadata,\n }\n}\n","import { createHash } from \"node:crypto\"\n\nimport { ApiHandler } from \"@rpcbase/api\"\nimport { getTenantFilesystemDb, models } from \"@rpcbase/db\"\nimport { GridFSBucket, ObjectId } from \"mongodb\"\nimport type { Model } from \"mongoose\"\n\nimport * as Uploads from \"../index\"\nimport {\n claimUploadFileReference,\n createUploadFileReference,\n} from \"../fileReferences\"\nimport { enqueueUploadPostProcessors } from \"../postProcessors\"\nimport { applyUploadProcessors, getMaxUploadProcessorBytes, selectUploadProcessors } from \"../processors\"\nimport {\n type SessionUser,\n type UploadChunkDoc,\n type UploadSessionDoc,\n buildUploadAccessMetadata,\n buildUploadsAbility,\n ensureFilesystemUploadIndexes,\n ensureUploadIndexes,\n getBucketName,\n getModelCtx,\n getUploadSessionAccessQuery,\n getTenantId,\n resolveUploadAccessScope,\n} from \"../shared\"\n\n\nconst waitForStreamFinished = async (stream: NodeJS.WritableStream): Promise<void> => new Promise((resolve, reject) => {\n stream.once(\"finish\", resolve)\n stream.once(\"error\", reject)\n})\n\nconst writeToStream = async (stream: NodeJS.WritableStream, chunk: Buffer): Promise<void> => {\n const ok = stream.write(chunk)\n if (ok) return\n await new Promise<void>((resolve, reject) => {\n const onDrain = () => {\n cleanup()\n resolve()\n }\n\n const onError = (error: unknown) => {\n cleanup()\n reject(error)\n }\n\n const cleanup = () => {\n stream.off(\"drain\", onDrain)\n stream.off(\"error\", onError)\n }\n\n stream.on(\"drain\", onDrain)\n stream.on(\"error\", onError)\n })\n}\n\nconst abortUploadStream = async (stream: unknown): Promise<void> => {\n if (!stream) return\n if (typeof (stream as { abort?: unknown }).abort === \"function\") {\n try {\n await (stream as { abort: () => Promise<void> | void }).abort()\n return\n } catch {\n //\n }\n }\n try {\n ;(stream as { destroy?: () => void }).destroy?.()\n } catch {\n //\n }\n}\n\nconst isDuplicateKeyError = (error: unknown): boolean => (\n error !== null &&\n typeof error === \"object\" &&\n \"code\" in error &&\n (error as { code?: unknown }).code === 11000\n)\n\nexport const completeUpload: ApiHandler<Record<string, never>, Uploads.CompleteResponsePayload, SessionUser> = async (\n _payload,\n ctx,\n): Promise<Uploads.CompleteResponsePayload> => {\n const tenantId = getTenantId(ctx)\n if (!tenantId) {\n ctx.res.status(400)\n return { ok: false, error: \"tenant_missing\" }\n }\n\n const uploadId = String(ctx.req.params?.uploadId ?? \"\").trim()\n if (!uploadId) {\n ctx.res.status(400)\n return { ok: false, error: \"invalid_upload_id\" }\n }\n\n const ability = buildUploadsAbility(ctx, tenantId)\n const modelCtx = getModelCtx(ctx, tenantId, ability)\n\n const [UploadSession, UploadChunk] = await Promise.all([\n models.get(\"RBUploadSession\", modelCtx) as Promise<Model<UploadSessionDoc>>,\n models.get(\"RBUploadChunk\", modelCtx) as Promise<Model<UploadChunkDoc>>,\n ])\n\n if (!ability.can(\"update\", \"RBUploadSession\")) {\n ctx.res.status(401)\n return { ok: false, error: \"unauthorized\" }\n }\n\n const existing = await UploadSession.findOne({ $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"read\")] }).lean()\n if (!existing) {\n ctx.res.status(404)\n return { ok: false, error: \"not_found\" }\n }\n\n if (existing.status === \"done\" && existing.fileId) {\n return {\n ok: true,\n fileId: existing.fileId,\n ...(existing.deduplicated ? { deduplicated: true } : {}),\n }\n }\n\n const locked = await UploadSession.findOneAndUpdate(\n { $and: [{ _id: uploadId }, { status: \"uploading\" }, getUploadSessionAccessQuery(ability, \"update\")] },\n { $set: { status: \"assembling\" }, $unset: { error: \"\" } },\n { returnDocument: \"after\" },\n ).lean()\n\n if (!locked) {\n ctx.res.status(409)\n return { ok: false, error: \"not_uploading\" }\n }\n\n await ensureUploadIndexes(UploadSession, UploadChunk)\n\n const fsDb = await getTenantFilesystemDb(tenantId)\n const nativeDb = fsDb.db\n if (!nativeDb) {\n await UploadSession.updateOne(\n { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] },\n { $set: { status: \"error\", error: \"filesystem_db_unavailable\" } },\n )\n ctx.res.status(500)\n return { ok: false, error: \"assembly_failed\" }\n }\n const bucketName = getBucketName()\n const bucket = new GridFSBucket(nativeDb, { bucketName })\n if (locked.deduplication) {\n await ensureFilesystemUploadIndexes(nativeDb, bucketName)\n }\n\n const lockedUserId = typeof locked.userId === \"string\" ? locked.userId : undefined\n const accessMetadata = buildUploadAccessMetadata(locked)\n const unclaimedDeduplication = locked.deduplication\n ? {\n deduplication: {\n namespace: locked.deduplication.namespace,\n algorithm: locked.deduplication.algorithm,\n hash: locked.deduplication.hash,\n pipelineVersion: locked.deduplication.pipelineVersion,\n },\n }\n : {}\n const createFileMetadata = (\n storedMimeType: string,\n storedSize: number,\n additional: Record<string, unknown> = {},\n ): Record<string, unknown> => ({\n uploadId,\n tenantId,\n mimeType: storedMimeType,\n totalSize: storedSize,\n ...additional,\n ...accessMetadata,\n ...unclaimedDeduplication,\n })\n const maxProcessorBytes = getMaxUploadProcessorBytes()\n const shouldBufferForProcessing = locked.totalSize <= maxProcessorBytes\n const declaredMimeType = locked.mimeType.trim().toLowerCase()\n const declaredSvg = declaredMimeType === \"image/svg+xml\" || locked.filename.trim().toLowerCase().endsWith(\".svg\")\n\n let uploadStream: NodeJS.WritableStream | null = null\n let finalFilename = locked.filename\n let finalMimeType = locked.mimeType\n let finalSize = locked.totalSize\n let inlineProcessors: string[] = []\n let finalMetadata = createFileMetadata(locked.mimeType, locked.totalSize)\n\n try {\n if (!shouldBufferForProcessing && declaredSvg) {\n throw new Error(\"svg_too_large\")\n }\n\n const cursor = UploadChunk.find({ uploadId }).sort({ index: 1 }).cursor() as unknown as AsyncIterable<UploadChunkDoc> & {\n close: () => Promise<void>\n }\n\n let expectedIndex = 0\n let sourceBytes = 0\n const sourceHasher = locked.deduplication ? createHash(\"sha256\") : null\n const chunks: Buffer[] = []\n let bufferedBytes = 0\n\n const pendingChunks: Buffer[] = []\n const sniffParts: Buffer[] = []\n let sniffBytes = 0\n\n try {\n for await (const chunkDoc of cursor) {\n if (chunkDoc.index !== expectedIndex) {\n throw new Error(\"missing_chunks\")\n }\n\n const chunk = chunkDoc.data\n sourceBytes += chunk.length\n sourceHasher?.update(chunk)\n\n if (shouldBufferForProcessing) {\n chunks.push(chunk)\n bufferedBytes += chunk.length\n } else if (!uploadStream) {\n pendingChunks.push(chunk)\n\n if (sniffBytes < maxProcessorBytes) {\n const slice = chunk.subarray(0, Math.min(chunk.length, maxProcessorBytes - sniffBytes))\n if (slice.length) {\n sniffParts.push(slice)\n sniffBytes += slice.length\n }\n }\n\n if (sniffBytes >= maxProcessorBytes) {\n const sniff = Buffer.concat(sniffParts, sniffBytes)\n const processors = selectUploadProcessors({\n filename: locked.filename,\n clientMimeType: locked.mimeType,\n totalSize: locked.totalSize,\n sniff,\n })\n\n if (processors.length) {\n throw new Error(processors.some((processor) => processor.id === \"sanitize-svg\") ? \"svg_too_large\" : \"processor_input_too_large\")\n }\n\n finalMetadata = createFileMetadata(locked.mimeType, locked.totalSize)\n uploadStream = bucket.openUploadStream(locked.filename, {\n metadata: finalMetadata,\n })\n\n for (const pending of pendingChunks) {\n await writeToStream(uploadStream, pending)\n }\n pendingChunks.length = 0\n }\n } else {\n await writeToStream(uploadStream, chunk)\n }\n\n expectedIndex += 1\n }\n } finally {\n try {\n await cursor.close()\n } catch {\n //\n }\n }\n\n if (expectedIndex !== locked.chunksTotal) {\n throw new Error(\"missing_chunks\")\n }\n\n if (sourceBytes !== locked.totalSize) {\n throw new Error(\"invalid_source_size\")\n }\n\n if (sourceHasher && sourceHasher.digest(\"hex\") !== locked.deduplication?.hash) {\n throw new Error(\"content_hash_mismatch\")\n }\n\n if (shouldBufferForProcessing) {\n const assembled = Buffer.concat(chunks, bufferedBytes)\n const {\n data: processed,\n filename: processedFilename,\n mimeType: processedMimeType,\n applied,\n metadata: processedMetadata,\n } = await applyUploadProcessors(assembled, {\n filename: locked.filename,\n clientMimeType: locked.mimeType,\n })\n\n finalFilename = processedFilename\n finalMimeType = processedMimeType\n finalSize = processed.length\n inlineProcessors = applied\n finalMetadata = createFileMetadata(processedMimeType, processed.length, {\n ...(applied.length ? { processors: applied, sourceTotalSize: locked.totalSize } : {}),\n ...processedMetadata,\n })\n uploadStream = bucket.openUploadStream(finalFilename, {\n metadata: finalMetadata,\n })\n\n const finished = waitForStreamFinished(uploadStream)\n uploadStream.end(processed)\n await finished\n } else {\n if (!uploadStream) {\n const sniff = Buffer.concat(sniffParts, sniffBytes)\n const processors = selectUploadProcessors({\n filename: locked.filename,\n clientMimeType: locked.mimeType,\n totalSize: locked.totalSize,\n sniff,\n })\n\n if (processors.length) {\n throw new Error(processors.some((processor) => processor.id === \"sanitize-svg\") ? \"svg_too_large\" : \"processor_input_too_large\")\n }\n\n finalMetadata = createFileMetadata(locked.mimeType, locked.totalSize)\n uploadStream = bucket.openUploadStream(locked.filename, {\n metadata: finalMetadata,\n })\n\n for (const pending of pendingChunks) {\n await writeToStream(uploadStream, pending)\n }\n pendingChunks.length = 0\n }\n\n const finished = waitForStreamFinished(uploadStream)\n uploadStream.end()\n await finished\n }\n\n const uploadedBlobId = String((uploadStream as unknown as { id?: unknown }).id ?? \"\")\n if (!uploadedBlobId) {\n throw new Error(\"missing_file_id\")\n }\n\n let resolvedBlobId = uploadedBlobId\n let deduplicated = false\n let returnedFileId: string | null = locked.deduplication ? null : uploadedBlobId\n\n if (locked.deduplication) {\n const filesCollection = nativeDb.collection(`${bucketName}.files`)\n const uploadedBlobObjectId = new ObjectId(uploadedBlobId)\n const referenceOptions = {\n tenantId,\n bucketName,\n filename: finalFilename,\n mimeType: finalMimeType,\n totalSize: finalSize,\n accessScope: resolveUploadAccessScope({\n accessScope: locked.accessScope,\n isPublic: locked.isPublic,\n }),\n ...(lockedUserId ? { userId: lockedUserId } : {}),\n ...(typeof locked.ownerKeyHash === \"string\" ? { ownerKeyHash: locked.ownerKeyHash } : {}),\n }\n let lastDuplicateKeyError: unknown = null\n\n for (let attempt = 0; attempt < 3 && !returnedFileId; attempt += 1) {\n try {\n returnedFileId = await claimUploadFileReference({\n ...referenceOptions,\n blobId: uploadedBlobObjectId,\n deduplication: locked.deduplication,\n })\n if (!returnedFileId) throw new Error(\"missing_file_id\")\n finalMetadata = {\n ...finalMetadata,\n deduplication: locked.deduplication,\n }\n break\n } catch (error) {\n if (!isDuplicateKeyError(error)) throw error\n lastDuplicateKeyError = error\n }\n\n const canonicalFile = await filesCollection.findOne(\n { \"metadata.deduplication.key\": locked.deduplication.key },\n { projection: { _id: 1 } },\n )\n if (!(canonicalFile?._id instanceof ObjectId)) continue\n\n const canonicalReferenceId = await createUploadFileReference({\n ...referenceOptions,\n blobId: canonicalFile._id,\n deduplicationKey: locked.deduplication.key,\n })\n if (!canonicalReferenceId) continue\n\n await bucket.delete(uploadedBlobObjectId)\n resolvedBlobId = canonicalFile._id.toHexString()\n returnedFileId = canonicalReferenceId\n deduplicated = true\n }\n\n if (!returnedFileId) {\n await bucket.delete(uploadedBlobObjectId)\n if (lastDuplicateKeyError) throw lastDuplicateKeyError\n }\n }\n if (!returnedFileId) {\n throw new Error(\"canonical_file_missing\")\n }\n\n await UploadSession.updateOne(\n { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] },\n {\n $set: {\n status: \"done\",\n fileId: returnedFileId,\n filename: finalFilename,\n mimeType: finalMimeType,\n totalSize: finalSize,\n ...(deduplicated ? { deduplicated: true } : {}),\n },\n $unset: { error: \"\" },\n },\n )\n\n if (!deduplicated) {\n await enqueueUploadPostProcessors({\n tenantId,\n uploadId,\n fileId: resolvedBlobId,\n filename: finalFilename,\n mimeType: finalMimeType,\n clientMimeType: locked.mimeType,\n totalSize: finalSize,\n ...(typeof locked.isPublic === \"boolean\" ? { isPublic: locked.isPublic } : {}),\n ...(locked.accessScope ? { accessScope: locked.accessScope } : {}),\n ...(typeof locked.ownerKeyHash === \"string\" ? { ownerKeyHash: locked.ownerKeyHash } : {}),\n ...(lockedUserId ? { userId: lockedUserId } : {}),\n inlineProcessors,\n metadata: finalMetadata,\n }).catch((error) => {\n console.error(\"Upload post processor enqueue failed\", {\n tenantId,\n uploadId,\n fileId: resolvedBlobId,\n error,\n })\n })\n }\n\n try {\n await UploadChunk.deleteMany({ uploadId })\n } catch {\n //\n }\n\n return {\n ok: true,\n fileId: returnedFileId,\n ...(deduplicated ? { deduplicated: true } : {}),\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n\n await abortUploadStream(uploadStream)\n\n if (message === \"missing_chunks\") {\n await UploadSession.updateOne(\n { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] },\n { $set: { status: \"uploading\" } },\n )\n ctx.res.status(409)\n return { ok: false, error: \"missing_chunks\" }\n }\n\n if (message === \"content_hash_mismatch\") {\n await UploadSession.updateOne(\n { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] },\n { $set: { status: \"error\", error: message } },\n )\n ctx.res.status(400)\n return { ok: false, error: message }\n }\n\n if (message === \"svg_too_large\") {\n await UploadSession.updateOne(\n { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] },\n { $set: { status: \"error\", error: message } },\n )\n ctx.res.status(413)\n return { ok: false, error: message }\n }\n\n if (message === \"heif_too_large\" || message === \"processor_input_too_large\") {\n await UploadSession.updateOne(\n { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] },\n { $set: { status: \"error\", error: message } },\n )\n ctx.res.status(413)\n return { ok: false, error: message }\n }\n\n if (message.startsWith(\"heif_decode_unsupported\")) {\n await UploadSession.updateOne(\n { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] },\n { $set: { status: \"error\", error: message } },\n )\n ctx.res.status(415)\n return { ok: false, error: message }\n }\n\n if (message === \"svg_invalid\" || message === \"svg_sanitize_failed\") {\n await UploadSession.updateOne(\n { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] },\n { $set: { status: \"error\", error: message } },\n )\n ctx.res.status(400)\n return { ok: false, error: message }\n }\n\n await UploadSession.updateOne(\n { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] },\n { $set: { status: \"error\", error: message } },\n )\n\n ctx.res.status(500)\n return { ok: false, error: \"assembly_failed\" }\n }\n}\n","import { ApiHandler } from \"@rpcbase/api\"\nimport { models } from \"@rpcbase/db\"\nimport type { Model } from \"mongoose\"\n\nimport * as Uploads from \"../index\"\nimport {\n type SessionUser,\n type UploadChunkDoc,\n type UploadSessionDoc,\n buildUploadsAbility,\n getModelCtx,\n getUploadSessionAccessQuery,\n getTenantId,\n} from \"../shared\"\n\n\nexport const getStatus: ApiHandler<Record<string, never>, Uploads.StatusResponsePayload, SessionUser> = async (\n _payload,\n ctx,\n): Promise<Uploads.StatusResponsePayload> => {\n const tenantId = getTenantId(ctx)\n if (!tenantId) {\n ctx.res.status(400)\n return { ok: false, error: \"tenant_missing\" }\n }\n\n const uploadId = String(ctx.req.params?.uploadId ?? \"\").trim()\n if (!uploadId) {\n ctx.res.status(400)\n return { ok: false, error: \"invalid_upload_id\" }\n }\n\n const ability = buildUploadsAbility(ctx, tenantId)\n const modelCtx = getModelCtx(ctx, tenantId, ability)\n\n const [UploadSession, UploadChunk] = await Promise.all([\n models.get(\"RBUploadSession\", modelCtx) as Promise<Model<UploadSessionDoc>>,\n models.get(\"RBUploadChunk\", modelCtx) as Promise<Model<UploadChunkDoc>>,\n ])\n\n if (!ability.can(\"read\", \"RBUploadSession\")) {\n ctx.res.status(401)\n return { ok: false, error: \"unauthorized\" }\n }\n\n const session = await UploadSession.findOne({ $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"read\")] }).lean()\n if (!session) {\n ctx.res.status(404)\n return { ok: false, error: \"not_found\" }\n }\n\n const receivedDocs = await UploadChunk.find(\n { uploadId },\n { index: 1, _id: 0 },\n ).sort({ index: 1 }).lean()\n\n const received = (receivedDocs as unknown as Array<{ index?: unknown }>)\n .map((doc) => (typeof doc.index === \"number\" ? doc.index : -1))\n .filter((n) => Number.isInteger(n) && n >= 0)\n\n return {\n ok: true,\n status: session.status,\n chunkSize: session.chunkSize,\n chunksTotal: session.chunksTotal,\n received,\n ...(session.fileId ? { fileId: session.fileId } : {}),\n }\n}\n","import { z } from \"zod\"\n\n\nexport const InitRoute = \"/api/rb/file-uploads\"\nexport const ChunkRoute = \"/api/rb/file-uploads/:uploadId/chunks/:index\"\nexport const StatusRoute = \"/api/rb/file-uploads/:uploadId/status\"\nexport const CompleteRoute = \"/api/rb/file-uploads/:uploadId/complete\"\n\nexport const uploadAccessScopeSchema = z.enum([\"owner\", \"tenant\", \"public\"])\n\nexport const uploadDeduplicationSchema = z.object({\n namespace: z.string().trim().min(1).max(128).regex(/^[a-z0-9][a-z0-9._:-]*$/i),\n algorithm: z.literal(\"sha256\"),\n hash: z.string().trim().regex(/^[a-f0-9]{64}$/i),\n})\n\nexport const initRequestSchema = z.object({\n filename: z.string().min(1),\n mimeType: z.string().min(1),\n isPublic: z.boolean().optional(),\n accessScope: uploadAccessScopeSchema.optional(),\n deduplication: uploadDeduplicationSchema.optional(),\n totalSize: z.number().int().min(1),\n}).superRefine((value, ctx) => {\n if (value.accessScope !== undefined && value.isPublic !== undefined) {\n ctx.addIssue({\n code: \"custom\",\n message: \"accessScope and isPublic are mutually exclusive\",\n path: [\"accessScope\"],\n })\n }\n})\n\nexport type InitRequestPayload = z.infer<typeof initRequestSchema>\n\nexport const initResponseSchema = z.object({\n ok: z.boolean(),\n error: z.string().optional(),\n uploadId: z.string().optional(),\n uploadKey: z.string().optional(),\n chunkSize: z.number().int().optional(),\n chunksTotal: z.number().int().optional(),\n fileId: z.string().optional(),\n deduplicated: z.boolean().optional(),\n})\n\nexport type InitResponsePayload = z.infer<typeof initResponseSchema>\n\nexport const statusResponseSchema = z.object({\n ok: z.boolean(),\n error: z.string().optional(),\n status: z.enum([\"uploading\", \"assembling\", \"done\", \"error\"]).optional(),\n chunkSize: z.number().int().optional(),\n chunksTotal: z.number().int().optional(),\n received: z.array(z.number().int().min(0)).optional(),\n fileId: z.string().optional(),\n})\n\nexport type StatusResponsePayload = z.infer<typeof statusResponseSchema>\n\nexport const completeResponseSchema = z.object({\n ok: z.boolean(),\n error: z.string().optional(),\n fileId: z.string().optional(),\n deduplicated: z.boolean().optional(),\n})\n\nexport type CompleteResponsePayload = z.infer<typeof completeResponseSchema>\n","import { randomBytes } from \"node:crypto\"\n\nimport { ApiHandler } from \"@rpcbase/api\"\nimport { getTenantFilesystemDb, models } from \"@rpcbase/db\"\nimport { ObjectId } from \"mongodb\"\nimport type { Model } from \"mongoose\"\n\nimport * as Uploads from \"../index\"\nimport { createUploadFileReference } from \"../fileReferences\"\nimport {\n type SessionUser,\n type UploadChunkDoc,\n type UploadSessionDoc,\n buildUploadDeduplication,\n buildUploadsAbility,\n computeSha256Hex,\n ensureFilesystemUploadIndexes,\n ensureUploadIndexes,\n getBucketName,\n getChunkSizeBytes,\n getModelCtx,\n getSessionTtlMs,\n getTenantId,\n getUserId,\n resolveUploadAccessScope,\n} from \"../shared\"\n\n\nexport const initUpload: ApiHandler<Uploads.InitRequestPayload, Uploads.InitResponsePayload, SessionUser> = async (\n payload,\n ctx,\n): Promise<Uploads.InitResponsePayload> => {\n const tenantId = getTenantId(ctx)\n if (!tenantId) {\n ctx.res.status(400)\n return { ok: false, error: \"tenant_missing\" }\n }\n\n const userId = getUserId(ctx)\n\n const parsed = Uploads.initRequestSchema.safeParse(payload ?? {})\n if (!parsed.success) {\n ctx.res.status(400)\n return { ok: false, error: \"invalid_payload\" }\n }\n\n const { filename, mimeType, totalSize, isPublic, deduplication: requestedDeduplication } = parsed.data\n const accessScope = resolveUploadAccessScope({ accessScope: parsed.data.accessScope, isPublic })\n if (accessScope === \"tenant\" && !userId) {\n ctx.res.status(401)\n return { ok: false, error: \"authentication_required\" }\n }\n\n const uploadKey = userId ? null : randomBytes(32).toString(\"base64url\")\n const ownerKeyHash = uploadKey ? computeSha256Hex(Buffer.from(uploadKey)) : undefined\n const deduplication = requestedDeduplication\n ? buildUploadDeduplication({\n namespace: requestedDeduplication.namespace,\n hash: requestedDeduplication.hash,\n filename,\n mimeType,\n totalSize,\n accessScope,\n ...(userId ? { userId } : {}),\n ...(ownerKeyHash ? { ownerKeyHash } : {}),\n })\n : null\n\n if (requestedDeduplication && !deduplication) {\n ctx.res.status(400)\n return { ok: false, error: \"deduplication_unavailable\" }\n }\n\n if (deduplication) {\n const fsDb = await getTenantFilesystemDb(tenantId)\n const nativeDb = fsDb.db\n if (!nativeDb) {\n ctx.res.status(500)\n return { ok: false, error: \"filesystem_db_unavailable\" }\n }\n\n const bucketName = getBucketName()\n await ensureFilesystemUploadIndexes(nativeDb, bucketName)\n const existingFile = await nativeDb.collection(`${bucketName}.files`).findOne(\n { \"metadata.deduplication.key\": deduplication.key },\n { projection: { _id: 1, filename: 1, length: 1, metadata: 1 } },\n )\n if (existingFile?._id instanceof ObjectId) {\n const existingMetadata = existingFile.metadata && typeof existingFile.metadata === \"object\"\n ? existingFile.metadata as Record<string, unknown>\n : {}\n const existingMimeType = typeof existingMetadata.mimeType === \"string\"\n ? existingMetadata.mimeType\n : mimeType\n const existingSize = typeof existingFile.length === \"number\"\n ? existingFile.length\n : totalSize\n const fileId = await createUploadFileReference({\n tenantId,\n bucketName,\n blobId: existingFile._id,\n filename,\n mimeType: existingMimeType,\n totalSize: existingSize,\n accessScope,\n ...(userId ? { userId } : {}),\n ...(ownerKeyHash ? { ownerKeyHash } : {}),\n deduplicationKey: deduplication.key,\n })\n if (fileId) {\n return {\n ok: true,\n fileId,\n deduplicated: true,\n ...(uploadKey ? { uploadKey } : {}),\n }\n }\n }\n }\n\n const chunkSize = getChunkSizeBytes()\n const chunksTotal = Math.ceil(totalSize / chunkSize)\n\n const ability = buildUploadsAbility(ctx, tenantId)\n const modelCtx = getModelCtx(ctx, tenantId, ability)\n\n const [UploadSession, UploadChunk] = await Promise.all([\n models.get(\"RBUploadSession\", modelCtx) as Promise<Model<UploadSessionDoc>>,\n models.get(\"RBUploadChunk\", modelCtx) as Promise<Model<UploadChunkDoc>>,\n ])\n\n await ensureUploadIndexes(UploadSession, UploadChunk)\n\n const uploadId = new ObjectId().toString()\n const now = Date.now()\n const expiresAt = new Date(now + getSessionTtlMs())\n\n await UploadSession.create({\n _id: uploadId,\n ...(userId ? { userId } : {}),\n ...(ownerKeyHash ? { ownerKeyHash } : {}),\n filename,\n mimeType,\n isPublic: accessScope === \"public\",\n accessScope,\n ...(deduplication ? { deduplication } : {}),\n totalSize,\n chunkSize,\n chunksTotal,\n status: \"uploading\",\n createdAt: new Date(now),\n expiresAt,\n })\n\n return {\n ok: true,\n uploadId,\n chunkSize,\n chunksTotal,\n ...(uploadKey ? { uploadKey } : {}),\n }\n}\n","import { ApiHandler } from \"@rpcbase/api\"\nimport { models } from \"@rpcbase/db\"\nimport type { Model } from \"mongoose\"\n\nimport {\n type SessionUser,\n type UploadChunkDoc,\n type UploadSessionDoc,\n buildUploadsAbility,\n computeSha256Hex,\n ensureUploadIndexes,\n getModelCtx,\n getUploadSessionAccessQuery,\n getTenantId,\n normalizeSha256Hex,\n toBufferPayload,\n} from \"../shared\"\n\n\ntype ChunkResponsePayload = {\n ok: boolean\n error?: string\n}\n\nexport const uploadChunk: ApiHandler<Buffer, ChunkResponsePayload, SessionUser> = async (\n payload,\n ctx,\n): Promise<ChunkResponsePayload> => {\n const tenantId = getTenantId(ctx)\n if (!tenantId) {\n ctx.res.status(400)\n return { ok: false, error: \"tenant_missing\" }\n }\n\n const uploadId = String(ctx.req.params?.uploadId ?? \"\").trim()\n const indexRaw = String(ctx.req.params?.index ?? \"\").trim()\n const index = Number(indexRaw)\n\n if (!uploadId || !Number.isInteger(index) || index < 0) {\n ctx.res.status(400)\n return { ok: false, error: \"invalid_chunk_ref\" }\n }\n\n const ability = buildUploadsAbility(ctx, tenantId)\n const modelCtx = getModelCtx(ctx, tenantId, ability)\n\n const [UploadSession, UploadChunk] = await Promise.all([\n models.get(\"RBUploadSession\", modelCtx) as Promise<Model<UploadSessionDoc>>,\n models.get(\"RBUploadChunk\", modelCtx) as Promise<Model<UploadChunkDoc>>,\n ])\n\n if (!ability.can(\"update\", \"RBUploadSession\")) {\n ctx.res.status(401)\n return { ok: false, error: \"unauthorized\" }\n }\n\n const session = await UploadSession.findOne({ $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] }).lean()\n if (!session) {\n ctx.res.status(404)\n return { ok: false, error: \"not_found\" }\n }\n\n if (session.status !== \"uploading\") {\n ctx.res.status(409)\n return { ok: false, error: \"not_uploading\" }\n }\n\n if (index >= session.chunksTotal) {\n ctx.res.status(400)\n return { ok: false, error: \"index_out_of_range\" }\n }\n\n const data = toBufferPayload(payload)\n if (!data) {\n ctx.res.status(400)\n return { ok: false, error: \"invalid_body\" }\n }\n\n const expectedSize = index === session.chunksTotal - 1\n ? session.totalSize - session.chunkSize * (session.chunksTotal - 1)\n : session.chunkSize\n\n if (data.length > expectedSize) {\n ctx.res.status(413)\n return { ok: false, error: \"chunk_too_large\" }\n }\n\n if (data.length !== expectedSize) {\n ctx.res.status(400)\n return { ok: false, error: \"invalid_chunk_size\" }\n }\n\n const checksumHeader = ctx.req.get(\"X-Chunk-SHA256\")\n const sha256 = checksumHeader ? computeSha256Hex(data) : undefined\n\n if (checksumHeader) {\n const expectedSha256 = normalizeSha256Hex(checksumHeader)\n if (sha256 !== expectedSha256) {\n ctx.res.status(400)\n return { ok: false, error: \"checksum_mismatch\" }\n }\n }\n\n await ensureUploadIndexes(UploadSession, UploadChunk)\n\n await UploadChunk.updateOne(\n { uploadId, index },\n {\n $set: {\n uploadId,\n index,\n data,\n size: data.length,\n sha256,\n expiresAt: session.expiresAt,\n },\n $setOnInsert: {\n createdAt: new Date(),\n },\n },\n { upsert: true },\n )\n\n ctx.res.status(204)\n return { ok: true }\n}\n","export const rawBodyParser = ({\n limitBytes,\n maxClientBytesPerSecond,\n}: {\n limitBytes: number\n maxClientBytesPerSecond?: number | null\n}) => {\n return (req: any, res: any, next: any) => {\n const contentType = typeof req?.headers?.[\"content-type\"] === \"string\"\n ? String(req.headers[\"content-type\"])\n : \"\"\n\n if (!contentType.includes(\"application/octet-stream\")) {\n next()\n return\n }\n\n let total = 0\n const chunks: Buffer[] = []\n let done = false\n let paused = false\n let throttleTimeout: ReturnType<typeof setTimeout> | null = null\n\n const rateBytesPerSecond = typeof maxClientBytesPerSecond === \"number\" && maxClientBytesPerSecond > 0\n ? maxClientBytesPerSecond\n : null\n\n const cleanup = () => {\n req.off(\"data\", onData)\n req.off(\"end\", onEnd)\n req.off(\"error\", onError)\n req.off(\"aborted\", onAborted)\n if (throttleTimeout) {\n clearTimeout(throttleTimeout)\n throttleTimeout = null\n }\n }\n\n const finish = (error?: unknown) => {\n if (done) return\n done = true\n\n cleanup()\n\n if (error) {\n next(error)\n return\n }\n\n req.body = Buffer.concat(chunks, total)\n next()\n }\n\n const onData = (chunk: any) => {\n if (done) return\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)\n total += buffer.length\n\n if (total > limitBytes) {\n done = true\n cleanup()\n req.destroy()\n res.status(413).json({ ok: false, error: \"chunk_too_large\" })\n return\n }\n\n chunks.push(buffer)\n\n if (!rateBytesPerSecond) return\n\n const now = Date.now()\n const clientKey = getClientKey(req)\n const state = getClientRateState(clientKey, rateBytesPerSecond, now)\n const waitMs = consumeRateBudget(state, buffer.length, rateBytesPerSecond, now)\n\n if (waitMs > 0 && !paused) {\n paused = true\n req.pause()\n throttleTimeout = setTimeout(() => {\n throttleTimeout = null\n paused = false\n if (done) return\n try {\n req.resume()\n } catch {\n //\n }\n }, waitMs)\n }\n }\n\n const onEnd = () => finish()\n const onError = (err: unknown) => finish(err)\n const onAborted = () => finish(new Error(\"request_aborted\"))\n\n req.on(\"data\", onData)\n req.on(\"end\", onEnd)\n req.on(\"error\", onError)\n req.on(\"aborted\", onAborted)\n }\n}\n\ntype ClientRateState = {\n tokens: number\n lastRefillMs: number\n lastSeenMs: number\n}\n\nconst MAX_BURST_SECONDS = 1\nconst STALE_CLIENT_MS = 15 * 60 * 1000\n\nconst clientRateStates = new Map<string, ClientRateState>()\nlet lastCleanupMs = 0\n\nconst getClientKey = (req: any): string => {\n const rawClientIp = typeof req?.clientIp === \"string\" ? req.clientIp : \"\"\n if (rawClientIp.trim()) return rawClientIp.trim()\n const rawIp = typeof req?.ip === \"string\" ? req.ip : \"\"\n return rawIp.trim() || \"unknown\"\n}\n\nconst maybeCleanupStates = (now: number) => {\n if (now - lastCleanupMs < 60_000) return\n lastCleanupMs = now\n\n if (clientRateStates.size < 2000) return\n\n for (const [key, state] of clientRateStates) {\n if (now - state.lastSeenMs > STALE_CLIENT_MS) {\n clientRateStates.delete(key)\n }\n }\n}\n\nconst getClientRateState = (key: string, rateBytesPerSecond: number, now: number): ClientRateState => {\n maybeCleanupStates(now)\n\n const capacity = rateBytesPerSecond * MAX_BURST_SECONDS\n const existing = clientRateStates.get(key)\n if (existing) {\n existing.lastSeenMs = now\n existing.tokens = Math.min(capacity, existing.tokens)\n return existing\n }\n\n const next: ClientRateState = {\n tokens: capacity,\n lastRefillMs: now,\n lastSeenMs: now,\n }\n clientRateStates.set(key, next)\n return next\n}\n\nconst consumeRateBudget = (\n state: ClientRateState,\n bytes: number,\n rateBytesPerSecond: number,\n now: number,\n): number => {\n const capacity = rateBytesPerSecond * MAX_BURST_SECONDS\n const elapsedMs = Math.max(0, now - state.lastRefillMs)\n\n if (elapsedMs > 0) {\n state.tokens = Math.min(capacity, state.tokens + (elapsedMs * rateBytesPerSecond) / 1000)\n state.lastRefillMs = now\n }\n\n state.tokens -= bytes\n\n if (state.tokens >= 0) return 0\n return Math.ceil((-state.tokens / rateBytesPerSecond) * 1000)\n}\n","import { Api } from \"@rpcbase/api\"\n\nimport { completeUpload } from \"./handlers/completeUpload\"\nimport { getStatus } from \"./handlers/getStatus\"\nimport { initUpload } from \"./handlers/initUpload\"\nimport { uploadChunk } from \"./handlers/uploadChunk\"\nimport { rawBodyParser } from \"./middleware/rawBodyParser\"\nimport { getChunkSizeBytes, getMaxClientUploadBytesPerSecond, getRawBodyLimitBytes, type SessionUser } from \"./shared\"\n\nimport * as Uploads from \"./index\"\n\n\nexport default (api: Api<SessionUser>) => {\n const chunkSizeBytes = getChunkSizeBytes()\n api.use(\n Uploads.InitRoute,\n rawBodyParser({\n limitBytes: getRawBodyLimitBytes(chunkSizeBytes),\n maxClientBytesPerSecond: getMaxClientUploadBytesPerSecond(),\n }),\n )\n\n api.post(Uploads.InitRoute, initUpload)\n api.put(Uploads.ChunkRoute, uploadChunk)\n api.get(Uploads.StatusRoute, getStatus)\n api.post(Uploads.CompleteRoute, completeUpload)\n}\n"],"mappings":"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"}