deepspace 0.12.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/worker.js CHANGED
@@ -6241,8 +6241,28 @@ function registryErrorJson(c, err) {
6241
6241
  }
6242
6242
 
6243
6243
  // src/shared/app-files.ts
6244
- var MAX_APP_FILE_BYTES = 25 * 1024 * 1024;
6244
+ var MAX_APP_FILE_BYTES = 1024 * 1024 * 1024;
6245
+ var MAX_UPLOAD_REQUEST_BYTES = 25 * 1024 * 1024;
6246
+ var UPLOAD_PART_BYTES = 20 * 1024 * 1024;
6247
+ var MAX_UPLOAD_PARTS = Math.ceil(MAX_APP_FILE_BYTES / UPLOAD_PART_BYTES);
6248
+ var MAX_BASE64_UPLOAD_BYTES = Math.floor(MAX_UPLOAD_REQUEST_BYTES * 3 / 4);
6249
+ var MAX_DEPLOY_ASSET_FILE_BYTES = 25 * 1024 * 1024;
6250
+ var DANGEROUS_MIME_TYPES = /* @__PURE__ */ new Set([
6251
+ "text/html",
6252
+ "application/xhtml+xml",
6253
+ "image/svg+xml",
6254
+ "application/xml",
6255
+ "text/xml",
6256
+ "application/javascript",
6257
+ "text/javascript",
6258
+ "application/ecmascript",
6259
+ "text/ecmascript"
6260
+ ]);
6261
+ function activeContentMessage(mimeType) {
6262
+ 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.`;
6263
+ }
6245
6264
  function formatBytes(bytes) {
6265
+ if (bytes >= 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GiB`;
6246
6266
  if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
6247
6267
  if (bytes >= 1024) return `${Math.round(bytes / 1024)} KiB`;
6248
6268
  return `${bytes} B`;
@@ -6250,12 +6270,24 @@ function formatBytes(bytes) {
6250
6270
  function encodeKeyPath(key) {
6251
6271
  return key.split("/").map(encodeURIComponent).join("/");
6252
6272
  }
6253
- function oversizeMessage(bytes) {
6254
- return `That file is ${formatBytes(bytes)}; the limit is ${formatBytes(MAX_APP_FILE_BYTES)} per file.`;
6273
+ function oversizeMessage(bytes, limit = MAX_APP_FILE_BYTES) {
6274
+ return `That file is ${formatBytes(bytes)}; the limit is ${formatBytes(limit)} per file.`;
6275
+ }
6276
+ function oversizeRequestMessage(bytes) {
6277
+ return `This request carries ${formatBytes(bytes)}; one request may carry ${formatBytes(MAX_UPLOAD_REQUEST_BYTES)}. Files above ${formatBytes(UPLOAD_PART_BYTES)} upload in parts.`;
6278
+ }
6279
+ function oversizePartMessage(bytes) {
6280
+ return `This part carries ${formatBytes(bytes)}; a part may carry ${formatBytes(UPLOAD_PART_BYTES)} \u2014 the size the upload advertised.`;
6255
6281
  }
6256
6282
 
6257
6283
  // src/server/utils/scoped-r2-files.ts
6258
6284
  var CORS_HEADERS3 = { "Access-Control-Allow-Origin": "*" };
6285
+ function fail(status, error, code, extra) {
6286
+ return Response.json(
6287
+ { error, ...code ? { code } : {}, ...extra },
6288
+ { status, headers: CORS_HEADERS3 }
6289
+ );
6290
+ }
6259
6291
  function contentDisposition(originalName) {
6260
6292
  const safe = originalName.replace(/[^\x20-\x7e]|["\\]/g, "_") || "download";
6261
6293
  const encoded = encodeURIComponent(originalName).replace(
@@ -6270,29 +6302,89 @@ function sanitizeSubpath(raw) {
6270
6302
  if (segments.some((s) => s === ".." || s === ".")) return null;
6271
6303
  return raw;
6272
6304
  }
6273
- function oversize(bytes, slack = 0) {
6274
- if (bytes <= MAX_APP_FILE_BYTES + slack) return null;
6275
- return Response.json(
6276
- { error: oversizeMessage(bytes), code: "too_large", maxBytes: MAX_APP_FILE_BYTES },
6277
- { status: 413, headers: CORS_HEADERS3 }
6278
- );
6305
+ function overRequestBound(bytes, slack = 0) {
6306
+ if (bytes <= MAX_UPLOAD_REQUEST_BYTES + slack) return null;
6307
+ return fail(413, oversizeRequestMessage(bytes), "too_large", {
6308
+ maxBytes: MAX_UPLOAD_REQUEST_BYTES,
6309
+ partSize: UPLOAD_PART_BYTES
6310
+ });
6311
+ }
6312
+ function overCeiling(bytes) {
6313
+ if (bytes <= MAX_APP_FILE_BYTES) return null;
6314
+ return fail(413, oversizeMessage(bytes), "too_large", { maxBytes: MAX_APP_FILE_BYTES });
6315
+ }
6316
+ function overPartBound(bytes) {
6317
+ if (bytes <= UPLOAD_PART_BYTES) return null;
6318
+ return fail(413, oversizePartMessage(bytes), "too_large", { maxBytes: UPLOAD_PART_BYTES });
6319
+ }
6320
+ function declaredLength(request, what) {
6321
+ const raw = request.headers.get("content-length");
6322
+ const declared = raw === null ? NaN : Number(raw);
6323
+ if (!Number.isSafeInteger(declared) || declared < 0) {
6324
+ return fail(
6325
+ 411,
6326
+ `${what} must declare a Content-Length. A chunked or length-less body is refused: R2 cannot size a write it has no length for.`,
6327
+ "length_required"
6328
+ );
6329
+ }
6330
+ return declared;
6279
6331
  }
6280
6332
  function isKeyWithinPrefix(key, prefix) {
6281
6333
  return key.startsWith(prefix);
6282
6334
  }
6283
- var DANGEROUS_MIME_TYPES = /* @__PURE__ */ new Set([
6284
- "text/html",
6285
- "application/xhtml+xml",
6286
- "image/svg+xml",
6287
- "application/xml",
6288
- "text/xml",
6289
- "application/javascript",
6290
- "text/javascript",
6291
- "application/ecmascript",
6292
- "text/ecmascript"
6335
+ function anchorKey(raw, prefix) {
6336
+ const safe = sanitizeSubpath(raw);
6337
+ if (safe === null) return fail(400, "Invalid key: traversal not allowed", "invalid_key");
6338
+ if (!safe) return fail(400, "Invalid key: empty", "invalid_key");
6339
+ return { key: `${prefix}${safe}` };
6340
+ }
6341
+ var VERB_ROUTES = /* @__PURE__ */ new Set([
6342
+ "POST upload",
6343
+ "POST multipart",
6344
+ "DELETE multipart",
6345
+ "PUT multipart/part",
6346
+ "POST multipart/complete"
6293
6347
  ]);
6294
- function normalizeMimeType(raw) {
6295
- return raw.split(";")[0].trim().toLowerCase();
6348
+ function isFilesVerbPath(method, subpath) {
6349
+ return VERB_ROUTES.has(`${method} ${subpath}`);
6350
+ }
6351
+ function resolveMimeType(raw) {
6352
+ const mimeType = raw.split(";")[0].trim().toLowerCase() || "application/octet-stream";
6353
+ return DANGEROUS_MIME_TYPES.has(mimeType) ? fail(415, activeContentMessage(mimeType), "active_content") : { mimeType };
6354
+ }
6355
+ function r2ErrorCode(error) {
6356
+ const message = error instanceof Error ? error.message : String(error);
6357
+ const match = /\((\d{4,6})\)\s*$/.exec(message.trim());
6358
+ return match ? Number(match[1]) : null;
6359
+ }
6360
+ function multipartFailure(error) {
6361
+ switch (r2ErrorCode(error)) {
6362
+ case 10024:
6363
+ return fail(
6364
+ 404,
6365
+ "That upload does not exist \u2014 it was completed, abandoned, or expired. Start a new one.",
6366
+ "unknown_upload"
6367
+ );
6368
+ case 10011:
6369
+ // EntityTooSmall
6370
+ case 10025:
6371
+ // InvalidPart — a named part was never uploaded
6372
+ case 10048:
6373
+ return fail(
6374
+ 400,
6375
+ "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.",
6376
+ "bad_parts"
6377
+ );
6378
+ case 10012:
6379
+ // MetadataTooLarge
6380
+ case 10020:
6381
+ return fail(400, "The upload names a key or metadata R2 will not accept.", "invalid_key");
6382
+ default: {
6383
+ const message = error instanceof Error ? error.message : String(error);
6384
+ console.error("[files:multipart] R2 error:", message);
6385
+ return fail(503, "The files service could not complete that upload. Retry shortly.", "r2_error");
6386
+ }
6387
+ }
6296
6388
  }
6297
6389
  function createScopedR2Handler(config) {
6298
6390
  const requireAuthForMutations = config.requireAuthForMutations ?? true;
@@ -6324,14 +6416,12 @@ function createScopedR2Handler(config) {
6324
6416
  return Response.json({ error: errorMsg }, { status, headers: CORS_HEADERS3 });
6325
6417
  }
6326
6418
  const prefix = result.prefix;
6327
- if (subpath === "upload" && request.method === "POST") {
6419
+ if (isFilesVerbPath(request.method, subpath)) {
6328
6420
  if (requireAuthForMutations && !auth.userId) {
6329
- return Response.json(
6330
- { error: "Authentication required" },
6331
- { status: 401, headers: CORS_HEADERS3 }
6332
- );
6421
+ await request.body?.cancel().catch(() => void 0);
6422
+ return fail(401, "Authentication required", "unauthorized");
6333
6423
  }
6334
- return handleUpload(request, url, bucket, prefix, auth.userId, scope);
6424
+ return subpath === "upload" ? handleUpload(request, url, bucket, prefix, auth.userId, scope) : handleMultipart(request, url, bucket, prefix, auth.userId, scope, subpath);
6335
6425
  }
6336
6426
  if (!subpath && request.method === "GET") {
6337
6427
  return handleList(bucket, prefix, url, scope);
@@ -6358,93 +6448,250 @@ function createScopedR2Handler(config) {
6358
6448
  { status: 403, headers: CORS_HEADERS3 }
6359
6449
  );
6360
6450
  }
6361
- await bucket.delete(subpath);
6362
- return Response.json({ success: true, deleted: subpath }, { headers: CORS_HEADERS3 });
6451
+ const existed = await bucket.head(subpath) !== null;
6452
+ if (existed) await bucket.delete(subpath);
6453
+ return Response.json(
6454
+ { success: true, deleted: existed ? subpath : null, existed },
6455
+ { headers: CORS_HEADERS3 }
6456
+ );
6363
6457
  }
6364
6458
  return Response.json({ error: "Method not allowed" }, { status: 405, headers: CORS_HEADERS3 });
6365
6459
  };
6366
6460
  }
6367
6461
  async function handleUpload(request, url, bucket, prefix, userId, scope) {
6368
6462
  try {
6369
- const declared = oversize(Number(request.headers.get("content-length")), 8 * 1024);
6463
+ const declared = overRequestBound(Number(request.headers.get("content-length")), 8 * 1024);
6370
6464
  if (declared) return declared;
6371
6465
  const contentType = request.headers.get("content-type") || "";
6372
6466
  let fileData;
6373
6467
  let fileName;
6374
- let mimeType;
6468
+ let rawMimeType;
6375
6469
  if (contentType.includes("multipart/form-data")) {
6376
6470
  const formData = await request.formData();
6377
6471
  const file = formData.get("file");
6378
- if (!file) {
6379
- return Response.json({ error: "No file provided" }, { status: 400, headers: CORS_HEADERS3 });
6380
- }
6381
- const tooBig = oversize(file.size);
6472
+ if (!file) return fail(400, "No file provided", "bad_request");
6473
+ const tooBig = overRequestBound(file.size);
6382
6474
  if (tooBig) return tooBig;
6383
6475
  fileData = await file.arrayBuffer();
6384
6476
  fileName = formData.get("name")?.toString() || file.name;
6385
- mimeType = file.type || "application/octet-stream";
6477
+ rawMimeType = file.type || "application/octet-stream";
6386
6478
  } else {
6387
6479
  const body = await request.json();
6388
- if (!body.data || !body.name) {
6389
- return Response.json(
6390
- { error: "Missing data or name" },
6391
- { status: 400, headers: CORS_HEADERS3 }
6392
- );
6393
- }
6480
+ if (!body.data || !body.name) return fail(400, "Missing data or name", "bad_request");
6394
6481
  const base64Data = body.data.replace(/^data:[^;]+;base64,/, "");
6395
- const tooBig = oversize(Math.floor(base64Data.length * 3 / 4));
6482
+ const tooBig = overRequestBound(Math.floor(base64Data.length * 3 / 4));
6396
6483
  if (tooBig) return tooBig;
6397
6484
  fileData = Uint8Array.from(atob(base64Data), (c) => c.charCodeAt(0)).buffer;
6398
6485
  fileName = body.name;
6399
- mimeType = body.mimeType || "application/octet-stream";
6400
- }
6401
- mimeType = normalizeMimeType(mimeType) || "application/octet-stream";
6402
- if (DANGEROUS_MIME_TYPES.has(mimeType)) {
6403
- return Response.json(
6404
- { error: `Unsupported media type: ${mimeType}` },
6405
- { status: 415, headers: CORS_HEADERS3 }
6406
- );
6407
- }
6408
- const requestedKey = url.searchParams.get("key");
6409
- let key;
6410
- if (requestedKey !== null) {
6411
- const safe = sanitizeSubpath(requestedKey);
6412
- if (safe === null) {
6413
- return Response.json(
6414
- { error: "Invalid key: traversal not allowed" },
6415
- { status: 400, headers: CORS_HEADERS3 }
6416
- );
6417
- }
6418
- if (!safe) {
6419
- return Response.json(
6420
- { error: "Invalid key: empty" },
6421
- { status: 400, headers: CORS_HEADERS3 }
6422
- );
6423
- }
6424
- key = `${prefix}${safe}`;
6425
- } else {
6426
- const fileKey = `${Date.now()}-${Math.random().toString(36).slice(2, 11)}-${fileName}`;
6427
- key = `${prefix}${fileKey}`;
6428
- }
6429
- await bucket.put(key, fileData, {
6430
- httpMetadata: { contentType: mimeType },
6431
- customMetadata: {
6432
- originalName: fileName,
6433
- ...userId ? { uploadedBy: userId } : {},
6434
- uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
6435
- }
6486
+ rawMimeType = body.mimeType || "application/octet-stream";
6487
+ }
6488
+ const mime = resolveMimeType(rawMimeType);
6489
+ if (mime instanceof Response) return mime;
6490
+ const located = locateUpload(url, prefix, fileName);
6491
+ if (located instanceof Response) return located;
6492
+ await bucket.put(located.key, fileData, {
6493
+ httpMetadata: { contentType: mime.mimeType },
6494
+ customMetadata: fileMetadata(fileName, userId)
6436
6495
  });
6437
- const fileUrl = new URL(`/api/files/${encodeKeyPath(key)}`, url.origin);
6438
- fileUrl.searchParams.set("scope", scope);
6439
- return Response.json(
6440
- { success: true, key, url: fileUrl.toString(), name: fileName },
6441
- { headers: CORS_HEADERS3 }
6442
- );
6496
+ return storedFileJson(located.key, fileName, url, scope);
6443
6497
  } catch (err) {
6444
6498
  const msg = err instanceof Error ? err.message : "Upload failed";
6445
6499
  console.error("[handleUpload] Error:", msg, err instanceof Error ? err.stack : "");
6446
- return Response.json({ error: msg }, { status: 500, headers: CORS_HEADERS3 });
6500
+ return fail(500, msg, "upload_failed");
6501
+ }
6502
+ }
6503
+ function locateUpload(url, prefix, fileName) {
6504
+ const requested = url.searchParams.get("key");
6505
+ const subkey = requested ?? `${Date.now()}-${Math.random().toString(36).slice(2, 11)}-${fileName}`;
6506
+ const anchored = anchorKey(subkey, prefix);
6507
+ return anchored instanceof Response ? anchored : { key: anchored.key, subkey };
6508
+ }
6509
+ function fileMetadata(fileName, userId) {
6510
+ return {
6511
+ originalName: fileName,
6512
+ ...userId ? { uploadedBy: userId } : {},
6513
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
6514
+ };
6515
+ }
6516
+ function storedFileJson(key, name, url, scope) {
6517
+ const fileUrl = new URL(`/api/files/${encodeKeyPath(key)}`, url.origin);
6518
+ fileUrl.searchParams.set("scope", scope);
6519
+ return Response.json(
6520
+ { success: true, key, url: fileUrl.toString(), name },
6521
+ { headers: CORS_HEADERS3 }
6522
+ );
6523
+ }
6524
+ var MAX_CONTROL_BODY_BYTES = 64 * 1024;
6525
+ async function readControlJson(request) {
6526
+ const declared = declaredLength(request, "This request");
6527
+ if (declared instanceof Response) return declared;
6528
+ if (declared > MAX_CONTROL_BODY_BYTES) {
6529
+ return fail(413, `Request body exceeds ${MAX_CONTROL_BODY_BYTES} bytes`, "too_large");
6530
+ }
6531
+ let parsed;
6532
+ try {
6533
+ parsed = await request.json();
6534
+ } catch {
6535
+ return fail(400, "Body must be JSON", "bad_request");
6536
+ }
6537
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : fail(400, "Body must be a JSON object", "bad_request");
6538
+ }
6539
+ function resolveSession(url, prefix) {
6540
+ const uploadKey = url.searchParams.get("uploadKey");
6541
+ const uploadId = url.searchParams.get("uploadId");
6542
+ if (!uploadKey || !uploadId) {
6543
+ return fail(400, "uploadKey and uploadId are required", "bad_request");
6544
+ }
6545
+ const anchored = anchorKey(uploadKey, prefix);
6546
+ return anchored instanceof Response ? anchored : { key: anchored.key, uploadId };
6547
+ }
6548
+ async function streamPart(upload, partNumber, body, expectedBytes) {
6549
+ let observedBytes = 0;
6550
+ const sized = new FixedLengthStream(expectedBytes);
6551
+ const pumped = body.pipeThrough(
6552
+ new TransformStream({
6553
+ transform(chunk, controller) {
6554
+ observedBytes += chunk.byteLength;
6555
+ controller.enqueue(chunk);
6556
+ }
6557
+ })
6558
+ ).pipeTo(sized.writable);
6559
+ const [uploaded, drained] = await Promise.allSettled([
6560
+ upload.uploadPart(partNumber, sized.readable),
6561
+ pumped
6562
+ ]);
6563
+ if (uploaded.status === "rejected" || drained.status === "rejected") {
6564
+ const reason = uploaded.status === "rejected" ? uploaded.reason : drained.reason;
6565
+ return { ok: false, lengthMismatch: observedBytes !== expectedBytes, error: reason };
6566
+ }
6567
+ return { ok: true, part: uploaded.value };
6568
+ }
6569
+ async function handleMultipart(request, url, bucket, prefix, userId, scope, subpath) {
6570
+ if (subpath === "multipart" && request.method === "POST") {
6571
+ return multipartInit(request, url, bucket, prefix, userId);
6572
+ }
6573
+ if (subpath === "multipart/part") return multipartPart(request, url, bucket, prefix);
6574
+ if (subpath === "multipart/complete") {
6575
+ return multipartComplete(request, url, bucket, prefix, scope);
6576
+ }
6577
+ return multipartAbort(url, bucket, prefix);
6578
+ }
6579
+ async function multipartInit(request, url, bucket, prefix, userId) {
6580
+ const body = await readControlJson(request);
6581
+ if (body instanceof Response) return body;
6582
+ const size = body.size;
6583
+ if (!Number.isSafeInteger(size) || size < 0) {
6584
+ return fail(400, "size must be a non-negative integer number of bytes", "bad_request");
6585
+ }
6586
+ const tooBig = overCeiling(size);
6587
+ if (tooBig) return tooBig;
6588
+ const name = typeof body.name === "string" ? body.name.trim() : "";
6589
+ if (!name) return fail(400, "name is required", "bad_request");
6590
+ const mime = resolveMimeType(typeof body.mimeType === "string" ? body.mimeType : "");
6591
+ if (mime instanceof Response) return mime;
6592
+ const located = locateUpload(url, prefix, name);
6593
+ if (located instanceof Response) return located;
6594
+ let upload;
6595
+ try {
6596
+ upload = await bucket.createMultipartUpload(located.key, {
6597
+ httpMetadata: { contentType: mime.mimeType },
6598
+ customMetadata: fileMetadata(name, userId)
6599
+ });
6600
+ } catch (error) {
6601
+ return multipartFailure(error);
6602
+ }
6603
+ return Response.json(
6604
+ {
6605
+ uploadId: upload.uploadId,
6606
+ // Echoed back on every later call. Relative to the app, exactly like
6607
+ // `?key=`, so the mount that anchors keys anchors this one too.
6608
+ uploadKey: located.subkey,
6609
+ partSize: UPLOAD_PART_BYTES,
6610
+ maxParts: MAX_UPLOAD_PARTS,
6611
+ maxBytes: MAX_APP_FILE_BYTES
6612
+ },
6613
+ { headers: CORS_HEADERS3 }
6614
+ );
6615
+ }
6616
+ async function multipartPart(request, url, bucket, prefix) {
6617
+ const refuse = async (response) => {
6618
+ await request.body?.cancel().catch(() => void 0);
6619
+ return response;
6620
+ };
6621
+ const declaredBytes = declaredLength(request, "Part uploads");
6622
+ if (declaredBytes instanceof Response) return refuse(declaredBytes);
6623
+ const tooBig = overPartBound(declaredBytes);
6624
+ if (tooBig) return refuse(tooBig);
6625
+ const partNumber = Number(url.searchParams.get("partNumber"));
6626
+ if (!Number.isSafeInteger(partNumber) || partNumber < 1 || partNumber > MAX_UPLOAD_PARTS) {
6627
+ return refuse(
6628
+ fail(400, `partNumber must be a whole number from 1 to ${MAX_UPLOAD_PARTS}`, "bad_request")
6629
+ );
6630
+ }
6631
+ const session = resolveSession(url, prefix);
6632
+ if (session instanceof Response) return refuse(session);
6633
+ if (!request.body) return fail(400, "A part body is required", "bad_request");
6634
+ const upload = bucket.resumeMultipartUpload(session.key, session.uploadId);
6635
+ const stored = await streamPart(upload, partNumber, request.body, declaredBytes);
6636
+ if (!stored.ok) {
6637
+ return stored.lengthMismatch ? fail(
6638
+ 400,
6639
+ `Part ${partNumber} declared ${declaredBytes} bytes but its body did not carry them`,
6640
+ "bad_request"
6641
+ ) : multipartFailure(stored.error);
6642
+ }
6643
+ return Response.json(
6644
+ { partNumber: stored.part.partNumber, etag: stored.part.etag },
6645
+ { headers: CORS_HEADERS3 }
6646
+ );
6647
+ }
6648
+ async function multipartComplete(request, url, bucket, prefix, scope) {
6649
+ const body = await readControlJson(request);
6650
+ if (body instanceof Response) return body;
6651
+ const session = resolveSession(url, prefix);
6652
+ if (session instanceof Response) return session;
6653
+ const raw = body.parts;
6654
+ if (!Array.isArray(raw) || raw.length === 0) {
6655
+ return fail(400, "parts must be a non-empty array", "bad_request");
6656
+ }
6657
+ if (raw.length > MAX_UPLOAD_PARTS) {
6658
+ return fail(400, `An upload may have at most ${MAX_UPLOAD_PARTS} parts`, "bad_request");
6659
+ }
6660
+ const parts = [];
6661
+ for (let index = 0; index < raw.length; index++) {
6662
+ const { partNumber, etag } = raw[index] ?? {};
6663
+ if (!Number.isSafeInteger(partNumber) || partNumber < 1) {
6664
+ return fail(400, `parts[${index}].partNumber must be a positive integer`, "bad_request");
6665
+ }
6666
+ if (typeof etag !== "string" || !etag) {
6667
+ return fail(400, `parts[${index}].etag must be the string uploadPart returned`, "bad_request");
6668
+ }
6669
+ parts.push({ partNumber, etag });
6670
+ }
6671
+ let object;
6672
+ try {
6673
+ object = await bucket.resumeMultipartUpload(session.key, session.uploadId).complete(parts);
6674
+ } catch (error) {
6675
+ return multipartFailure(error);
6676
+ }
6677
+ const tooBig = overCeiling(object.size);
6678
+ if (tooBig) {
6679
+ await bucket.delete(session.key).catch(() => void 0);
6680
+ return tooBig;
6681
+ }
6682
+ const name = object.customMetadata?.originalName ?? session.key.split("/").pop() ?? "file";
6683
+ return storedFileJson(session.key, name, url, scope);
6684
+ }
6685
+ async function multipartAbort(url, bucket, prefix) {
6686
+ const session = resolveSession(url, prefix);
6687
+ if (session instanceof Response) return session;
6688
+ try {
6689
+ await bucket.resumeMultipartUpload(session.key, session.uploadId).abort();
6690
+ } catch (error) {
6691
+ console.error("[files:multipart] abort:", error instanceof Error ? error.message : error);
6692
+ return Response.json({ success: true, aborted: false }, { headers: CORS_HEADERS3 });
6447
6693
  }
6694
+ return Response.json({ success: true, aborted: true }, { headers: CORS_HEADERS3 });
6448
6695
  }
6449
6696
  async function handleList(bucket, prefix, url, scope) {
6450
6697
  const userPrefix = url.searchParams.get("prefix") || "";
@@ -8583,7 +8830,12 @@ function serverCard(request, manifest, publicBasePath) {
8583
8830
  name: manifest.name,
8584
8831
  description: `Search and read the published ${documentationSubject(manifest.name)}.`,
8585
8832
  version: MCP_SERVER_VERSION,
8833
+ // Preferred version, then everything the handshake will actually accept.
8834
+ // Advertising only the preferred one made the card look like it disagreed
8835
+ // with `initialize`, which per spec MUST echo a supported version the
8836
+ // client asked for rather than force the newest.
8586
8837
  protocolVersion: CURRENT_PROTOCOL_VERSION,
8838
+ protocolVersions: [...SUPPORTED_PROTOCOL_VERSIONS],
8587
8839
  transport: {
8588
8840
  type: "streamable-http",
8589
8841
  url: new URL(`${publicBasePath}/mcp`, request.url).toString()
@@ -9097,6 +9349,9 @@ export {
9097
9349
  JobRoom,
9098
9350
  LOG_LEVELS,
9099
9351
  MAX_APP_FILE_BYTES,
9352
+ MAX_DEPLOY_ASSET_FILE_BYTES,
9353
+ MAX_UPLOAD_PARTS,
9354
+ MAX_UPLOAD_REQUEST_BYTES,
9100
9355
  MESSAGES_SCHEMA,
9101
9356
  MSG,
9102
9357
  MSG_AWARENESS,
@@ -9123,6 +9378,7 @@ export {
9123
9378
  SYSTEM_MANAGED_COLUMNS,
9124
9379
  SchemaRegistry,
9125
9380
  TEST_ACCOUNT_EMAIL_SUFFIX,
9381
+ UPLOAD_PART_BYTES,
9126
9382
  USERS_COLUMNS,
9127
9383
  VOTING_SCHEMAS,
9128
9384
  WORKSPACE_SCHEMAS,
@@ -9169,6 +9425,7 @@ export {
9169
9425
  encodeSyncStep2,
9170
9426
  encodeUpdate,
9171
9427
  enqueueJob,
9428
+ formatBytes,
9172
9429
  getChat,
9173
9430
  getDeepSpaceAIModel,
9174
9431
  getGlobalDOSchemas,
@@ -9180,6 +9437,7 @@ export {
9180
9437
  handleSyncMessage,
9181
9438
  hasContent,
9182
9439
  isAutoProvision,
9440
+ isFilesVerbPath,
9183
9441
  isOwner,
9184
9442
  isTestAccountClaims,
9185
9443
  isTestAccountEmail,