deepspace 0.12.0 → 0.14.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/CHANGELOG.md +162 -0
- package/dist/{chunk-ZJ43EK3N.js → chunk-233GQU2H.js} +492 -365
- package/dist/chunk-233GQU2H.js.map +1 -0
- package/dist/cli.js +536 -172
- package/dist/cli.js.map +1 -1
- package/dist/documentation-client-core.d.ts +2 -2
- package/dist/documentation-client-core.js +1 -1
- package/dist/documentation-default-renderer.cjs +34 -34
- package/dist/documentation-react.d.ts +1 -1
- package/dist/documentation-react.js +1 -1
- package/dist/documentation-runtime.js +10 -10
- package/dist/documentation-server-core.d.ts +2 -2
- package/dist/documentation-server-core.js +1 -1
- package/dist/documentation.js +2 -2
- package/dist/documentation.js.map +1 -1
- package/dist/index.d.ts +28 -3
- package/dist/index.js +93 -8
- package/dist/index.js.map +1 -1
- package/dist/{public-BGRX_Cjd.d.ts → public-BtArwbVM.d.ts} +8 -0
- package/dist/server.d.ts +143 -27
- package/dist/server.js +427 -91
- package/dist/server.js.map +1 -1
- package/dist/worker.d.ts +140 -24
- package/dist/worker.js +432 -91
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-ZJ43EK3N.js.map +0 -1
package/dist/worker.js
CHANGED
|
@@ -6241,8 +6241,45 @@ function registryErrorJson(c, err) {
|
|
|
6241
6241
|
}
|
|
6242
6242
|
|
|
6243
6243
|
// src/shared/app-files.ts
|
|
6244
|
-
var MAX_APP_FILE_BYTES =
|
|
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 APP_STORAGE_LIMIT_BYTES = {
|
|
6250
|
+
test: 0,
|
|
6251
|
+
free: 128 * 1024 * 1024,
|
|
6252
|
+
starter: 512 * 1024 * 1024,
|
|
6253
|
+
premium: 2 * 1024 * 1024 * 1024,
|
|
6254
|
+
admin: 10 * 1024 * 1024 * 1024
|
|
6255
|
+
};
|
|
6256
|
+
function storageLimitForTier(tier) {
|
|
6257
|
+
if (tier && Object.hasOwn(APP_STORAGE_LIMIT_BYTES, tier)) {
|
|
6258
|
+
return APP_STORAGE_LIMIT_BYTES[tier];
|
|
6259
|
+
}
|
|
6260
|
+
return APP_STORAGE_LIMIT_BYTES.free;
|
|
6261
|
+
}
|
|
6262
|
+
function storageQuotaMessage(incomingBytes, usedBytes, limitBytes) {
|
|
6263
|
+
const free = Math.max(0, limitBytes - usedBytes);
|
|
6264
|
+
return `This upload needs ${formatBytes(incomingBytes)} but the app's file storage has ${formatBytes(free)} free (${formatBytes(usedBytes)} of ${formatBytes(limitBytes)} used). Delete files with \`deepspace app files rm <key>\`, or upgrade the owner's plan.`;
|
|
6265
|
+
}
|
|
6266
|
+
var MAX_DEPLOY_ASSET_FILE_BYTES = 25 * 1024 * 1024;
|
|
6267
|
+
var DANGEROUS_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
6268
|
+
"text/html",
|
|
6269
|
+
"application/xhtml+xml",
|
|
6270
|
+
"image/svg+xml",
|
|
6271
|
+
"application/xml",
|
|
6272
|
+
"text/xml",
|
|
6273
|
+
"application/javascript",
|
|
6274
|
+
"text/javascript",
|
|
6275
|
+
"application/ecmascript",
|
|
6276
|
+
"text/ecmascript"
|
|
6277
|
+
]);
|
|
6278
|
+
function activeContentMessage(mimeType) {
|
|
6279
|
+
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.`;
|
|
6280
|
+
}
|
|
6245
6281
|
function formatBytes(bytes) {
|
|
6282
|
+
if (bytes >= 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GiB`;
|
|
6246
6283
|
if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
|
|
6247
6284
|
if (bytes >= 1024) return `${Math.round(bytes / 1024)} KiB`;
|
|
6248
6285
|
return `${bytes} B`;
|
|
@@ -6250,12 +6287,24 @@ function formatBytes(bytes) {
|
|
|
6250
6287
|
function encodeKeyPath(key) {
|
|
6251
6288
|
return key.split("/").map(encodeURIComponent).join("/");
|
|
6252
6289
|
}
|
|
6253
|
-
function oversizeMessage(bytes) {
|
|
6254
|
-
return `That file is ${formatBytes(bytes)}; the limit is ${formatBytes(
|
|
6290
|
+
function oversizeMessage(bytes, limit = MAX_APP_FILE_BYTES) {
|
|
6291
|
+
return `That file is ${formatBytes(bytes)}; the limit is ${formatBytes(limit)} per file.`;
|
|
6292
|
+
}
|
|
6293
|
+
function oversizeRequestMessage(bytes) {
|
|
6294
|
+
return `This request carries ${formatBytes(bytes)}; one request may carry ${formatBytes(MAX_UPLOAD_REQUEST_BYTES)}. Files above ${formatBytes(UPLOAD_PART_BYTES)} upload in parts.`;
|
|
6295
|
+
}
|
|
6296
|
+
function oversizePartMessage(bytes) {
|
|
6297
|
+
return `This part carries ${formatBytes(bytes)}; a part may carry ${formatBytes(UPLOAD_PART_BYTES)} \u2014 the size the upload advertised.`;
|
|
6255
6298
|
}
|
|
6256
6299
|
|
|
6257
6300
|
// src/server/utils/scoped-r2-files.ts
|
|
6258
6301
|
var CORS_HEADERS3 = { "Access-Control-Allow-Origin": "*" };
|
|
6302
|
+
function fail(status, error, code, extra) {
|
|
6303
|
+
return Response.json(
|
|
6304
|
+
{ error, ...code ? { code } : {}, ...extra },
|
|
6305
|
+
{ status, headers: CORS_HEADERS3 }
|
|
6306
|
+
);
|
|
6307
|
+
}
|
|
6259
6308
|
function contentDisposition(originalName) {
|
|
6260
6309
|
const safe = originalName.replace(/[^\x20-\x7e]|["\\]/g, "_") || "download";
|
|
6261
6310
|
const encoded = encodeURIComponent(originalName).replace(
|
|
@@ -6270,29 +6319,135 @@ function sanitizeSubpath(raw) {
|
|
|
6270
6319
|
if (segments.some((s) => s === ".." || s === ".")) return null;
|
|
6271
6320
|
return raw;
|
|
6272
6321
|
}
|
|
6273
|
-
function
|
|
6274
|
-
if (bytes <=
|
|
6275
|
-
return
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
);
|
|
6322
|
+
function overRequestBound(bytes, slack = 0) {
|
|
6323
|
+
if (bytes <= MAX_UPLOAD_REQUEST_BYTES + slack) return null;
|
|
6324
|
+
return fail(413, oversizeRequestMessage(bytes), "too_large", {
|
|
6325
|
+
maxBytes: MAX_UPLOAD_REQUEST_BYTES,
|
|
6326
|
+
partSize: UPLOAD_PART_BYTES
|
|
6327
|
+
});
|
|
6328
|
+
}
|
|
6329
|
+
function overCeiling(bytes) {
|
|
6330
|
+
if (bytes <= MAX_APP_FILE_BYTES) return null;
|
|
6331
|
+
return fail(413, oversizeMessage(bytes), "too_large", { maxBytes: MAX_APP_FILE_BYTES });
|
|
6332
|
+
}
|
|
6333
|
+
function overPartBound(bytes) {
|
|
6334
|
+
if (bytes <= UPLOAD_PART_BYTES) return null;
|
|
6335
|
+
return fail(413, oversizePartMessage(bytes), "too_large", { maxBytes: UPLOAD_PART_BYTES });
|
|
6336
|
+
}
|
|
6337
|
+
function declaredLength(request, what) {
|
|
6338
|
+
const raw = request.headers.get("content-length");
|
|
6339
|
+
const declared = raw === null ? NaN : Number(raw);
|
|
6340
|
+
if (!Number.isSafeInteger(declared) || declared < 0) {
|
|
6341
|
+
return fail(
|
|
6342
|
+
411,
|
|
6343
|
+
`${what} must declare a Content-Length. A chunked or length-less body is refused: R2 cannot size a write it has no length for.`,
|
|
6344
|
+
"length_required"
|
|
6345
|
+
);
|
|
6346
|
+
}
|
|
6347
|
+
return declared;
|
|
6279
6348
|
}
|
|
6280
6349
|
function isKeyWithinPrefix(key, prefix) {
|
|
6281
6350
|
return key.startsWith(prefix);
|
|
6282
6351
|
}
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6352
|
+
async function allocationUsage(bucket, prefix) {
|
|
6353
|
+
let total = 0;
|
|
6354
|
+
let cursor;
|
|
6355
|
+
do {
|
|
6356
|
+
const page = await bucket.list({ prefix, cursor, limit: 1e3 });
|
|
6357
|
+
for (const object of page.objects) total += object.size;
|
|
6358
|
+
cursor = page.truncated ? page.cursor : void 0;
|
|
6359
|
+
} while (cursor !== void 0);
|
|
6360
|
+
return total;
|
|
6361
|
+
}
|
|
6362
|
+
async function admitStorage(bucket, storage, incomingBytes, replacesKey) {
|
|
6363
|
+
if (!storage) return null;
|
|
6364
|
+
const limit = await storage.limitBytes();
|
|
6365
|
+
if (limit === null) {
|
|
6366
|
+
return fail(
|
|
6367
|
+
503,
|
|
6368
|
+
"The storage limit for this app could not be verified; nothing was uploaded. Retry shortly.",
|
|
6369
|
+
"storage_limit_unavailable"
|
|
6370
|
+
);
|
|
6371
|
+
}
|
|
6372
|
+
const stored = await allocationUsage(bucket, storage.prefix);
|
|
6373
|
+
const replaced = replacesKey === null ? 0 : (await bucket.head(replacesKey))?.size ?? 0;
|
|
6374
|
+
const used = stored - replaced;
|
|
6375
|
+
if (used + incomingBytes <= limit) return null;
|
|
6376
|
+
return fail(409, storageQuotaMessage(incomingBytes, used, limit), "storage_quota_exceeded", {
|
|
6377
|
+
usedBytes: used,
|
|
6378
|
+
limitBytes: limit
|
|
6379
|
+
});
|
|
6380
|
+
}
|
|
6381
|
+
async function admitAssembledObject(bucket, storage, key, objectBytes) {
|
|
6382
|
+
if (!storage) return null;
|
|
6383
|
+
const limit = await storage.limitBytes();
|
|
6384
|
+
if (limit === null) {
|
|
6385
|
+
console.error("[files:quota] limit unavailable at complete; keeping", key);
|
|
6386
|
+
return null;
|
|
6387
|
+
}
|
|
6388
|
+
const stored = await allocationUsage(bucket, storage.prefix);
|
|
6389
|
+
if (stored <= limit) return null;
|
|
6390
|
+
await bucket.delete(key).catch(() => void 0);
|
|
6391
|
+
return fail(
|
|
6392
|
+
409,
|
|
6393
|
+
`${storageQuotaMessage(objectBytes, stored - objectBytes, limit)} The assembled upload was not kept.`,
|
|
6394
|
+
"storage_quota_exceeded",
|
|
6395
|
+
{ usedBytes: stored - objectBytes, limitBytes: limit }
|
|
6396
|
+
);
|
|
6397
|
+
}
|
|
6398
|
+
function anchorKey(raw, prefix) {
|
|
6399
|
+
const safe = sanitizeSubpath(raw);
|
|
6400
|
+
if (safe === null) return fail(400, "Invalid key: traversal not allowed", "invalid_key");
|
|
6401
|
+
if (!safe) return fail(400, "Invalid key: empty", "invalid_key");
|
|
6402
|
+
return { key: `${prefix}${safe}` };
|
|
6403
|
+
}
|
|
6404
|
+
var VERB_ROUTES = /* @__PURE__ */ new Set([
|
|
6405
|
+
"POST upload",
|
|
6406
|
+
"POST multipart",
|
|
6407
|
+
"DELETE multipart",
|
|
6408
|
+
"PUT multipart/part",
|
|
6409
|
+
"POST multipart/complete"
|
|
6293
6410
|
]);
|
|
6294
|
-
function
|
|
6295
|
-
return
|
|
6411
|
+
function isFilesVerbPath(method, subpath) {
|
|
6412
|
+
return VERB_ROUTES.has(`${method} ${subpath}`);
|
|
6413
|
+
}
|
|
6414
|
+
function resolveMimeType(raw) {
|
|
6415
|
+
const mimeType = raw.split(";")[0].trim().toLowerCase() || "application/octet-stream";
|
|
6416
|
+
return DANGEROUS_MIME_TYPES.has(mimeType) ? fail(415, activeContentMessage(mimeType), "active_content") : { mimeType };
|
|
6417
|
+
}
|
|
6418
|
+
function r2ErrorCode(error) {
|
|
6419
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6420
|
+
const match = /\((\d{4,6})\)\s*$/.exec(message.trim());
|
|
6421
|
+
return match ? Number(match[1]) : null;
|
|
6422
|
+
}
|
|
6423
|
+
function multipartFailure(error) {
|
|
6424
|
+
switch (r2ErrorCode(error)) {
|
|
6425
|
+
case 10024:
|
|
6426
|
+
return fail(
|
|
6427
|
+
404,
|
|
6428
|
+
"That upload does not exist \u2014 it was completed, abandoned, or expired. Start a new one.",
|
|
6429
|
+
"unknown_upload"
|
|
6430
|
+
);
|
|
6431
|
+
case 10011:
|
|
6432
|
+
// EntityTooSmall
|
|
6433
|
+
case 10025:
|
|
6434
|
+
// InvalidPart — a named part was never uploaded
|
|
6435
|
+
case 10048:
|
|
6436
|
+
return fail(
|
|
6437
|
+
400,
|
|
6438
|
+
"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.",
|
|
6439
|
+
"bad_parts"
|
|
6440
|
+
);
|
|
6441
|
+
case 10012:
|
|
6442
|
+
// MetadataTooLarge
|
|
6443
|
+
case 10020:
|
|
6444
|
+
return fail(400, "The upload names a key or metadata R2 will not accept.", "invalid_key");
|
|
6445
|
+
default: {
|
|
6446
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6447
|
+
console.error("[files:multipart] R2 error:", message);
|
|
6448
|
+
return fail(503, "The files service could not complete that upload. Retry shortly.", "r2_error");
|
|
6449
|
+
}
|
|
6450
|
+
}
|
|
6296
6451
|
}
|
|
6297
6452
|
function createScopedR2Handler(config) {
|
|
6298
6453
|
const requireAuthForMutations = config.requireAuthForMutations ?? true;
|
|
@@ -6324,17 +6479,15 @@ function createScopedR2Handler(config) {
|
|
|
6324
6479
|
return Response.json({ error: errorMsg }, { status, headers: CORS_HEADERS3 });
|
|
6325
6480
|
}
|
|
6326
6481
|
const prefix = result.prefix;
|
|
6327
|
-
if (
|
|
6482
|
+
if (isFilesVerbPath(request.method, subpath)) {
|
|
6328
6483
|
if (requireAuthForMutations && !auth.userId) {
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
{ status: 401, headers: CORS_HEADERS3 }
|
|
6332
|
-
);
|
|
6484
|
+
await request.body?.cancel().catch(() => void 0);
|
|
6485
|
+
return fail(401, "Authentication required", "unauthorized");
|
|
6333
6486
|
}
|
|
6334
|
-
return handleUpload(request, url, bucket, prefix, auth
|
|
6487
|
+
return subpath === "upload" ? handleUpload(request, url, bucket, prefix, auth, scope) : handleMultipart(request, url, bucket, prefix, auth, scope, subpath);
|
|
6335
6488
|
}
|
|
6336
6489
|
if (!subpath && request.method === "GET") {
|
|
6337
|
-
return handleList(bucket, prefix, url, scope);
|
|
6490
|
+
return handleList(bucket, prefix, url, scope, auth);
|
|
6338
6491
|
}
|
|
6339
6492
|
if (subpath && request.method === "GET") {
|
|
6340
6493
|
if (!isKeyWithinPrefix(subpath, prefix)) {
|
|
@@ -6358,95 +6511,260 @@ function createScopedR2Handler(config) {
|
|
|
6358
6511
|
{ status: 403, headers: CORS_HEADERS3 }
|
|
6359
6512
|
);
|
|
6360
6513
|
}
|
|
6361
|
-
await bucket.
|
|
6362
|
-
|
|
6514
|
+
const existed = await bucket.head(subpath) !== null;
|
|
6515
|
+
if (existed) await bucket.delete(subpath);
|
|
6516
|
+
return Response.json(
|
|
6517
|
+
{ success: true, deleted: existed ? subpath : null, existed },
|
|
6518
|
+
{ headers: CORS_HEADERS3 }
|
|
6519
|
+
);
|
|
6363
6520
|
}
|
|
6364
6521
|
return Response.json({ error: "Method not allowed" }, { status: 405, headers: CORS_HEADERS3 });
|
|
6365
6522
|
};
|
|
6366
6523
|
}
|
|
6367
|
-
async function handleUpload(request, url, bucket, prefix,
|
|
6524
|
+
async function handleUpload(request, url, bucket, prefix, auth, scope) {
|
|
6368
6525
|
try {
|
|
6369
|
-
const declared =
|
|
6526
|
+
const declared = overRequestBound(Number(request.headers.get("content-length")), 8 * 1024);
|
|
6370
6527
|
if (declared) return declared;
|
|
6371
6528
|
const contentType = request.headers.get("content-type") || "";
|
|
6372
6529
|
let fileData;
|
|
6373
6530
|
let fileName;
|
|
6374
|
-
let
|
|
6531
|
+
let rawMimeType;
|
|
6375
6532
|
if (contentType.includes("multipart/form-data")) {
|
|
6376
6533
|
const formData = await request.formData();
|
|
6377
6534
|
const file = formData.get("file");
|
|
6378
|
-
if (!file)
|
|
6379
|
-
|
|
6380
|
-
}
|
|
6381
|
-
const tooBig = oversize(file.size);
|
|
6535
|
+
if (!file) return fail(400, "No file provided", "bad_request");
|
|
6536
|
+
const tooBig = overRequestBound(file.size);
|
|
6382
6537
|
if (tooBig) return tooBig;
|
|
6383
6538
|
fileData = await file.arrayBuffer();
|
|
6384
6539
|
fileName = formData.get("name")?.toString() || file.name;
|
|
6385
|
-
|
|
6540
|
+
rawMimeType = file.type || "application/octet-stream";
|
|
6386
6541
|
} else {
|
|
6387
6542
|
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
|
-
}
|
|
6543
|
+
if (!body.data || !body.name) return fail(400, "Missing data or name", "bad_request");
|
|
6394
6544
|
const base64Data = body.data.replace(/^data:[^;]+;base64,/, "");
|
|
6395
|
-
const tooBig =
|
|
6545
|
+
const tooBig = overRequestBound(Math.floor(base64Data.length * 3 / 4));
|
|
6396
6546
|
if (tooBig) return tooBig;
|
|
6397
6547
|
fileData = Uint8Array.from(atob(base64Data), (c) => c.charCodeAt(0)).buffer;
|
|
6398
6548
|
fileName = body.name;
|
|
6399
|
-
|
|
6400
|
-
}
|
|
6401
|
-
|
|
6402
|
-
if (
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
|
|
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
|
-
}
|
|
6549
|
+
rawMimeType = body.mimeType || "application/octet-stream";
|
|
6550
|
+
}
|
|
6551
|
+
const mime = resolveMimeType(rawMimeType);
|
|
6552
|
+
if (mime instanceof Response) return mime;
|
|
6553
|
+
const located = locateUpload(url, prefix, fileName);
|
|
6554
|
+
if (located instanceof Response) return located;
|
|
6555
|
+
const replacesKey = url.searchParams.get("key") === null ? null : located.key;
|
|
6556
|
+
const overQuota = await admitStorage(bucket, auth.storage, fileData.byteLength, replacesKey);
|
|
6557
|
+
if (overQuota) return overQuota;
|
|
6558
|
+
await bucket.put(located.key, fileData, {
|
|
6559
|
+
httpMetadata: { contentType: mime.mimeType },
|
|
6560
|
+
customMetadata: fileMetadata(fileName, auth.userId)
|
|
6436
6561
|
});
|
|
6437
|
-
|
|
6438
|
-
fileUrl.searchParams.set("scope", scope);
|
|
6439
|
-
return Response.json(
|
|
6440
|
-
{ success: true, key, url: fileUrl.toString(), name: fileName },
|
|
6441
|
-
{ headers: CORS_HEADERS3 }
|
|
6442
|
-
);
|
|
6562
|
+
return storedFileJson(located.key, fileName, url, scope);
|
|
6443
6563
|
} catch (err) {
|
|
6444
6564
|
const msg = err instanceof Error ? err.message : "Upload failed";
|
|
6445
6565
|
console.error("[handleUpload] Error:", msg, err instanceof Error ? err.stack : "");
|
|
6446
|
-
return
|
|
6566
|
+
return fail(500, msg, "upload_failed");
|
|
6447
6567
|
}
|
|
6448
6568
|
}
|
|
6449
|
-
|
|
6569
|
+
function locateUpload(url, prefix, fileName) {
|
|
6570
|
+
const requested = url.searchParams.get("key");
|
|
6571
|
+
const subkey = requested ?? `${Date.now()}-${Math.random().toString(36).slice(2, 11)}-${fileName}`;
|
|
6572
|
+
const anchored = anchorKey(subkey, prefix);
|
|
6573
|
+
return anchored instanceof Response ? anchored : { key: anchored.key, subkey };
|
|
6574
|
+
}
|
|
6575
|
+
function fileMetadata(fileName, userId) {
|
|
6576
|
+
return {
|
|
6577
|
+
originalName: fileName,
|
|
6578
|
+
...userId ? { uploadedBy: userId } : {},
|
|
6579
|
+
uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6580
|
+
};
|
|
6581
|
+
}
|
|
6582
|
+
function storedFileJson(key, name, url, scope) {
|
|
6583
|
+
const fileUrl = new URL(`/api/files/${encodeKeyPath(key)}`, url.origin);
|
|
6584
|
+
fileUrl.searchParams.set("scope", scope);
|
|
6585
|
+
return Response.json(
|
|
6586
|
+
{ success: true, key, url: fileUrl.toString(), name },
|
|
6587
|
+
{ headers: CORS_HEADERS3 }
|
|
6588
|
+
);
|
|
6589
|
+
}
|
|
6590
|
+
var MAX_CONTROL_BODY_BYTES = 64 * 1024;
|
|
6591
|
+
async function readControlJson(request) {
|
|
6592
|
+
const declared = declaredLength(request, "This request");
|
|
6593
|
+
if (declared instanceof Response) return declared;
|
|
6594
|
+
if (declared > MAX_CONTROL_BODY_BYTES) {
|
|
6595
|
+
return fail(413, `Request body exceeds ${MAX_CONTROL_BODY_BYTES} bytes`, "too_large");
|
|
6596
|
+
}
|
|
6597
|
+
let parsed;
|
|
6598
|
+
try {
|
|
6599
|
+
parsed = await request.json();
|
|
6600
|
+
} catch {
|
|
6601
|
+
return fail(400, "Body must be JSON", "bad_request");
|
|
6602
|
+
}
|
|
6603
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : fail(400, "Body must be a JSON object", "bad_request");
|
|
6604
|
+
}
|
|
6605
|
+
function resolveSession(url, prefix) {
|
|
6606
|
+
const uploadKey = url.searchParams.get("uploadKey");
|
|
6607
|
+
const uploadId = url.searchParams.get("uploadId");
|
|
6608
|
+
if (!uploadKey || !uploadId) {
|
|
6609
|
+
return fail(400, "uploadKey and uploadId are required", "bad_request");
|
|
6610
|
+
}
|
|
6611
|
+
const anchored = anchorKey(uploadKey, prefix);
|
|
6612
|
+
return anchored instanceof Response ? anchored : { key: anchored.key, uploadId };
|
|
6613
|
+
}
|
|
6614
|
+
async function streamPart(upload, partNumber, body, expectedBytes) {
|
|
6615
|
+
let observedBytes = 0;
|
|
6616
|
+
const sized = new FixedLengthStream(expectedBytes);
|
|
6617
|
+
const pumped = body.pipeThrough(
|
|
6618
|
+
new TransformStream({
|
|
6619
|
+
transform(chunk, controller) {
|
|
6620
|
+
observedBytes += chunk.byteLength;
|
|
6621
|
+
controller.enqueue(chunk);
|
|
6622
|
+
}
|
|
6623
|
+
})
|
|
6624
|
+
).pipeTo(sized.writable);
|
|
6625
|
+
const [uploaded, drained] = await Promise.allSettled([
|
|
6626
|
+
upload.uploadPart(partNumber, sized.readable),
|
|
6627
|
+
pumped
|
|
6628
|
+
]);
|
|
6629
|
+
if (uploaded.status === "rejected" || drained.status === "rejected") {
|
|
6630
|
+
const reason = uploaded.status === "rejected" ? uploaded.reason : drained.reason;
|
|
6631
|
+
return { ok: false, lengthMismatch: observedBytes !== expectedBytes, error: reason };
|
|
6632
|
+
}
|
|
6633
|
+
return { ok: true, part: uploaded.value };
|
|
6634
|
+
}
|
|
6635
|
+
async function handleMultipart(request, url, bucket, prefix, auth, scope, subpath) {
|
|
6636
|
+
if (subpath === "multipart" && request.method === "POST") {
|
|
6637
|
+
return multipartInit(request, url, bucket, prefix, auth);
|
|
6638
|
+
}
|
|
6639
|
+
if (subpath === "multipart/part") return multipartPart(request, url, bucket, prefix);
|
|
6640
|
+
if (subpath === "multipart/complete") {
|
|
6641
|
+
return multipartComplete(request, url, bucket, prefix, auth, scope);
|
|
6642
|
+
}
|
|
6643
|
+
return multipartAbort(url, bucket, prefix);
|
|
6644
|
+
}
|
|
6645
|
+
async function multipartInit(request, url, bucket, prefix, auth) {
|
|
6646
|
+
const body = await readControlJson(request);
|
|
6647
|
+
if (body instanceof Response) return body;
|
|
6648
|
+
const size = body.size;
|
|
6649
|
+
if (!Number.isSafeInteger(size) || size < 0) {
|
|
6650
|
+
return fail(400, "size must be a non-negative integer number of bytes", "bad_request");
|
|
6651
|
+
}
|
|
6652
|
+
const tooBig = overCeiling(size);
|
|
6653
|
+
if (tooBig) return tooBig;
|
|
6654
|
+
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
6655
|
+
if (!name) return fail(400, "name is required", "bad_request");
|
|
6656
|
+
const mime = resolveMimeType(typeof body.mimeType === "string" ? body.mimeType : "");
|
|
6657
|
+
if (mime instanceof Response) return mime;
|
|
6658
|
+
const located = locateUpload(url, prefix, name);
|
|
6659
|
+
if (located instanceof Response) return located;
|
|
6660
|
+
const replacesKey = url.searchParams.get("key") === null ? null : located.key;
|
|
6661
|
+
const overQuota = await admitStorage(bucket, auth.storage, size, replacesKey);
|
|
6662
|
+
if (overQuota) return overQuota;
|
|
6663
|
+
let upload;
|
|
6664
|
+
try {
|
|
6665
|
+
upload = await bucket.createMultipartUpload(located.key, {
|
|
6666
|
+
httpMetadata: { contentType: mime.mimeType },
|
|
6667
|
+
customMetadata: fileMetadata(name, auth.userId)
|
|
6668
|
+
});
|
|
6669
|
+
} catch (error) {
|
|
6670
|
+
return multipartFailure(error);
|
|
6671
|
+
}
|
|
6672
|
+
return Response.json(
|
|
6673
|
+
{
|
|
6674
|
+
uploadId: upload.uploadId,
|
|
6675
|
+
// Echoed back on every later call. Relative to the app, exactly like
|
|
6676
|
+
// `?key=`, so the mount that anchors keys anchors this one too.
|
|
6677
|
+
uploadKey: located.subkey,
|
|
6678
|
+
partSize: UPLOAD_PART_BYTES,
|
|
6679
|
+
maxParts: MAX_UPLOAD_PARTS,
|
|
6680
|
+
maxBytes: MAX_APP_FILE_BYTES
|
|
6681
|
+
},
|
|
6682
|
+
{ headers: CORS_HEADERS3 }
|
|
6683
|
+
);
|
|
6684
|
+
}
|
|
6685
|
+
async function multipartPart(request, url, bucket, prefix) {
|
|
6686
|
+
const refuse = async (response) => {
|
|
6687
|
+
await request.body?.cancel().catch(() => void 0);
|
|
6688
|
+
return response;
|
|
6689
|
+
};
|
|
6690
|
+
const declaredBytes = declaredLength(request, "Part uploads");
|
|
6691
|
+
if (declaredBytes instanceof Response) return refuse(declaredBytes);
|
|
6692
|
+
const tooBig = overPartBound(declaredBytes);
|
|
6693
|
+
if (tooBig) return refuse(tooBig);
|
|
6694
|
+
const partNumber = Number(url.searchParams.get("partNumber"));
|
|
6695
|
+
if (!Number.isSafeInteger(partNumber) || partNumber < 1 || partNumber > MAX_UPLOAD_PARTS) {
|
|
6696
|
+
return refuse(
|
|
6697
|
+
fail(400, `partNumber must be a whole number from 1 to ${MAX_UPLOAD_PARTS}`, "bad_request")
|
|
6698
|
+
);
|
|
6699
|
+
}
|
|
6700
|
+
const session = resolveSession(url, prefix);
|
|
6701
|
+
if (session instanceof Response) return refuse(session);
|
|
6702
|
+
if (!request.body) return fail(400, "A part body is required", "bad_request");
|
|
6703
|
+
const upload = bucket.resumeMultipartUpload(session.key, session.uploadId);
|
|
6704
|
+
const stored = await streamPart(upload, partNumber, request.body, declaredBytes);
|
|
6705
|
+
if (!stored.ok) {
|
|
6706
|
+
return stored.lengthMismatch ? fail(
|
|
6707
|
+
400,
|
|
6708
|
+
`Part ${partNumber} declared ${declaredBytes} bytes but its body did not carry them`,
|
|
6709
|
+
"bad_request"
|
|
6710
|
+
) : multipartFailure(stored.error);
|
|
6711
|
+
}
|
|
6712
|
+
return Response.json(
|
|
6713
|
+
{ partNumber: stored.part.partNumber, etag: stored.part.etag },
|
|
6714
|
+
{ headers: CORS_HEADERS3 }
|
|
6715
|
+
);
|
|
6716
|
+
}
|
|
6717
|
+
async function multipartComplete(request, url, bucket, prefix, auth, scope) {
|
|
6718
|
+
const body = await readControlJson(request);
|
|
6719
|
+
if (body instanceof Response) return body;
|
|
6720
|
+
const session = resolveSession(url, prefix);
|
|
6721
|
+
if (session instanceof Response) return session;
|
|
6722
|
+
const raw = body.parts;
|
|
6723
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
6724
|
+
return fail(400, "parts must be a non-empty array", "bad_request");
|
|
6725
|
+
}
|
|
6726
|
+
if (raw.length > MAX_UPLOAD_PARTS) {
|
|
6727
|
+
return fail(400, `An upload may have at most ${MAX_UPLOAD_PARTS} parts`, "bad_request");
|
|
6728
|
+
}
|
|
6729
|
+
const parts = [];
|
|
6730
|
+
for (let index = 0; index < raw.length; index++) {
|
|
6731
|
+
const { partNumber, etag } = raw[index] ?? {};
|
|
6732
|
+
if (!Number.isSafeInteger(partNumber) || partNumber < 1) {
|
|
6733
|
+
return fail(400, `parts[${index}].partNumber must be a positive integer`, "bad_request");
|
|
6734
|
+
}
|
|
6735
|
+
if (typeof etag !== "string" || !etag) {
|
|
6736
|
+
return fail(400, `parts[${index}].etag must be the string uploadPart returned`, "bad_request");
|
|
6737
|
+
}
|
|
6738
|
+
parts.push({ partNumber, etag });
|
|
6739
|
+
}
|
|
6740
|
+
let object;
|
|
6741
|
+
try {
|
|
6742
|
+
object = await bucket.resumeMultipartUpload(session.key, session.uploadId).complete(parts);
|
|
6743
|
+
} catch (error) {
|
|
6744
|
+
return multipartFailure(error);
|
|
6745
|
+
}
|
|
6746
|
+
const tooBig = overCeiling(object.size);
|
|
6747
|
+
if (tooBig) {
|
|
6748
|
+
await bucket.delete(session.key).catch(() => void 0);
|
|
6749
|
+
return tooBig;
|
|
6750
|
+
}
|
|
6751
|
+
const overQuota = await admitAssembledObject(bucket, auth.storage, session.key, object.size);
|
|
6752
|
+
if (overQuota) return overQuota;
|
|
6753
|
+
const name = object.customMetadata?.originalName ?? session.key.split("/").pop() ?? "file";
|
|
6754
|
+
return storedFileJson(session.key, name, url, scope);
|
|
6755
|
+
}
|
|
6756
|
+
async function multipartAbort(url, bucket, prefix) {
|
|
6757
|
+
const session = resolveSession(url, prefix);
|
|
6758
|
+
if (session instanceof Response) return session;
|
|
6759
|
+
try {
|
|
6760
|
+
await bucket.resumeMultipartUpload(session.key, session.uploadId).abort();
|
|
6761
|
+
} catch (error) {
|
|
6762
|
+
console.error("[files:multipart] abort:", error instanceof Error ? error.message : error);
|
|
6763
|
+
return Response.json({ success: true, aborted: false }, { headers: CORS_HEADERS3 });
|
|
6764
|
+
}
|
|
6765
|
+
return Response.json({ success: true, aborted: true }, { headers: CORS_HEADERS3 });
|
|
6766
|
+
}
|
|
6767
|
+
async function handleList(bucket, prefix, url, scope, auth) {
|
|
6450
6768
|
const userPrefix = url.searchParams.get("prefix") || "";
|
|
6451
6769
|
const listPrefix = `${prefix}${userPrefix}`;
|
|
6452
6770
|
const limit = parseInt(url.searchParams.get("limit") || "100", 10);
|
|
@@ -6462,7 +6780,16 @@ async function handleList(bucket, prefix, url, scope) {
|
|
|
6462
6780
|
...obj.customMetadata
|
|
6463
6781
|
};
|
|
6464
6782
|
});
|
|
6465
|
-
|
|
6783
|
+
let storage;
|
|
6784
|
+
if (auth.storage) {
|
|
6785
|
+
storage = { usedBytes: await allocationUsage(bucket, auth.storage.prefix) };
|
|
6786
|
+
const limitBytes = await auth.storage.limitBytes();
|
|
6787
|
+
if (limitBytes !== null) storage.limitBytes = limitBytes;
|
|
6788
|
+
}
|
|
6789
|
+
return Response.json(
|
|
6790
|
+
{ files, truncated: listed.truncated, ...storage ? { storage } : {} },
|
|
6791
|
+
{ headers: CORS_HEADERS3 }
|
|
6792
|
+
);
|
|
6466
6793
|
}
|
|
6467
6794
|
async function handleDownload(bucket, key, scope) {
|
|
6468
6795
|
const object = await bucket.get(key);
|
|
@@ -8583,7 +8910,12 @@ function serverCard(request, manifest, publicBasePath) {
|
|
|
8583
8910
|
name: manifest.name,
|
|
8584
8911
|
description: `Search and read the published ${documentationSubject(manifest.name)}.`,
|
|
8585
8912
|
version: MCP_SERVER_VERSION,
|
|
8913
|
+
// Preferred version, then everything the handshake will actually accept.
|
|
8914
|
+
// Advertising only the preferred one made the card look like it disagreed
|
|
8915
|
+
// with `initialize`, which per spec MUST echo a supported version the
|
|
8916
|
+
// client asked for rather than force the newest.
|
|
8586
8917
|
protocolVersion: CURRENT_PROTOCOL_VERSION,
|
|
8918
|
+
protocolVersions: [...SUPPORTED_PROTOCOL_VERSIONS],
|
|
8587
8919
|
transport: {
|
|
8588
8920
|
type: "streamable-http",
|
|
8589
8921
|
url: new URL(`${publicBasePath}/mcp`, request.url).toString()
|
|
@@ -9065,6 +9397,7 @@ export {
|
|
|
9065
9397
|
APP_ID_RE,
|
|
9066
9398
|
APP_LOG_EVENT_KEYS,
|
|
9067
9399
|
APP_NAME_RULES,
|
|
9400
|
+
APP_STORAGE_LIMIT_BYTES,
|
|
9068
9401
|
AUTO_PROVISIONABLE_TYPES,
|
|
9069
9402
|
AUTO_PROVISION_SENTINEL,
|
|
9070
9403
|
Awareness,
|
|
@@ -9097,6 +9430,9 @@ export {
|
|
|
9097
9430
|
JobRoom,
|
|
9098
9431
|
LOG_LEVELS,
|
|
9099
9432
|
MAX_APP_FILE_BYTES,
|
|
9433
|
+
MAX_DEPLOY_ASSET_FILE_BYTES,
|
|
9434
|
+
MAX_UPLOAD_PARTS,
|
|
9435
|
+
MAX_UPLOAD_REQUEST_BYTES,
|
|
9100
9436
|
MESSAGES_SCHEMA,
|
|
9101
9437
|
MSG,
|
|
9102
9438
|
MSG_AWARENESS,
|
|
@@ -9123,6 +9459,7 @@ export {
|
|
|
9123
9459
|
SYSTEM_MANAGED_COLUMNS,
|
|
9124
9460
|
SchemaRegistry,
|
|
9125
9461
|
TEST_ACCOUNT_EMAIL_SUFFIX,
|
|
9462
|
+
UPLOAD_PART_BYTES,
|
|
9126
9463
|
USERS_COLUMNS,
|
|
9127
9464
|
VOTING_SCHEMAS,
|
|
9128
9465
|
WORKSPACE_SCHEMAS,
|
|
@@ -9169,6 +9506,7 @@ export {
|
|
|
9169
9506
|
encodeSyncStep2,
|
|
9170
9507
|
encodeUpdate,
|
|
9171
9508
|
enqueueJob,
|
|
9509
|
+
formatBytes,
|
|
9172
9510
|
getChat,
|
|
9173
9511
|
getDeepSpaceAIModel,
|
|
9174
9512
|
getGlobalDOSchemas,
|
|
@@ -9180,6 +9518,7 @@ export {
|
|
|
9180
9518
|
handleSyncMessage,
|
|
9181
9519
|
hasContent,
|
|
9182
9520
|
isAutoProvision,
|
|
9521
|
+
isFilesVerbPath,
|
|
9183
9522
|
isOwner,
|
|
9184
9523
|
isTestAccountClaims,
|
|
9185
9524
|
isTestAccountEmail,
|
|
@@ -9210,6 +9549,8 @@ export {
|
|
|
9210
9549
|
rowToData,
|
|
9211
9550
|
runMigrations,
|
|
9212
9551
|
serverBuild,
|
|
9552
|
+
storageLimitForTier,
|
|
9553
|
+
storageQuotaMessage,
|
|
9213
9554
|
streamDeepSpaceAgent,
|
|
9214
9555
|
timingSafeEqualHex,
|
|
9215
9556
|
timingSafeEqualStrings,
|