deepspace 0.11.0 → 0.13.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.
package/dist/server.js CHANGED
@@ -174,8 +174,54 @@ var BUILT_IN_TOOLS = [
174
174
  }
175
175
  ];
176
176
 
177
+ // src/shared/app-files.ts
178
+ var MAX_APP_FILE_BYTES = 1024 * 1024 * 1024;
179
+ var MAX_UPLOAD_REQUEST_BYTES = 25 * 1024 * 1024;
180
+ var UPLOAD_PART_BYTES = 20 * 1024 * 1024;
181
+ var MAX_UPLOAD_PARTS = Math.ceil(MAX_APP_FILE_BYTES / UPLOAD_PART_BYTES);
182
+ var MAX_BASE64_UPLOAD_BYTES = Math.floor(MAX_UPLOAD_REQUEST_BYTES * 3 / 4);
183
+ var MAX_DEPLOY_ASSET_FILE_BYTES = 25 * 1024 * 1024;
184
+ var DANGEROUS_MIME_TYPES = /* @__PURE__ */ new Set([
185
+ "text/html",
186
+ "application/xhtml+xml",
187
+ "image/svg+xml",
188
+ "application/xml",
189
+ "text/xml",
190
+ "application/javascript",
191
+ "text/javascript",
192
+ "application/ecmascript",
193
+ "text/ecmascript"
194
+ ]);
195
+ function activeContentMessage(mimeType) {
196
+ return `${mimeType} is not accepted: the files API serves your app's own origin, so stored HTML/SVG/JavaScript would execute there with your users' sessions. Ship it as a build asset under \`public/\` instead, where it is served as part of your app rather than as user-uploaded content.`;
197
+ }
198
+ function formatBytes(bytes) {
199
+ if (bytes >= 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GiB`;
200
+ if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
201
+ if (bytes >= 1024) return `${Math.round(bytes / 1024)} KiB`;
202
+ return `${bytes} B`;
203
+ }
204
+ function encodeKeyPath(key) {
205
+ return key.split("/").map(encodeURIComponent).join("/");
206
+ }
207
+ function oversizeMessage(bytes, limit = MAX_APP_FILE_BYTES) {
208
+ return `That file is ${formatBytes(bytes)}; the limit is ${formatBytes(limit)} per file.`;
209
+ }
210
+ function oversizeRequestMessage(bytes) {
211
+ return `This request carries ${formatBytes(bytes)}; one request may carry ${formatBytes(MAX_UPLOAD_REQUEST_BYTES)}. Files above ${formatBytes(UPLOAD_PART_BYTES)} upload in parts.`;
212
+ }
213
+ function oversizePartMessage(bytes) {
214
+ return `This part carries ${formatBytes(bytes)}; a part may carry ${formatBytes(UPLOAD_PART_BYTES)} \u2014 the size the upload advertised.`;
215
+ }
216
+
177
217
  // src/server/utils/scoped-r2-files.ts
178
218
  var CORS_HEADERS = { "Access-Control-Allow-Origin": "*" };
219
+ function fail(status, error, code, extra) {
220
+ return Response.json(
221
+ { error, ...code ? { code } : {}, ...extra },
222
+ { status, headers: CORS_HEADERS }
223
+ );
224
+ }
179
225
  function contentDisposition(originalName) {
180
226
  const safe = originalName.replace(/[^\x20-\x7e]|["\\]/g, "_") || "download";
181
227
  const encoded = encodeURIComponent(originalName).replace(
@@ -190,25 +236,89 @@ function sanitizeSubpath(raw) {
190
236
  if (segments.some((s) => s === ".." || s === ".")) return null;
191
237
  return raw;
192
238
  }
193
- function encodeKeyPath(key) {
194
- return key.split("/").map(encodeURIComponent).join("/");
239
+ function overRequestBound(bytes, slack = 0) {
240
+ if (bytes <= MAX_UPLOAD_REQUEST_BYTES + slack) return null;
241
+ return fail(413, oversizeRequestMessage(bytes), "too_large", {
242
+ maxBytes: MAX_UPLOAD_REQUEST_BYTES,
243
+ partSize: UPLOAD_PART_BYTES
244
+ });
245
+ }
246
+ function overCeiling(bytes) {
247
+ if (bytes <= MAX_APP_FILE_BYTES) return null;
248
+ return fail(413, oversizeMessage(bytes), "too_large", { maxBytes: MAX_APP_FILE_BYTES });
249
+ }
250
+ function overPartBound(bytes) {
251
+ if (bytes <= UPLOAD_PART_BYTES) return null;
252
+ return fail(413, oversizePartMessage(bytes), "too_large", { maxBytes: UPLOAD_PART_BYTES });
253
+ }
254
+ function declaredLength(request, what) {
255
+ const raw = request.headers.get("content-length");
256
+ const declared = raw === null ? NaN : Number(raw);
257
+ if (!Number.isSafeInteger(declared) || declared < 0) {
258
+ return fail(
259
+ 411,
260
+ `${what} must declare a Content-Length. A chunked or length-less body is refused: R2 cannot size a write it has no length for.`,
261
+ "length_required"
262
+ );
263
+ }
264
+ return declared;
195
265
  }
196
266
  function isKeyWithinPrefix(key, prefix) {
197
267
  return key.startsWith(prefix);
198
268
  }
199
- var DANGEROUS_MIME_TYPES = /* @__PURE__ */ new Set([
200
- "text/html",
201
- "application/xhtml+xml",
202
- "image/svg+xml",
203
- "application/xml",
204
- "text/xml",
205
- "application/javascript",
206
- "text/javascript",
207
- "application/ecmascript",
208
- "text/ecmascript"
269
+ function anchorKey(raw, prefix) {
270
+ const safe = sanitizeSubpath(raw);
271
+ if (safe === null) return fail(400, "Invalid key: traversal not allowed", "invalid_key");
272
+ if (!safe) return fail(400, "Invalid key: empty", "invalid_key");
273
+ return { key: `${prefix}${safe}` };
274
+ }
275
+ var VERB_ROUTES = /* @__PURE__ */ new Set([
276
+ "POST upload",
277
+ "POST multipart",
278
+ "DELETE multipart",
279
+ "PUT multipart/part",
280
+ "POST multipart/complete"
209
281
  ]);
210
- function normalizeMimeType(raw) {
211
- return raw.split(";")[0].trim().toLowerCase();
282
+ function isFilesVerbPath(method, subpath) {
283
+ return VERB_ROUTES.has(`${method} ${subpath}`);
284
+ }
285
+ function resolveMimeType(raw) {
286
+ const mimeType = raw.split(";")[0].trim().toLowerCase() || "application/octet-stream";
287
+ return DANGEROUS_MIME_TYPES.has(mimeType) ? fail(415, activeContentMessage(mimeType), "active_content") : { mimeType };
288
+ }
289
+ function r2ErrorCode(error) {
290
+ const message = error instanceof Error ? error.message : String(error);
291
+ const match = /\((\d{4,6})\)\s*$/.exec(message.trim());
292
+ return match ? Number(match[1]) : null;
293
+ }
294
+ function multipartFailure(error) {
295
+ switch (r2ErrorCode(error)) {
296
+ case 10024:
297
+ return fail(
298
+ 404,
299
+ "That upload does not exist \u2014 it was completed, abandoned, or expired. Start a new one.",
300
+ "unknown_upload"
301
+ );
302
+ case 10011:
303
+ // EntityTooSmall
304
+ case 10025:
305
+ // InvalidPart — a named part was never uploaded
306
+ case 10048:
307
+ return fail(
308
+ 400,
309
+ "The parts do not assemble: every part except the last must be exactly the size the upload advertised, and each must be uploaded before it is named.",
310
+ "bad_parts"
311
+ );
312
+ case 10012:
313
+ // MetadataTooLarge
314
+ case 10020:
315
+ return fail(400, "The upload names a key or metadata R2 will not accept.", "invalid_key");
316
+ default: {
317
+ const message = error instanceof Error ? error.message : String(error);
318
+ console.error("[files:multipart] R2 error:", message);
319
+ return fail(503, "The files service could not complete that upload. Retry shortly.", "r2_error");
320
+ }
321
+ }
212
322
  }
213
323
  function createScopedR2Handler(config) {
214
324
  const requireAuthForMutations = config.requireAuthForMutations ?? true;
@@ -240,14 +350,12 @@ function createScopedR2Handler(config) {
240
350
  return Response.json({ error: errorMsg }, { status, headers: CORS_HEADERS });
241
351
  }
242
352
  const prefix = result.prefix;
243
- if (subpath === "upload" && request.method === "POST") {
353
+ if (isFilesVerbPath(request.method, subpath)) {
244
354
  if (requireAuthForMutations && !auth.userId) {
245
- return Response.json(
246
- { error: "Authentication required" },
247
- { status: 401, headers: CORS_HEADERS }
248
- );
355
+ await request.body?.cancel().catch(() => void 0);
356
+ return fail(401, "Authentication required", "unauthorized");
249
357
  }
250
- return handleUpload(request, url, bucket, prefix, auth.userId, scope);
358
+ return subpath === "upload" ? handleUpload(request, url, bucket, prefix, auth.userId, scope) : handleMultipart(request, url, bucket, prefix, auth.userId, scope, subpath);
251
359
  }
252
360
  if (!subpath && request.method === "GET") {
253
361
  return handleList(bucket, prefix, url, scope);
@@ -274,87 +382,250 @@ function createScopedR2Handler(config) {
274
382
  { status: 403, headers: CORS_HEADERS }
275
383
  );
276
384
  }
277
- await bucket.delete(subpath);
278
- return Response.json({ success: true, deleted: subpath }, { headers: CORS_HEADERS });
385
+ const existed = await bucket.head(subpath) !== null;
386
+ if (existed) await bucket.delete(subpath);
387
+ return Response.json(
388
+ { success: true, deleted: existed ? subpath : null, existed },
389
+ { headers: CORS_HEADERS }
390
+ );
279
391
  }
280
392
  return Response.json({ error: "Method not allowed" }, { status: 405, headers: CORS_HEADERS });
281
393
  };
282
394
  }
283
395
  async function handleUpload(request, url, bucket, prefix, userId, scope) {
284
396
  try {
397
+ const declared = overRequestBound(Number(request.headers.get("content-length")), 8 * 1024);
398
+ if (declared) return declared;
285
399
  const contentType = request.headers.get("content-type") || "";
286
400
  let fileData;
287
401
  let fileName;
288
- let mimeType;
402
+ let rawMimeType;
289
403
  if (contentType.includes("multipart/form-data")) {
290
404
  const formData = await request.formData();
291
405
  const file = formData.get("file");
292
- if (!file) {
293
- return Response.json({ error: "No file provided" }, { status: 400, headers: CORS_HEADERS });
294
- }
406
+ if (!file) return fail(400, "No file provided", "bad_request");
407
+ const tooBig = overRequestBound(file.size);
408
+ if (tooBig) return tooBig;
295
409
  fileData = await file.arrayBuffer();
296
410
  fileName = formData.get("name")?.toString() || file.name;
297
- mimeType = file.type || "application/octet-stream";
411
+ rawMimeType = file.type || "application/octet-stream";
298
412
  } else {
299
413
  const body = await request.json();
300
- if (!body.data || !body.name) {
301
- return Response.json(
302
- { error: "Missing data or name" },
303
- { status: 400, headers: CORS_HEADERS }
304
- );
305
- }
414
+ if (!body.data || !body.name) return fail(400, "Missing data or name", "bad_request");
306
415
  const base64Data = body.data.replace(/^data:[^;]+;base64,/, "");
416
+ const tooBig = overRequestBound(Math.floor(base64Data.length * 3 / 4));
417
+ if (tooBig) return tooBig;
307
418
  fileData = Uint8Array.from(atob(base64Data), (c) => c.charCodeAt(0)).buffer;
308
419
  fileName = body.name;
309
- mimeType = body.mimeType || "application/octet-stream";
310
- }
311
- mimeType = normalizeMimeType(mimeType) || "application/octet-stream";
312
- if (DANGEROUS_MIME_TYPES.has(mimeType)) {
313
- return Response.json(
314
- { error: `Unsupported media type: ${mimeType}` },
315
- { status: 415, headers: CORS_HEADERS }
316
- );
317
- }
318
- const requestedKey = url.searchParams.get("key");
319
- let key;
320
- if (requestedKey !== null) {
321
- const safe = sanitizeSubpath(requestedKey);
322
- if (safe === null) {
323
- return Response.json(
324
- { error: "Invalid key: traversal not allowed" },
325
- { status: 400, headers: CORS_HEADERS }
326
- );
327
- }
328
- if (!safe) {
329
- return Response.json(
330
- { error: "Invalid key: empty" },
331
- { status: 400, headers: CORS_HEADERS }
332
- );
333
- }
334
- key = `${prefix}${safe}`;
335
- } else {
336
- const fileKey = `${Date.now()}-${Math.random().toString(36).slice(2, 11)}-${fileName}`;
337
- key = `${prefix}${fileKey}`;
338
- }
339
- await bucket.put(key, fileData, {
340
- httpMetadata: { contentType: mimeType },
341
- customMetadata: {
342
- originalName: fileName,
343
- ...userId ? { uploadedBy: userId } : {},
344
- uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
345
- }
420
+ rawMimeType = body.mimeType || "application/octet-stream";
421
+ }
422
+ const mime = resolveMimeType(rawMimeType);
423
+ if (mime instanceof Response) return mime;
424
+ const located = locateUpload(url, prefix, fileName);
425
+ if (located instanceof Response) return located;
426
+ await bucket.put(located.key, fileData, {
427
+ httpMetadata: { contentType: mime.mimeType },
428
+ customMetadata: fileMetadata(fileName, userId)
346
429
  });
347
- const fileUrl = new URL(`/api/files/${encodeKeyPath(key)}`, url.origin);
348
- fileUrl.searchParams.set("scope", scope);
349
- return Response.json(
350
- { success: true, key, url: fileUrl.toString(), name: fileName },
351
- { headers: CORS_HEADERS }
352
- );
430
+ return storedFileJson(located.key, fileName, url, scope);
353
431
  } catch (err) {
354
432
  const msg = err instanceof Error ? err.message : "Upload failed";
355
433
  console.error("[handleUpload] Error:", msg, err instanceof Error ? err.stack : "");
356
- return Response.json({ error: msg }, { status: 500, headers: CORS_HEADERS });
434
+ return fail(500, msg, "upload_failed");
435
+ }
436
+ }
437
+ function locateUpload(url, prefix, fileName) {
438
+ const requested = url.searchParams.get("key");
439
+ const subkey = requested ?? `${Date.now()}-${Math.random().toString(36).slice(2, 11)}-${fileName}`;
440
+ const anchored = anchorKey(subkey, prefix);
441
+ return anchored instanceof Response ? anchored : { key: anchored.key, subkey };
442
+ }
443
+ function fileMetadata(fileName, userId) {
444
+ return {
445
+ originalName: fileName,
446
+ ...userId ? { uploadedBy: userId } : {},
447
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
448
+ };
449
+ }
450
+ function storedFileJson(key, name, url, scope) {
451
+ const fileUrl = new URL(`/api/files/${encodeKeyPath(key)}`, url.origin);
452
+ fileUrl.searchParams.set("scope", scope);
453
+ return Response.json(
454
+ { success: true, key, url: fileUrl.toString(), name },
455
+ { headers: CORS_HEADERS }
456
+ );
457
+ }
458
+ var MAX_CONTROL_BODY_BYTES = 64 * 1024;
459
+ async function readControlJson(request) {
460
+ const declared = declaredLength(request, "This request");
461
+ if (declared instanceof Response) return declared;
462
+ if (declared > MAX_CONTROL_BODY_BYTES) {
463
+ return fail(413, `Request body exceeds ${MAX_CONTROL_BODY_BYTES} bytes`, "too_large");
464
+ }
465
+ let parsed;
466
+ try {
467
+ parsed = await request.json();
468
+ } catch {
469
+ return fail(400, "Body must be JSON", "bad_request");
470
+ }
471
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : fail(400, "Body must be a JSON object", "bad_request");
472
+ }
473
+ function resolveSession(url, prefix) {
474
+ const uploadKey = url.searchParams.get("uploadKey");
475
+ const uploadId = url.searchParams.get("uploadId");
476
+ if (!uploadKey || !uploadId) {
477
+ return fail(400, "uploadKey and uploadId are required", "bad_request");
478
+ }
479
+ const anchored = anchorKey(uploadKey, prefix);
480
+ return anchored instanceof Response ? anchored : { key: anchored.key, uploadId };
481
+ }
482
+ async function streamPart(upload, partNumber, body, expectedBytes) {
483
+ let observedBytes = 0;
484
+ const sized = new FixedLengthStream(expectedBytes);
485
+ const pumped = body.pipeThrough(
486
+ new TransformStream({
487
+ transform(chunk, controller) {
488
+ observedBytes += chunk.byteLength;
489
+ controller.enqueue(chunk);
490
+ }
491
+ })
492
+ ).pipeTo(sized.writable);
493
+ const [uploaded, drained] = await Promise.allSettled([
494
+ upload.uploadPart(partNumber, sized.readable),
495
+ pumped
496
+ ]);
497
+ if (uploaded.status === "rejected" || drained.status === "rejected") {
498
+ const reason = uploaded.status === "rejected" ? uploaded.reason : drained.reason;
499
+ return { ok: false, lengthMismatch: observedBytes !== expectedBytes, error: reason };
500
+ }
501
+ return { ok: true, part: uploaded.value };
502
+ }
503
+ async function handleMultipart(request, url, bucket, prefix, userId, scope, subpath) {
504
+ if (subpath === "multipart" && request.method === "POST") {
505
+ return multipartInit(request, url, bucket, prefix, userId);
506
+ }
507
+ if (subpath === "multipart/part") return multipartPart(request, url, bucket, prefix);
508
+ if (subpath === "multipart/complete") {
509
+ return multipartComplete(request, url, bucket, prefix, scope);
510
+ }
511
+ return multipartAbort(url, bucket, prefix);
512
+ }
513
+ async function multipartInit(request, url, bucket, prefix, userId) {
514
+ const body = await readControlJson(request);
515
+ if (body instanceof Response) return body;
516
+ const size = body.size;
517
+ if (!Number.isSafeInteger(size) || size < 0) {
518
+ return fail(400, "size must be a non-negative integer number of bytes", "bad_request");
519
+ }
520
+ const tooBig = overCeiling(size);
521
+ if (tooBig) return tooBig;
522
+ const name = typeof body.name === "string" ? body.name.trim() : "";
523
+ if (!name) return fail(400, "name is required", "bad_request");
524
+ const mime = resolveMimeType(typeof body.mimeType === "string" ? body.mimeType : "");
525
+ if (mime instanceof Response) return mime;
526
+ const located = locateUpload(url, prefix, name);
527
+ if (located instanceof Response) return located;
528
+ let upload;
529
+ try {
530
+ upload = await bucket.createMultipartUpload(located.key, {
531
+ httpMetadata: { contentType: mime.mimeType },
532
+ customMetadata: fileMetadata(name, userId)
533
+ });
534
+ } catch (error) {
535
+ return multipartFailure(error);
536
+ }
537
+ return Response.json(
538
+ {
539
+ uploadId: upload.uploadId,
540
+ // Echoed back on every later call. Relative to the app, exactly like
541
+ // `?key=`, so the mount that anchors keys anchors this one too.
542
+ uploadKey: located.subkey,
543
+ partSize: UPLOAD_PART_BYTES,
544
+ maxParts: MAX_UPLOAD_PARTS,
545
+ maxBytes: MAX_APP_FILE_BYTES
546
+ },
547
+ { headers: CORS_HEADERS }
548
+ );
549
+ }
550
+ async function multipartPart(request, url, bucket, prefix) {
551
+ const refuse = async (response) => {
552
+ await request.body?.cancel().catch(() => void 0);
553
+ return response;
554
+ };
555
+ const declaredBytes = declaredLength(request, "Part uploads");
556
+ if (declaredBytes instanceof Response) return refuse(declaredBytes);
557
+ const tooBig = overPartBound(declaredBytes);
558
+ if (tooBig) return refuse(tooBig);
559
+ const partNumber = Number(url.searchParams.get("partNumber"));
560
+ if (!Number.isSafeInteger(partNumber) || partNumber < 1 || partNumber > MAX_UPLOAD_PARTS) {
561
+ return refuse(
562
+ fail(400, `partNumber must be a whole number from 1 to ${MAX_UPLOAD_PARTS}`, "bad_request")
563
+ );
564
+ }
565
+ const session = resolveSession(url, prefix);
566
+ if (session instanceof Response) return refuse(session);
567
+ if (!request.body) return fail(400, "A part body is required", "bad_request");
568
+ const upload = bucket.resumeMultipartUpload(session.key, session.uploadId);
569
+ const stored = await streamPart(upload, partNumber, request.body, declaredBytes);
570
+ if (!stored.ok) {
571
+ return stored.lengthMismatch ? fail(
572
+ 400,
573
+ `Part ${partNumber} declared ${declaredBytes} bytes but its body did not carry them`,
574
+ "bad_request"
575
+ ) : multipartFailure(stored.error);
576
+ }
577
+ return Response.json(
578
+ { partNumber: stored.part.partNumber, etag: stored.part.etag },
579
+ { headers: CORS_HEADERS }
580
+ );
581
+ }
582
+ async function multipartComplete(request, url, bucket, prefix, scope) {
583
+ const body = await readControlJson(request);
584
+ if (body instanceof Response) return body;
585
+ const session = resolveSession(url, prefix);
586
+ if (session instanceof Response) return session;
587
+ const raw = body.parts;
588
+ if (!Array.isArray(raw) || raw.length === 0) {
589
+ return fail(400, "parts must be a non-empty array", "bad_request");
590
+ }
591
+ if (raw.length > MAX_UPLOAD_PARTS) {
592
+ return fail(400, `An upload may have at most ${MAX_UPLOAD_PARTS} parts`, "bad_request");
593
+ }
594
+ const parts = [];
595
+ for (let index = 0; index < raw.length; index++) {
596
+ const { partNumber, etag } = raw[index] ?? {};
597
+ if (!Number.isSafeInteger(partNumber) || partNumber < 1) {
598
+ return fail(400, `parts[${index}].partNumber must be a positive integer`, "bad_request");
599
+ }
600
+ if (typeof etag !== "string" || !etag) {
601
+ return fail(400, `parts[${index}].etag must be the string uploadPart returned`, "bad_request");
602
+ }
603
+ parts.push({ partNumber, etag });
604
+ }
605
+ let object;
606
+ try {
607
+ object = await bucket.resumeMultipartUpload(session.key, session.uploadId).complete(parts);
608
+ } catch (error) {
609
+ return multipartFailure(error);
610
+ }
611
+ const tooBig = overCeiling(object.size);
612
+ if (tooBig) {
613
+ await bucket.delete(session.key).catch(() => void 0);
614
+ return tooBig;
615
+ }
616
+ const name = object.customMetadata?.originalName ?? session.key.split("/").pop() ?? "file";
617
+ return storedFileJson(session.key, name, url, scope);
618
+ }
619
+ async function multipartAbort(url, bucket, prefix) {
620
+ const session = resolveSession(url, prefix);
621
+ if (session instanceof Response) return session;
622
+ try {
623
+ await bucket.resumeMultipartUpload(session.key, session.uploadId).abort();
624
+ } catch (error) {
625
+ console.error("[files:multipart] abort:", error instanceof Error ? error.message : error);
626
+ return Response.json({ success: true, aborted: false }, { headers: CORS_HEADERS });
357
627
  }
628
+ return Response.json({ success: true, aborted: true }, { headers: CORS_HEADERS });
358
629
  }
359
630
  async function handleList(bucket, prefix, url, scope) {
360
631
  const userPrefix = url.searchParams.get("prefix") || "";
@@ -7662,6 +7933,10 @@ export {
7662
7933
  GLOBAL_DO_TYPE_NAMES,
7663
7934
  GameRoom,
7664
7935
  JobRoom,
7936
+ MAX_APP_FILE_BYTES,
7937
+ MAX_DEPLOY_ASSET_FILE_BYTES,
7938
+ MAX_UPLOAD_PARTS,
7939
+ MAX_UPLOAD_REQUEST_BYTES,
7665
7940
  MESSAGES_SCHEMA,
7666
7941
  PresenceRoom,
7667
7942
  REACTIONS_SCHEMA,
@@ -7680,6 +7955,7 @@ export {
7680
7955
  SubscriptionAuthError,
7681
7956
  SubscriptionRequiredError,
7682
7957
  TEST_ACCOUNT_EMAIL_SUFFIX,
7958
+ UPLOAD_PART_BYTES,
7683
7959
  USERS_COLUMNS,
7684
7960
  VOTING_SCHEMAS,
7685
7961
  WORKSPACE_SCHEMAS,
@@ -7722,6 +7998,7 @@ export {
7722
7998
  deleteRecord,
7723
7999
  enqueueJob,
7724
8000
  executeQuery,
8001
+ formatBytes,
7725
8002
  getAllUsers,
7726
8003
  getChat,
7727
8004
  getGlobalDOSchemas,
@@ -7745,6 +8022,7 @@ export {
7745
8022
  handleYjsJoin,
7746
8023
  handleYjsLeave,
7747
8024
  isAutoProvision,
8025
+ isFilesVerbPath,
7748
8026
  isOwner,
7749
8027
  isTestAccountClaims,
7750
8028
  isTestAccountEmail,