@rpcbase/server 0.594.0 → 0.595.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 (61) hide show
  1. package/dist/convertHeifToWebp-BT1n72DQ.js +148 -0
  2. package/dist/convertHeifToWebp-BT1n72DQ.js.map +1 -0
  3. package/dist/email-Co1GNjlT.js +7204 -0
  4. package/dist/email-Co1GNjlT.js.map +1 -0
  5. package/dist/handler-BqlcQ9sE.js +245 -0
  6. package/dist/handler-BqlcQ9sE.js.map +1 -0
  7. package/dist/handler-C92_gJkX.js +743 -0
  8. package/dist/handler-C92_gJkX.js.map +1 -0
  9. package/dist/handler-CapJTGzc.js +795 -0
  10. package/dist/handler-CapJTGzc.js.map +1 -0
  11. package/dist/handler-Fk-5fOSm.js +119 -0
  12. package/dist/handler-Fk-5fOSm.js.map +1 -0
  13. package/dist/htmlTemplate.d.ts +16 -0
  14. package/dist/htmlTemplate.d.ts.map +1 -0
  15. package/dist/index.d.ts +1 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +5140 -5286
  18. package/dist/index.js.map +1 -1
  19. package/dist/notifications.js +273 -382
  20. package/dist/notifications.js.map +1 -1
  21. package/dist/postProcessors-iffHvje4.js +89 -0
  22. package/dist/postProcessors-iffHvje4.js.map +1 -0
  23. package/dist/queryWindow-DP8zD0lb.js +555 -0
  24. package/dist/queryWindow-DP8zD0lb.js.map +1 -0
  25. package/dist/render_resend-B1SSQ4f7.js +7 -0
  26. package/dist/render_resend-B1SSQ4f7.js.map +1 -0
  27. package/dist/rts/index.js +990 -1207
  28. package/dist/rts/index.js.map +1 -1
  29. package/dist/schemas-B2fUvgYo.js +4151 -0
  30. package/dist/schemas-B2fUvgYo.js.map +1 -0
  31. package/dist/shared-DfrVDSp0.js +79 -0
  32. package/dist/shared-DfrVDSp0.js.map +1 -0
  33. package/dist/ssrMiddleware.d.ts +7 -3
  34. package/dist/ssrMiddleware.d.ts.map +1 -1
  35. package/dist/uploads/worker.js +139 -168
  36. package/dist/uploads/worker.js.map +1 -1
  37. package/dist/uploads.js +12 -22
  38. package/dist/uploads.js.map +1 -1
  39. package/package.json +1 -1
  40. package/dist/convertHeifToWebp-C-DGXZ2k.js +0 -169
  41. package/dist/convertHeifToWebp-C-DGXZ2k.js.map +0 -1
  42. package/dist/email-BCf24GmK.js +0 -8071
  43. package/dist/email-BCf24GmK.js.map +0 -1
  44. package/dist/handler-BqoKvylN.js +0 -147
  45. package/dist/handler-BqoKvylN.js.map +0 -1
  46. package/dist/handler-CUOJ51-w.js +0 -903
  47. package/dist/handler-CUOJ51-w.js.map +0 -1
  48. package/dist/handler-D-XdgeG_.js +0 -986
  49. package/dist/handler-D-XdgeG_.js.map +0 -1
  50. package/dist/handler-F0gFTzvh.js +0 -275
  51. package/dist/handler-F0gFTzvh.js.map +0 -1
  52. package/dist/postProcessors-D27fGZP0.js +0 -107
  53. package/dist/postProcessors-D27fGZP0.js.map +0 -1
  54. package/dist/queryWindow-Cdr7K-S1.js +0 -714
  55. package/dist/queryWindow-Cdr7K-S1.js.map +0 -1
  56. package/dist/render_resend_false-MiC__Smr.js +0 -6
  57. package/dist/render_resend_false-MiC__Smr.js.map +0 -1
  58. package/dist/schemas-Drf83ni9.js +0 -4517
  59. package/dist/schemas-Drf83ni9.js.map +0 -1
  60. package/dist/shared-CJrm9Wjp.js +0 -104
  61. package/dist/shared-CJrm9Wjp.js.map +0 -1
@@ -1,986 +0,0 @@
1
- import { models, getTenantFilesystemDb } from "@rpcbase/db";
2
- import { GridFSBucket, ObjectId } from "mongodb";
3
- import { e as enqueueUploadPostProcessors } from "./postProcessors-D27fGZP0.js";
4
- import { c as convertHeifToWebpProcessor } from "./convertHeifToWebp-C-DGXZ2k.js";
5
- import { JSDOM } from "jsdom";
6
- import createDOMPurify from "dompurify";
7
- import { a as getTenantId, b as buildUploadsAbility, c as getUploadSessionAccessQuery, e as ensureUploadIndexes, g as getBucketName, d as getModelCtx, f as getUserId, h as getChunkSizeBytes, i as getSessionTtlMs, j as computeSha256Hex, t as toBufferPayload, n as normalizeSha256Hex, k as getMaxClientUploadBytesPerSecond, l as getRawBodyLimitBytes } from "./shared-CJrm9Wjp.js";
8
- import { randomBytes } from "node:crypto";
9
- import { o as object, n as number, b as boolean, s as string, a as array, _ as _enum } from "./schemas-Drf83ni9.js";
10
- const MAX_SVG_BYTES = 128 * 1024;
11
- const window = new JSDOM("").window;
12
- const DOMPurify = createDOMPurify(window);
13
- const normalizeForSniff = (raw) => raw.replace(/^\uFEFF/, "").trimStart();
14
- const looksLikeSvgText = (text) => {
15
- const normalized = normalizeForSniff(text);
16
- if (!normalized.startsWith("<")) return false;
17
- return /<svg(?:\s|>)/i.test(normalized);
18
- };
19
- const looksLikeSvg = (sniff) => looksLikeSvgText(sniff.toString("utf8"));
20
- const sanitizeSvg = (svg) => DOMPurify.sanitize(svg, {
21
- USE_PROFILES: {
22
- svg: true,
23
- svgFilters: true
24
- }
25
- });
26
- const sanitizeSvgProcessor = {
27
- id: "sanitize-svg",
28
- maxBytes: MAX_SVG_BYTES,
29
- match: ({
30
- sniff
31
- }) => looksLikeSvg(sniff),
32
- process: (data) => {
33
- if (data.length > MAX_SVG_BYTES) {
34
- throw new Error("svg_too_large");
35
- }
36
- const svgText = data.toString("utf8");
37
- if (!looksLikeSvgText(svgText)) {
38
- throw new Error("svg_invalid");
39
- }
40
- const sanitized = sanitizeSvg(svgText);
41
- if (!sanitized.trim() || !looksLikeSvgText(sanitized)) {
42
- throw new Error("svg_sanitize_failed");
43
- }
44
- const sanitizedBuffer = Buffer.from(sanitized, "utf8");
45
- if (sanitizedBuffer.length > MAX_SVG_BYTES) {
46
- throw new Error("svg_too_large");
47
- }
48
- return {
49
- data: sanitizedBuffer,
50
- mimeType: "image/svg+xml"
51
- };
52
- }
53
- };
54
- const uploadProcessors = Object.freeze([sanitizeSvgProcessor, convertHeifToWebpProcessor]);
55
- const getMaxUploadProcessorBytes = () => uploadProcessors.reduce((max, processor) => Math.max(max, processor.maxBytes), 0);
56
- const selectUploadProcessors = (ctx) => uploadProcessors.filter((processor) => processor.match(ctx));
57
- const applyUploadProcessors = async (data, ctx) => {
58
- let currentData = data;
59
- let currentFilename = ctx.filename;
60
- let currentMimeType = ctx.clientMimeType;
61
- const applied = [];
62
- const metadata = {};
63
- for (const processor of uploadProcessors) {
64
- const processorCtx = {
65
- filename: currentFilename,
66
- clientMimeType: currentMimeType,
67
- totalSize: currentData.length,
68
- sniff: currentData
69
- };
70
- if (!processor.match(processorCtx)) continue;
71
- if (currentData.length > processor.maxBytes) {
72
- throw new Error("processor_input_too_large");
73
- }
74
- const result = await processor.process(currentData, processorCtx);
75
- currentData = result.data;
76
- if (typeof result.filename === "string" && result.filename.trim()) {
77
- currentFilename = result.filename.trim();
78
- }
79
- if (typeof result.mimeType === "string" && result.mimeType.trim()) {
80
- currentMimeType = result.mimeType.trim();
81
- }
82
- if (result.metadata && typeof result.metadata === "object") {
83
- Object.assign(metadata, result.metadata);
84
- }
85
- applied.push(processor.id);
86
- }
87
- return {
88
- data: currentData,
89
- filename: currentFilename,
90
- mimeType: currentMimeType,
91
- applied,
92
- metadata
93
- };
94
- };
95
- const waitForStreamFinished = async (stream) => new Promise((resolve, reject) => {
96
- stream.once("finish", resolve);
97
- stream.once("error", reject);
98
- });
99
- const writeToStream = async (stream, chunk) => {
100
- const ok = stream.write(chunk);
101
- if (ok) return;
102
- await new Promise((resolve, reject) => {
103
- const onDrain = () => {
104
- cleanup();
105
- resolve();
106
- };
107
- const onError = (error) => {
108
- cleanup();
109
- reject(error);
110
- };
111
- const cleanup = () => {
112
- stream.off("drain", onDrain);
113
- stream.off("error", onError);
114
- };
115
- stream.on("drain", onDrain);
116
- stream.on("error", onError);
117
- });
118
- };
119
- const abortUploadStream = async (stream) => {
120
- if (!stream) return;
121
- if (typeof stream.abort === "function") {
122
- try {
123
- await stream.abort();
124
- return;
125
- } catch {
126
- }
127
- }
128
- try {
129
- ;
130
- stream.destroy?.();
131
- } catch {
132
- }
133
- };
134
- const completeUpload = async (_payload, ctx) => {
135
- const tenantId = getTenantId(ctx);
136
- if (!tenantId) {
137
- ctx.res.status(400);
138
- return {
139
- ok: false,
140
- error: "tenant_missing"
141
- };
142
- }
143
- const uploadId = String(ctx.req.params?.uploadId ?? "").trim();
144
- if (!uploadId) {
145
- ctx.res.status(400);
146
- return {
147
- ok: false,
148
- error: "invalid_upload_id"
149
- };
150
- }
151
- const ability = buildUploadsAbility(ctx, tenantId);
152
- const modelCtx = getModelCtx(ctx, tenantId, ability);
153
- const [UploadSession, UploadChunk] = await Promise.all([models.get("RBUploadSession", modelCtx), models.get("RBUploadChunk", modelCtx)]);
154
- if (!ability.can("update", "RBUploadSession")) {
155
- ctx.res.status(401);
156
- return {
157
- ok: false,
158
- error: "unauthorized"
159
- };
160
- }
161
- const existing = await UploadSession.findOne({
162
- $and: [{
163
- _id: uploadId
164
- }, getUploadSessionAccessQuery(ability, "read")]
165
- }).lean();
166
- if (!existing) {
167
- ctx.res.status(404);
168
- return {
169
- ok: false,
170
- error: "not_found"
171
- };
172
- }
173
- if (existing.status === "done" && existing.fileId) {
174
- return {
175
- ok: true,
176
- fileId: existing.fileId
177
- };
178
- }
179
- const locked = await UploadSession.findOneAndUpdate({
180
- $and: [{
181
- _id: uploadId
182
- }, {
183
- status: "uploading"
184
- }, getUploadSessionAccessQuery(ability, "update")]
185
- }, {
186
- $set: {
187
- status: "assembling"
188
- },
189
- $unset: {
190
- error: ""
191
- }
192
- }, {
193
- returnDocument: "after"
194
- }).lean();
195
- if (!locked) {
196
- ctx.res.status(409);
197
- return {
198
- ok: false,
199
- error: "not_uploading"
200
- };
201
- }
202
- await ensureUploadIndexes(UploadSession, UploadChunk);
203
- const fsDb = await getTenantFilesystemDb(tenantId);
204
- const nativeDb = fsDb.db;
205
- if (!nativeDb) {
206
- await UploadSession.updateOne({
207
- $and: [{
208
- _id: uploadId
209
- }, getUploadSessionAccessQuery(ability, "update")]
210
- }, {
211
- $set: {
212
- status: "error",
213
- error: "filesystem_db_unavailable"
214
- }
215
- });
216
- ctx.res.status(500);
217
- return {
218
- ok: false,
219
- error: "assembly_failed"
220
- };
221
- }
222
- const bucketName = getBucketName();
223
- const bucket = new GridFSBucket(nativeDb, {
224
- bucketName
225
- });
226
- const lockedUserId = typeof locked.userId === "string" ? locked.userId : void 0;
227
- const maxProcessorBytes = getMaxUploadProcessorBytes();
228
- const shouldBufferForProcessing = locked.totalSize <= maxProcessorBytes;
229
- const declaredMimeType = locked.mimeType.trim().toLowerCase();
230
- const declaredSvg = declaredMimeType === "image/svg+xml" || locked.filename.trim().toLowerCase().endsWith(".svg");
231
- let uploadStream = null;
232
- let finalFilename = locked.filename;
233
- let finalMimeType = locked.mimeType;
234
- let finalSize = locked.totalSize;
235
- let inlineProcessors = [];
236
- let finalMetadata = {
237
- uploadId,
238
- tenantId,
239
- mimeType: locked.mimeType,
240
- totalSize: locked.totalSize,
241
- ...typeof locked.isPublic === "boolean" ? {
242
- isPublic: locked.isPublic
243
- } : {},
244
- ...typeof locked.ownerKeyHash === "string" ? {
245
- ownerKeyHash: locked.ownerKeyHash
246
- } : {},
247
- ...lockedUserId ? {
248
- userId: lockedUserId
249
- } : {}
250
- };
251
- try {
252
- if (!shouldBufferForProcessing && declaredSvg) {
253
- throw new Error("svg_too_large");
254
- }
255
- const cursor = UploadChunk.find({
256
- uploadId
257
- }).sort({
258
- index: 1
259
- }).cursor();
260
- let expectedIndex = 0;
261
- const chunks = [];
262
- let bufferedBytes = 0;
263
- const pendingChunks = [];
264
- const sniffParts = [];
265
- let sniffBytes = 0;
266
- try {
267
- for await (const chunkDoc of cursor) {
268
- if (chunkDoc.index !== expectedIndex) {
269
- throw new Error("missing_chunks");
270
- }
271
- const chunk = chunkDoc.data;
272
- if (shouldBufferForProcessing) {
273
- chunks.push(chunk);
274
- bufferedBytes += chunk.length;
275
- } else if (!uploadStream) {
276
- pendingChunks.push(chunk);
277
- if (sniffBytes < maxProcessorBytes) {
278
- const slice = chunk.subarray(0, Math.min(chunk.length, maxProcessorBytes - sniffBytes));
279
- if (slice.length) {
280
- sniffParts.push(slice);
281
- sniffBytes += slice.length;
282
- }
283
- }
284
- if (sniffBytes >= maxProcessorBytes) {
285
- const sniff = Buffer.concat(sniffParts, sniffBytes);
286
- const processors = selectUploadProcessors({
287
- filename: locked.filename,
288
- clientMimeType: locked.mimeType,
289
- totalSize: locked.totalSize,
290
- sniff
291
- });
292
- if (processors.length) {
293
- throw new Error(processors.some((processor) => processor.id === "sanitize-svg") ? "svg_too_large" : "processor_input_too_large");
294
- }
295
- finalMetadata = {
296
- uploadId,
297
- tenantId,
298
- mimeType: locked.mimeType,
299
- totalSize: locked.totalSize,
300
- ...typeof locked.isPublic === "boolean" ? {
301
- isPublic: locked.isPublic
302
- } : {},
303
- ...typeof locked.ownerKeyHash === "string" ? {
304
- ownerKeyHash: locked.ownerKeyHash
305
- } : {},
306
- ...lockedUserId ? {
307
- userId: lockedUserId
308
- } : {}
309
- };
310
- uploadStream = bucket.openUploadStream(locked.filename, {
311
- metadata: finalMetadata
312
- });
313
- for (const pending of pendingChunks) {
314
- await writeToStream(uploadStream, pending);
315
- }
316
- pendingChunks.length = 0;
317
- }
318
- } else {
319
- await writeToStream(uploadStream, chunk);
320
- }
321
- expectedIndex += 1;
322
- }
323
- } finally {
324
- try {
325
- await cursor.close();
326
- } catch {
327
- }
328
- }
329
- if (expectedIndex !== locked.chunksTotal) {
330
- throw new Error("missing_chunks");
331
- }
332
- if (shouldBufferForProcessing) {
333
- const assembled = Buffer.concat(chunks, bufferedBytes);
334
- const {
335
- data: processed,
336
- filename: processedFilename,
337
- mimeType: processedMimeType,
338
- applied,
339
- metadata: processedMetadata
340
- } = await applyUploadProcessors(assembled, {
341
- filename: locked.filename,
342
- clientMimeType: locked.mimeType
343
- });
344
- finalFilename = processedFilename;
345
- finalMimeType = processedMimeType;
346
- finalSize = processed.length;
347
- inlineProcessors = applied;
348
- finalMetadata = {
349
- uploadId,
350
- tenantId,
351
- mimeType: processedMimeType,
352
- totalSize: processed.length,
353
- ...applied.length ? {
354
- processors: applied,
355
- sourceTotalSize: locked.totalSize
356
- } : {},
357
- ...processedMetadata,
358
- ...typeof locked.isPublic === "boolean" ? {
359
- isPublic: locked.isPublic
360
- } : {},
361
- ...typeof locked.ownerKeyHash === "string" ? {
362
- ownerKeyHash: locked.ownerKeyHash
363
- } : {},
364
- ...lockedUserId ? {
365
- userId: lockedUserId
366
- } : {}
367
- };
368
- uploadStream = bucket.openUploadStream(finalFilename, {
369
- metadata: finalMetadata
370
- });
371
- const finished = waitForStreamFinished(uploadStream);
372
- uploadStream.end(processed);
373
- await finished;
374
- } else {
375
- if (!uploadStream) {
376
- const sniff = Buffer.concat(sniffParts, sniffBytes);
377
- const processors = selectUploadProcessors({
378
- filename: locked.filename,
379
- clientMimeType: locked.mimeType,
380
- totalSize: locked.totalSize,
381
- sniff
382
- });
383
- if (processors.length) {
384
- throw new Error(processors.some((processor) => processor.id === "sanitize-svg") ? "svg_too_large" : "processor_input_too_large");
385
- }
386
- finalMetadata = {
387
- uploadId,
388
- tenantId,
389
- mimeType: locked.mimeType,
390
- totalSize: locked.totalSize,
391
- ...typeof locked.isPublic === "boolean" ? {
392
- isPublic: locked.isPublic
393
- } : {},
394
- ...typeof locked.ownerKeyHash === "string" ? {
395
- ownerKeyHash: locked.ownerKeyHash
396
- } : {},
397
- ...lockedUserId ? {
398
- userId: lockedUserId
399
- } : {}
400
- };
401
- uploadStream = bucket.openUploadStream(locked.filename, {
402
- metadata: finalMetadata
403
- });
404
- for (const pending of pendingChunks) {
405
- await writeToStream(uploadStream, pending);
406
- }
407
- pendingChunks.length = 0;
408
- }
409
- const finished = waitForStreamFinished(uploadStream);
410
- uploadStream.end();
411
- await finished;
412
- }
413
- const fileId = String(uploadStream.id ?? "");
414
- if (!fileId) {
415
- throw new Error("missing_file_id");
416
- }
417
- await UploadSession.updateOne({
418
- $and: [{
419
- _id: uploadId
420
- }, getUploadSessionAccessQuery(ability, "update")]
421
- }, {
422
- $set: {
423
- status: "done",
424
- fileId,
425
- filename: finalFilename,
426
- mimeType: finalMimeType,
427
- totalSize: finalSize
428
- },
429
- $unset: {
430
- error: ""
431
- }
432
- });
433
- await enqueueUploadPostProcessors({
434
- tenantId,
435
- uploadId,
436
- fileId,
437
- filename: finalFilename,
438
- mimeType: finalMimeType,
439
- clientMimeType: locked.mimeType,
440
- totalSize: finalSize,
441
- ...typeof locked.isPublic === "boolean" ? {
442
- isPublic: locked.isPublic
443
- } : {},
444
- ...typeof locked.ownerKeyHash === "string" ? {
445
- ownerKeyHash: locked.ownerKeyHash
446
- } : {},
447
- ...lockedUserId ? {
448
- userId: lockedUserId
449
- } : {},
450
- inlineProcessors,
451
- metadata: finalMetadata
452
- }).catch((error) => {
453
- console.error("Upload post processor enqueue failed", {
454
- tenantId,
455
- uploadId,
456
- fileId,
457
- error
458
- });
459
- });
460
- try {
461
- await UploadChunk.deleteMany({
462
- uploadId
463
- });
464
- } catch {
465
- }
466
- return {
467
- ok: true,
468
- fileId
469
- };
470
- } catch (error) {
471
- const message = error instanceof Error ? error.message : String(error);
472
- await abortUploadStream(uploadStream);
473
- if (message === "missing_chunks") {
474
- await UploadSession.updateOne({
475
- $and: [{
476
- _id: uploadId
477
- }, getUploadSessionAccessQuery(ability, "update")]
478
- }, {
479
- $set: {
480
- status: "uploading"
481
- }
482
- });
483
- ctx.res.status(409);
484
- return {
485
- ok: false,
486
- error: "missing_chunks"
487
- };
488
- }
489
- if (message === "svg_too_large") {
490
- await UploadSession.updateOne({
491
- $and: [{
492
- _id: uploadId
493
- }, getUploadSessionAccessQuery(ability, "update")]
494
- }, {
495
- $set: {
496
- status: "error",
497
- error: message
498
- }
499
- });
500
- ctx.res.status(413);
501
- return {
502
- ok: false,
503
- error: message
504
- };
505
- }
506
- if (message === "heif_too_large" || message === "processor_input_too_large") {
507
- await UploadSession.updateOne({
508
- $and: [{
509
- _id: uploadId
510
- }, getUploadSessionAccessQuery(ability, "update")]
511
- }, {
512
- $set: {
513
- status: "error",
514
- error: message
515
- }
516
- });
517
- ctx.res.status(413);
518
- return {
519
- ok: false,
520
- error: message
521
- };
522
- }
523
- if (message.startsWith("heif_decode_unsupported")) {
524
- await UploadSession.updateOne({
525
- $and: [{
526
- _id: uploadId
527
- }, getUploadSessionAccessQuery(ability, "update")]
528
- }, {
529
- $set: {
530
- status: "error",
531
- error: message
532
- }
533
- });
534
- ctx.res.status(415);
535
- return {
536
- ok: false,
537
- error: message
538
- };
539
- }
540
- if (message === "svg_invalid" || message === "svg_sanitize_failed") {
541
- await UploadSession.updateOne({
542
- $and: [{
543
- _id: uploadId
544
- }, getUploadSessionAccessQuery(ability, "update")]
545
- }, {
546
- $set: {
547
- status: "error",
548
- error: message
549
- }
550
- });
551
- ctx.res.status(400);
552
- return {
553
- ok: false,
554
- error: message
555
- };
556
- }
557
- await UploadSession.updateOne({
558
- $and: [{
559
- _id: uploadId
560
- }, getUploadSessionAccessQuery(ability, "update")]
561
- }, {
562
- $set: {
563
- status: "error",
564
- error: message
565
- }
566
- });
567
- ctx.res.status(500);
568
- return {
569
- ok: false,
570
- error: "assembly_failed"
571
- };
572
- }
573
- };
574
- const getStatus = async (_payload, ctx) => {
575
- const tenantId = getTenantId(ctx);
576
- if (!tenantId) {
577
- ctx.res.status(400);
578
- return {
579
- ok: false,
580
- error: "tenant_missing"
581
- };
582
- }
583
- const uploadId = String(ctx.req.params?.uploadId ?? "").trim();
584
- if (!uploadId) {
585
- ctx.res.status(400);
586
- return {
587
- ok: false,
588
- error: "invalid_upload_id"
589
- };
590
- }
591
- const ability = buildUploadsAbility(ctx, tenantId);
592
- const modelCtx = getModelCtx(ctx, tenantId, ability);
593
- const [UploadSession, UploadChunk] = await Promise.all([models.get("RBUploadSession", modelCtx), models.get("RBUploadChunk", modelCtx)]);
594
- if (!ability.can("read", "RBUploadSession")) {
595
- ctx.res.status(401);
596
- return {
597
- ok: false,
598
- error: "unauthorized"
599
- };
600
- }
601
- const session = await UploadSession.findOne({
602
- $and: [{
603
- _id: uploadId
604
- }, getUploadSessionAccessQuery(ability, "read")]
605
- }).lean();
606
- if (!session) {
607
- ctx.res.status(404);
608
- return {
609
- ok: false,
610
- error: "not_found"
611
- };
612
- }
613
- const receivedDocs = await UploadChunk.find({
614
- uploadId
615
- }, {
616
- index: 1,
617
- _id: 0
618
- }).sort({
619
- index: 1
620
- }).lean();
621
- const received = receivedDocs.map((doc) => typeof doc.index === "number" ? doc.index : -1).filter((n) => Number.isInteger(n) && n >= 0);
622
- return {
623
- ok: true,
624
- status: session.status,
625
- chunkSize: session.chunkSize,
626
- chunksTotal: session.chunksTotal,
627
- received,
628
- ...session.fileId ? {
629
- fileId: session.fileId
630
- } : {}
631
- };
632
- };
633
- const InitRoute = "/api/rb/file-uploads";
634
- const ChunkRoute = "/api/rb/file-uploads/:uploadId/chunks/:index";
635
- const StatusRoute = "/api/rb/file-uploads/:uploadId/status";
636
- const CompleteRoute = "/api/rb/file-uploads/:uploadId/complete";
637
- const initRequestSchema = object({
638
- filename: string().min(1),
639
- mimeType: string().min(1),
640
- isPublic: boolean().optional(),
641
- totalSize: number().int().min(1)
642
- });
643
- object({
644
- ok: boolean(),
645
- error: string().optional(),
646
- uploadId: string().optional(),
647
- uploadKey: string().optional(),
648
- chunkSize: number().int().optional(),
649
- chunksTotal: number().int().optional()
650
- });
651
- object({
652
- ok: boolean(),
653
- error: string().optional(),
654
- status: _enum(["uploading", "assembling", "done", "error"]).optional(),
655
- chunkSize: number().int().optional(),
656
- chunksTotal: number().int().optional(),
657
- received: array(number().int().min(0)).optional(),
658
- fileId: string().optional()
659
- });
660
- object({
661
- ok: boolean(),
662
- error: string().optional(),
663
- fileId: string().optional()
664
- });
665
- const initUpload = async (payload, ctx) => {
666
- const tenantId = getTenantId(ctx);
667
- if (!tenantId) {
668
- ctx.res.status(400);
669
- return {
670
- ok: false,
671
- error: "tenant_missing"
672
- };
673
- }
674
- const userId = getUserId(ctx);
675
- const parsed = initRequestSchema.safeParse(payload ?? {});
676
- if (!parsed.success) {
677
- ctx.res.status(400);
678
- return {
679
- ok: false,
680
- error: "invalid_payload"
681
- };
682
- }
683
- const chunkSize = getChunkSizeBytes();
684
- const {
685
- filename,
686
- mimeType,
687
- totalSize,
688
- isPublic
689
- } = parsed.data;
690
- const chunksTotal = Math.ceil(totalSize / chunkSize);
691
- const ability = buildUploadsAbility(ctx, tenantId);
692
- const modelCtx = getModelCtx(ctx, tenantId, ability);
693
- const [UploadSession, UploadChunk] = await Promise.all([models.get("RBUploadSession", modelCtx), models.get("RBUploadChunk", modelCtx)]);
694
- await ensureUploadIndexes(UploadSession, UploadChunk);
695
- const uploadId = new ObjectId().toString();
696
- const now = Date.now();
697
- const expiresAt = new Date(now + getSessionTtlMs());
698
- const uploadKey = userId ? null : randomBytes(32).toString("base64url");
699
- const ownerKeyHash = uploadKey ? computeSha256Hex(Buffer.from(uploadKey)) : void 0;
700
- await UploadSession.create({
701
- _id: uploadId,
702
- ...userId ? {
703
- userId
704
- } : {},
705
- ...ownerKeyHash ? {
706
- ownerKeyHash
707
- } : {},
708
- filename,
709
- mimeType,
710
- ...typeof isPublic === "boolean" ? {
711
- isPublic
712
- } : {},
713
- totalSize,
714
- chunkSize,
715
- chunksTotal,
716
- status: "uploading",
717
- createdAt: new Date(now),
718
- expiresAt
719
- });
720
- return {
721
- ok: true,
722
- uploadId,
723
- chunkSize,
724
- chunksTotal,
725
- ...uploadKey ? {
726
- uploadKey
727
- } : {}
728
- };
729
- };
730
- const uploadChunk = async (payload, ctx) => {
731
- const tenantId = getTenantId(ctx);
732
- if (!tenantId) {
733
- ctx.res.status(400);
734
- return {
735
- ok: false,
736
- error: "tenant_missing"
737
- };
738
- }
739
- const uploadId = String(ctx.req.params?.uploadId ?? "").trim();
740
- const indexRaw = String(ctx.req.params?.index ?? "").trim();
741
- const index = Number(indexRaw);
742
- if (!uploadId || !Number.isInteger(index) || index < 0) {
743
- ctx.res.status(400);
744
- return {
745
- ok: false,
746
- error: "invalid_chunk_ref"
747
- };
748
- }
749
- const ability = buildUploadsAbility(ctx, tenantId);
750
- const modelCtx = getModelCtx(ctx, tenantId, ability);
751
- const [UploadSession, UploadChunk] = await Promise.all([models.get("RBUploadSession", modelCtx), models.get("RBUploadChunk", modelCtx)]);
752
- if (!ability.can("update", "RBUploadSession")) {
753
- ctx.res.status(401);
754
- return {
755
- ok: false,
756
- error: "unauthorized"
757
- };
758
- }
759
- const session = await UploadSession.findOne({
760
- $and: [{
761
- _id: uploadId
762
- }, getUploadSessionAccessQuery(ability, "update")]
763
- }).lean();
764
- if (!session) {
765
- ctx.res.status(404);
766
- return {
767
- ok: false,
768
- error: "not_found"
769
- };
770
- }
771
- if (session.status !== "uploading") {
772
- ctx.res.status(409);
773
- return {
774
- ok: false,
775
- error: "not_uploading"
776
- };
777
- }
778
- if (index >= session.chunksTotal) {
779
- ctx.res.status(400);
780
- return {
781
- ok: false,
782
- error: "index_out_of_range"
783
- };
784
- }
785
- const data = toBufferPayload(payload);
786
- if (!data) {
787
- ctx.res.status(400);
788
- return {
789
- ok: false,
790
- error: "invalid_body"
791
- };
792
- }
793
- const expectedSize = index === session.chunksTotal - 1 ? session.totalSize - session.chunkSize * (session.chunksTotal - 1) : session.chunkSize;
794
- if (data.length > expectedSize) {
795
- ctx.res.status(413);
796
- return {
797
- ok: false,
798
- error: "chunk_too_large"
799
- };
800
- }
801
- if (data.length !== expectedSize) {
802
- ctx.res.status(400);
803
- return {
804
- ok: false,
805
- error: "invalid_chunk_size"
806
- };
807
- }
808
- const checksumHeader = ctx.req.get("X-Chunk-SHA256");
809
- const sha256 = checksumHeader ? computeSha256Hex(data) : void 0;
810
- if (checksumHeader) {
811
- const expectedSha256 = normalizeSha256Hex(checksumHeader);
812
- if (sha256 !== expectedSha256) {
813
- ctx.res.status(400);
814
- return {
815
- ok: false,
816
- error: "checksum_mismatch"
817
- };
818
- }
819
- }
820
- await ensureUploadIndexes(UploadSession, UploadChunk);
821
- await UploadChunk.updateOne({
822
- uploadId,
823
- index
824
- }, {
825
- $set: {
826
- uploadId,
827
- index,
828
- data,
829
- size: data.length,
830
- sha256,
831
- expiresAt: session.expiresAt
832
- },
833
- $setOnInsert: {
834
- createdAt: /* @__PURE__ */ new Date()
835
- }
836
- }, {
837
- upsert: true
838
- });
839
- ctx.res.status(204);
840
- return {
841
- ok: true
842
- };
843
- };
844
- const rawBodyParser = ({
845
- limitBytes,
846
- maxClientBytesPerSecond
847
- }) => {
848
- return (req, res, next) => {
849
- const contentType = typeof req?.headers?.["content-type"] === "string" ? String(req.headers["content-type"]) : "";
850
- if (!contentType.includes("application/octet-stream")) {
851
- next();
852
- return;
853
- }
854
- let total = 0;
855
- const chunks = [];
856
- let done = false;
857
- let paused = false;
858
- let throttleTimeout = null;
859
- const rateBytesPerSecond = typeof maxClientBytesPerSecond === "number" && maxClientBytesPerSecond > 0 ? maxClientBytesPerSecond : null;
860
- const cleanup = () => {
861
- req.off("data", onData);
862
- req.off("end", onEnd);
863
- req.off("error", onError);
864
- req.off("aborted", onAborted);
865
- if (throttleTimeout) {
866
- clearTimeout(throttleTimeout);
867
- throttleTimeout = null;
868
- }
869
- };
870
- const finish = (error) => {
871
- if (done) return;
872
- done = true;
873
- cleanup();
874
- if (error) {
875
- next(error);
876
- return;
877
- }
878
- req.body = Buffer.concat(chunks, total);
879
- next();
880
- };
881
- const onData = (chunk) => {
882
- if (done) return;
883
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
884
- total += buffer.length;
885
- if (total > limitBytes) {
886
- done = true;
887
- cleanup();
888
- req.destroy();
889
- res.status(413).json({
890
- ok: false,
891
- error: "chunk_too_large"
892
- });
893
- return;
894
- }
895
- chunks.push(buffer);
896
- if (!rateBytesPerSecond) return;
897
- const now = Date.now();
898
- const clientKey = getClientKey(req);
899
- const state = getClientRateState(clientKey, rateBytesPerSecond, now);
900
- const waitMs = consumeRateBudget(state, buffer.length, rateBytesPerSecond, now);
901
- if (waitMs > 0 && !paused) {
902
- paused = true;
903
- req.pause();
904
- throttleTimeout = setTimeout(() => {
905
- throttleTimeout = null;
906
- paused = false;
907
- if (done) return;
908
- try {
909
- req.resume();
910
- } catch {
911
- }
912
- }, waitMs);
913
- }
914
- };
915
- const onEnd = () => finish();
916
- const onError = (err) => finish(err);
917
- const onAborted = () => finish(new Error("request_aborted"));
918
- req.on("data", onData);
919
- req.on("end", onEnd);
920
- req.on("error", onError);
921
- req.on("aborted", onAborted);
922
- };
923
- };
924
- const MAX_BURST_SECONDS = 1;
925
- const STALE_CLIENT_MS = 15 * 60 * 1e3;
926
- const clientRateStates = /* @__PURE__ */ new Map();
927
- let lastCleanupMs = 0;
928
- const getClientKey = (req) => {
929
- const rawClientIp = typeof req?.clientIp === "string" ? req.clientIp : "";
930
- if (rawClientIp.trim()) return rawClientIp.trim();
931
- const rawIp = typeof req?.ip === "string" ? req.ip : "";
932
- return rawIp.trim() || "unknown";
933
- };
934
- const maybeCleanupStates = (now) => {
935
- if (now - lastCleanupMs < 6e4) return;
936
- lastCleanupMs = now;
937
- if (clientRateStates.size < 2e3) return;
938
- for (const [key, state] of clientRateStates) {
939
- if (now - state.lastSeenMs > STALE_CLIENT_MS) {
940
- clientRateStates.delete(key);
941
- }
942
- }
943
- };
944
- const getClientRateState = (key, rateBytesPerSecond, now) => {
945
- maybeCleanupStates(now);
946
- const capacity = rateBytesPerSecond * MAX_BURST_SECONDS;
947
- const existing = clientRateStates.get(key);
948
- if (existing) {
949
- existing.lastSeenMs = now;
950
- existing.tokens = Math.min(capacity, existing.tokens);
951
- return existing;
952
- }
953
- const next = {
954
- tokens: capacity,
955
- lastRefillMs: now,
956
- lastSeenMs: now
957
- };
958
- clientRateStates.set(key, next);
959
- return next;
960
- };
961
- const consumeRateBudget = (state, bytes, rateBytesPerSecond, now) => {
962
- const capacity = rateBytesPerSecond * MAX_BURST_SECONDS;
963
- const elapsedMs = Math.max(0, now - state.lastRefillMs);
964
- if (elapsedMs > 0) {
965
- state.tokens = Math.min(capacity, state.tokens + elapsedMs * rateBytesPerSecond / 1e3);
966
- state.lastRefillMs = now;
967
- }
968
- state.tokens -= bytes;
969
- if (state.tokens >= 0) return 0;
970
- return Math.ceil(-state.tokens / rateBytesPerSecond * 1e3);
971
- };
972
- const handler = (api) => {
973
- const chunkSizeBytes = getChunkSizeBytes();
974
- api.use(InitRoute, rawBodyParser({
975
- limitBytes: getRawBodyLimitBytes(chunkSizeBytes),
976
- maxClientBytesPerSecond: getMaxClientUploadBytesPerSecond()
977
- }));
978
- api.post(InitRoute, initUpload);
979
- api.put(ChunkRoute, uploadChunk);
980
- api.get(StatusRoute, getStatus);
981
- api.post(CompleteRoute, completeUpload);
982
- };
983
- export {
984
- handler as default
985
- };
986
- //# sourceMappingURL=handler-D-XdgeG_.js.map