deepspace 0.13.0 → 0.15.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 +33 -0
- package/dist/cli.js +426 -707
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +50 -5
- package/dist/server.js +95 -12
- package/dist/server.js.map +1 -1
- package/dist/worker.d.ts +125 -1
- package/dist/worker.js +177 -12
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
package/dist/server.d.ts
CHANGED
|
@@ -218,6 +218,33 @@ declare const UPLOAD_PART_BYTES: number;
|
|
|
218
218
|
* R2 allows 10,000 parts, so this is far inside what the API permits.
|
|
219
219
|
*/
|
|
220
220
|
declare const MAX_UPLOAD_PARTS: number;
|
|
221
|
+
/**
|
|
222
|
+
* Per-tier storage schedule, in bytes. Product semantics, not a deployment
|
|
223
|
+
* dial — the one table both storage allocations admit against:
|
|
224
|
+
*
|
|
225
|
+
* - the repo store (Git packs + retained bundles + deploy assets), admitted
|
|
226
|
+
* by the app's Repo DO (`platform/deploy-worker/src/vc/storage-quota.ts`)
|
|
227
|
+
* - the app-files allocation, admitted by the shared files handler
|
|
228
|
+
* (`server/utils/scoped-r2-files.ts`)
|
|
229
|
+
*
|
|
230
|
+
* Each allocation gets the full amount independently; they are different
|
|
231
|
+
* buckets with different writers (docs/platform/r2-storage.md). The
|
|
232
|
+
* customer-visible "storage" number is the app-files allocation — the repo
|
|
233
|
+
* store is platform bookkeeping that degrades (drops rollback retention)
|
|
234
|
+
* rather than surfacing here.
|
|
235
|
+
*/
|
|
236
|
+
declare const APP_STORAGE_LIMIT_BYTES: {
|
|
237
|
+
readonly test: 0;
|
|
238
|
+
readonly free: number;
|
|
239
|
+
readonly starter: number;
|
|
240
|
+
readonly premium: number;
|
|
241
|
+
readonly admin: number;
|
|
242
|
+
};
|
|
243
|
+
/** The storage limit for a billing tier; unknown or missing tiers get free. */
|
|
244
|
+
declare function storageLimitForTier(tier: string | null | undefined): number;
|
|
245
|
+
/** The one storage-quota refusal sentence. `usedBytes` is net of any object
|
|
246
|
+
* the upload replaces, so an upsert is charged only for its growth. */
|
|
247
|
+
declare function storageQuotaMessage(incomingBytes: number, usedBytes: number, limitBytes: number): string;
|
|
221
248
|
/**
|
|
222
249
|
* The largest single file a deploy may ship as a static asset.
|
|
223
250
|
*
|
|
@@ -290,8 +317,26 @@ interface ScopedR2Config {
|
|
|
290
317
|
*/
|
|
291
318
|
requireAuthForMutations?: boolean;
|
|
292
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* The storage-quota contract for one allocation, supplied per request by the
|
|
322
|
+
* mount. The handler is the enforcer; the mount only knows whose limit
|
|
323
|
+
* applies.
|
|
324
|
+
*/
|
|
325
|
+
interface StorageAdmission {
|
|
326
|
+
/** The WHOLE allocation the quota governs — the app prefix, not the
|
|
327
|
+
* (possibly deeper) scope prefix a request resolved to. */
|
|
328
|
+
prefix: string;
|
|
329
|
+
/**
|
|
330
|
+
* Resolve the owner's limit in bytes. `null` means the lookup failed;
|
|
331
|
+
* writes then fail closed (503) rather than admitting unmetered storage,
|
|
332
|
+
* mirroring the repo store's `StorageBillingUnavailableError`.
|
|
333
|
+
*/
|
|
334
|
+
limitBytes: () => Promise<number | null>;
|
|
335
|
+
}
|
|
293
336
|
interface ScopedR2Auth {
|
|
294
337
|
userId: string | null;
|
|
338
|
+
/** Absent = no quota on this mount (e.g. an app's own bucket). */
|
|
339
|
+
storage?: StorageAdmission;
|
|
295
340
|
}
|
|
296
341
|
type ScopedR2Handler = (request: Request, url: URL, bucket: R2Bucket, auth: ScopedR2Auth) => Promise<Response>;
|
|
297
342
|
declare function isFilesVerbPath(method: string, subpath: string): boolean;
|
|
@@ -3631,7 +3676,7 @@ declare function createDeepSpaceAuth(config: DeepSpaceAuthConfig): better_auth.A
|
|
|
3631
3676
|
id: string;
|
|
3632
3677
|
organizationId: string;
|
|
3633
3678
|
email: string;
|
|
3634
|
-
role: "
|
|
3679
|
+
role: "admin" | "member" | "owner";
|
|
3635
3680
|
status: better_auth_plugins.InvitationStatus;
|
|
3636
3681
|
inviterId: string;
|
|
3637
3682
|
expiresAt: Date;
|
|
@@ -3641,7 +3686,7 @@ declare function createDeepSpaceAuth(config: DeepSpaceAuthConfig): better_auth.A
|
|
|
3641
3686
|
Member: {
|
|
3642
3687
|
id: string;
|
|
3643
3688
|
organizationId: string;
|
|
3644
|
-
role: "
|
|
3689
|
+
role: "admin" | "member" | "owner";
|
|
3645
3690
|
createdAt: Date;
|
|
3646
3691
|
userId: string;
|
|
3647
3692
|
teamId?: string | undefined | undefined;
|
|
@@ -3669,7 +3714,7 @@ declare function createDeepSpaceAuth(config: DeepSpaceAuthConfig): better_auth.A
|
|
|
3669
3714
|
members: {
|
|
3670
3715
|
id: string;
|
|
3671
3716
|
organizationId: string;
|
|
3672
|
-
role: "
|
|
3717
|
+
role: "admin" | "member" | "owner";
|
|
3673
3718
|
createdAt: Date;
|
|
3674
3719
|
userId: string;
|
|
3675
3720
|
teamId?: string | undefined | undefined;
|
|
@@ -3684,7 +3729,7 @@ declare function createDeepSpaceAuth(config: DeepSpaceAuthConfig): better_auth.A
|
|
|
3684
3729
|
id: string;
|
|
3685
3730
|
organizationId: string;
|
|
3686
3731
|
email: string;
|
|
3687
|
-
role: "
|
|
3732
|
+
role: "admin" | "member" | "owner";
|
|
3688
3733
|
status: better_auth_plugins.InvitationStatus;
|
|
3689
3734
|
inviterId: string;
|
|
3690
3735
|
expiresAt: Date;
|
|
@@ -4614,4 +4659,4 @@ declare class RefundError extends Error {
|
|
|
4614
4659
|
constructor(message: string, status: number);
|
|
4615
4660
|
}
|
|
4616
4661
|
|
|
4617
|
-
export { AI_CHATS_SCHEMA, AI_MESSAGES_SCHEMA, ALLOWED_BINDING_TYPES, APP_ID_RE, APP_NAME_RULES, AUTO_PROVISIONABLE_TYPES, AUTO_PROVISION_SENTINEL, type ActionContext, type ActionHandler, type ActionResult, type ActionTools, type ApiWorkerEnv, type AppNameResolution, type AppNameValidation, type AuthWorkerEnv, BASE_USERS_SCHEMA, BUILT_IN_TOOLS, BaseRoom, CHANNELS_SCHEMA, CHANNEL_INVITATIONS_SCHEMA, CHANNEL_MEMBERS_SCHEMA, CONVERSATION_SCHEMAS, COST_RATES, CancelSubscriptionError, type CancelSubscriptionOpts, type CancelSubscriptionResult, CanvasRoom, type CanvasShape, type ChatContextConfig, type ChatMessageRow, type ChatRow, type ChatTurn, type CollectionPermissionSummary, type CollectionSchema, type ColumnDefinition, type ColumnInterpretation, type ComposioToolsOptions, type ConvMemberData, type ConvMessageData, type ConvReactionData, type ConvReadCursorData, type ConvVoteData, type ConversationStateData, type CronContext, type CronExecution, CronRoom, type CronRoomConfig, type CronTask, type CustomBinding, type CustomBindingManifest, DEFAULT_CONTEXT_CONFIG, DEFAULT_DO_MANIFEST, DEFAULT_QUERY_LIMIT, DEPLOYED_APPS_DEFAULTS, DIRECTORY_SCHEMAS, type DOBindings, type DOManifest, type DOManifestEntry, type DebugApiContext, type DeepSpaceAIEnv, type DeepSpaceAIOptions, type DeepSpaceAgentDiagnosticContext, DeepSpaceAgentModelError, DeepSpaceAgentProfileError, type DeepSpaceAgentStream, type DeepSpaceAgentStreamOptions, type DeepSpaceAuth, type DeepSpaceAuthConfig, type DirectoryCommunityData, type DirectoryConversationData, type DirectoryMembershipData, type DirectoryPostData, type DoMigrationDirective, type DoMigrationPlan, type ExistingDOBinding, GLOBAL_DO_TYPES, GLOBAL_DO_TYPE_NAMES, type GameInput, GameRoom, type GameRoomConfig, type GetActionData, type GlobalDOType, type Job, type JobContext, JobRoom, type JobRoomConfig, type JobStatus, type JwtClaims, type JwtVerifierConfig, MAX_APP_FILE_BYTES, MAX_DEPLOY_ASSET_FILE_BYTES, MAX_UPLOAD_PARTS, MAX_UPLOAD_REQUEST_BYTES, MESSAGES_SCHEMA, type MutateActionData, type PermissionAnalysis, type PermissionContext, type PermissionLevel, type PermissionSource, type PlanInfo, type PlatformWorkerEnv, type Player, type PrefixResult, type PresencePeer, PresenceRoom, type QueryActionData, REACTIONS_SCHEMA, READ_RECEIPTS_SCHEMA, REGISTRY_INSTANCE, RESERVED_BINDING_NAMES, RESERVED_COLLECTION_NAMES, RESOURCE_ID_RE, type RecordContext, RecordRoom, type RecordRoomConfig, RefundError, type RefundOpts, type RefundResult, RegistryClientError, type ResolvedColumn, type ResolvedPermission, type RolePermissions, type RunMigrationsResult, STRICT_APP_ID_RE, SYSTEM_COLLECTION_SCHEMAS, SYSTEM_MANAGED_COLUMNS, SchemaRegistry, type ScopeContext, type ScopedR2Auth, type ScopedR2Config, type ScopedR2Handler, type ScreenshotEnv, type ScreenshotResult, type SharedConnection, type StoredRecord, SubscriptionAuthError, type SubscriptionContext, type SubscriptionRead, SubscriptionRequiredError, type SubscriptionStatus, type Summarizer, TEST_ACCOUNT_EMAIL_SUFFIX, type TokenDebugInfo, type ToolResult, type ToolSchema, type ToolsApiContext, UPLOAD_PART_BYTES, USERS_COLUMNS, type User, type UserAttachment, type UserContext, VOTING_SCHEMAS, type ValidationError, type VerifiedAuth, type VerifyOutcome, type VerifyResult, type Viewport, WORKSPACE_SCHEMAS, type YjsContext, YjsRoom, analyzePermissions, apiWorkerFetch, appendMessage, applyAiToolDefaults, applySlidingWindow, authWorkerFetch, bindingManifestFromOutputConfig, broadcastChange, broadcastYjsUpdate, buildCronContext, buildTableSelect, buildUiParts, callRegistry, canCreate, canDelete, canRead, canUpdate, cancelSubscription, capToolResultSize, captureScreenshot, checkFieldPermissions, coerceValue, collectionTableName, columnId, composioTools, computeDoMigration, computeHmacHex, createChat, createDeepSpaceAI, createDeepSpaceAuth, createScopedR2Handler, dataToColumnValues, decodeJwtPayload, deepSpaceAgentErrorSummary, deleteChatCascade, deleteRecord, enqueueJob, executeQuery, formatBytes, getAllUsers, getChat, getGlobalDOSchemas, getGlobalDOType, getOrCreateYjsDoc, getRecord, getRolePermissions, getSubscription, getUser, getYjsDocKey, handleApiRequest, handleDelete, handlePut, handleSetRole, handleSubscribe, handleToolsRequest, handleUnsubscribe, handleUserList, handleUserUpdate, handleYjsBinaryMessage, handleYjsJoin, handleYjsLeave, isAutoProvision, isFilesVerbPath, isOwner, isTestAccountClaims, isTestAccountEmail, isTestAccountTier, lintSchema, loadMessages, makeDefaultSummarizer, matchOrigin, meterAi, meterUsage, meterVectorize, noopPermissionContext, parseQuotaLimit, platformCorsPatterns, platformWorkerFetch, prepareMessagesWithCompaction, priceBindingUsageEvent, putRecord, readRecord, recordMatchesSubscription, refundInvoice, registerUser, registryErrorJson, registryStub, requireSubscription, resolveAppName, resolveColumn, rowToData, runMigrations, saveYjsDoc, streamDeepSpaceAgent, timingSafeEqualHex, timingSafeEqualStrings, totalChars, truncateOldToolResults, turnsToCoreMessages, unwrapToolOutput, updateChat, validateAppName, validateBindingManifest, validateDoManifest, verifyJwt, workspaceAccountsSchema, workspaceContentSharesSchema, workspaceEmailHandlesSchema, workspaceFormResponsesSchema, workspacePeopleSchema, workspaceProjectsSchema, workspaceTagsSchema, workspaceTasksSchema, workspaceTeamMembersSchema, workspaceTeamsSchema, workspaceTransactionsSchema };
|
|
4662
|
+
export { AI_CHATS_SCHEMA, AI_MESSAGES_SCHEMA, ALLOWED_BINDING_TYPES, APP_ID_RE, APP_NAME_RULES, APP_STORAGE_LIMIT_BYTES, AUTO_PROVISIONABLE_TYPES, AUTO_PROVISION_SENTINEL, type ActionContext, type ActionHandler, type ActionResult, type ActionTools, type ApiWorkerEnv, type AppNameResolution, type AppNameValidation, type AuthWorkerEnv, BASE_USERS_SCHEMA, BUILT_IN_TOOLS, BaseRoom, CHANNELS_SCHEMA, CHANNEL_INVITATIONS_SCHEMA, CHANNEL_MEMBERS_SCHEMA, CONVERSATION_SCHEMAS, COST_RATES, CancelSubscriptionError, type CancelSubscriptionOpts, type CancelSubscriptionResult, CanvasRoom, type CanvasShape, type ChatContextConfig, type ChatMessageRow, type ChatRow, type ChatTurn, type CollectionPermissionSummary, type CollectionSchema, type ColumnDefinition, type ColumnInterpretation, type ComposioToolsOptions, type ConvMemberData, type ConvMessageData, type ConvReactionData, type ConvReadCursorData, type ConvVoteData, type ConversationStateData, type CronContext, type CronExecution, CronRoom, type CronRoomConfig, type CronTask, type CustomBinding, type CustomBindingManifest, DEFAULT_CONTEXT_CONFIG, DEFAULT_DO_MANIFEST, DEFAULT_QUERY_LIMIT, DEPLOYED_APPS_DEFAULTS, DIRECTORY_SCHEMAS, type DOBindings, type DOManifest, type DOManifestEntry, type DebugApiContext, type DeepSpaceAIEnv, type DeepSpaceAIOptions, type DeepSpaceAgentDiagnosticContext, DeepSpaceAgentModelError, DeepSpaceAgentProfileError, type DeepSpaceAgentStream, type DeepSpaceAgentStreamOptions, type DeepSpaceAuth, type DeepSpaceAuthConfig, type DirectoryCommunityData, type DirectoryConversationData, type DirectoryMembershipData, type DirectoryPostData, type DoMigrationDirective, type DoMigrationPlan, type ExistingDOBinding, GLOBAL_DO_TYPES, GLOBAL_DO_TYPE_NAMES, type GameInput, GameRoom, type GameRoomConfig, type GetActionData, type GlobalDOType, type Job, type JobContext, JobRoom, type JobRoomConfig, type JobStatus, type JwtClaims, type JwtVerifierConfig, MAX_APP_FILE_BYTES, MAX_DEPLOY_ASSET_FILE_BYTES, MAX_UPLOAD_PARTS, MAX_UPLOAD_REQUEST_BYTES, MESSAGES_SCHEMA, type MutateActionData, type PermissionAnalysis, type PermissionContext, type PermissionLevel, type PermissionSource, type PlanInfo, type PlatformWorkerEnv, type Player, type PrefixResult, type PresencePeer, PresenceRoom, type QueryActionData, REACTIONS_SCHEMA, READ_RECEIPTS_SCHEMA, REGISTRY_INSTANCE, RESERVED_BINDING_NAMES, RESERVED_COLLECTION_NAMES, RESOURCE_ID_RE, type RecordContext, RecordRoom, type RecordRoomConfig, RefundError, type RefundOpts, type RefundResult, RegistryClientError, type ResolvedColumn, type ResolvedPermission, type RolePermissions, type RunMigrationsResult, STRICT_APP_ID_RE, SYSTEM_COLLECTION_SCHEMAS, SYSTEM_MANAGED_COLUMNS, SchemaRegistry, type ScopeContext, type ScopedR2Auth, type ScopedR2Config, type ScopedR2Handler, type ScreenshotEnv, type ScreenshotResult, type SharedConnection, type StorageAdmission, type StoredRecord, SubscriptionAuthError, type SubscriptionContext, type SubscriptionRead, SubscriptionRequiredError, type SubscriptionStatus, type Summarizer, TEST_ACCOUNT_EMAIL_SUFFIX, type TokenDebugInfo, type ToolResult, type ToolSchema, type ToolsApiContext, UPLOAD_PART_BYTES, USERS_COLUMNS, type User, type UserAttachment, type UserContext, VOTING_SCHEMAS, type ValidationError, type VerifiedAuth, type VerifyOutcome, type VerifyResult, type Viewport, WORKSPACE_SCHEMAS, type YjsContext, YjsRoom, analyzePermissions, apiWorkerFetch, appendMessage, applyAiToolDefaults, applySlidingWindow, authWorkerFetch, bindingManifestFromOutputConfig, broadcastChange, broadcastYjsUpdate, buildCronContext, buildTableSelect, buildUiParts, callRegistry, canCreate, canDelete, canRead, canUpdate, cancelSubscription, capToolResultSize, captureScreenshot, checkFieldPermissions, coerceValue, collectionTableName, columnId, composioTools, computeDoMigration, computeHmacHex, createChat, createDeepSpaceAI, createDeepSpaceAuth, createScopedR2Handler, dataToColumnValues, decodeJwtPayload, deepSpaceAgentErrorSummary, deleteChatCascade, deleteRecord, enqueueJob, executeQuery, formatBytes, getAllUsers, getChat, getGlobalDOSchemas, getGlobalDOType, getOrCreateYjsDoc, getRecord, getRolePermissions, getSubscription, getUser, getYjsDocKey, handleApiRequest, handleDelete, handlePut, handleSetRole, handleSubscribe, handleToolsRequest, handleUnsubscribe, handleUserList, handleUserUpdate, handleYjsBinaryMessage, handleYjsJoin, handleYjsLeave, isAutoProvision, isFilesVerbPath, isOwner, isTestAccountClaims, isTestAccountEmail, isTestAccountTier, lintSchema, loadMessages, makeDefaultSummarizer, matchOrigin, meterAi, meterUsage, meterVectorize, noopPermissionContext, parseQuotaLimit, platformCorsPatterns, platformWorkerFetch, prepareMessagesWithCompaction, priceBindingUsageEvent, putRecord, readRecord, recordMatchesSubscription, refundInvoice, registerUser, registryErrorJson, registryStub, requireSubscription, resolveAppName, resolveColumn, rowToData, runMigrations, saveYjsDoc, storageLimitForTier, storageQuotaMessage, streamDeepSpaceAgent, timingSafeEqualHex, timingSafeEqualStrings, totalChars, truncateOldToolResults, turnsToCoreMessages, unwrapToolOutput, updateChat, validateAppName, validateBindingManifest, validateDoManifest, verifyJwt, workspaceAccountsSchema, workspaceContentSharesSchema, workspaceEmailHandlesSchema, workspaceFormResponsesSchema, workspacePeopleSchema, workspaceProjectsSchema, workspaceTagsSchema, workspaceTasksSchema, workspaceTeamMembersSchema, workspaceTeamsSchema, workspaceTransactionsSchema };
|
package/dist/server.js
CHANGED
|
@@ -180,6 +180,23 @@ var MAX_UPLOAD_REQUEST_BYTES = 25 * 1024 * 1024;
|
|
|
180
180
|
var UPLOAD_PART_BYTES = 20 * 1024 * 1024;
|
|
181
181
|
var MAX_UPLOAD_PARTS = Math.ceil(MAX_APP_FILE_BYTES / UPLOAD_PART_BYTES);
|
|
182
182
|
var MAX_BASE64_UPLOAD_BYTES = Math.floor(MAX_UPLOAD_REQUEST_BYTES * 3 / 4);
|
|
183
|
+
var APP_STORAGE_LIMIT_BYTES = {
|
|
184
|
+
test: 0,
|
|
185
|
+
free: 128 * 1024 * 1024,
|
|
186
|
+
starter: 512 * 1024 * 1024,
|
|
187
|
+
premium: 2 * 1024 * 1024 * 1024,
|
|
188
|
+
admin: 10 * 1024 * 1024 * 1024
|
|
189
|
+
};
|
|
190
|
+
function storageLimitForTier(tier) {
|
|
191
|
+
if (tier && Object.hasOwn(APP_STORAGE_LIMIT_BYTES, tier)) {
|
|
192
|
+
return APP_STORAGE_LIMIT_BYTES[tier];
|
|
193
|
+
}
|
|
194
|
+
return APP_STORAGE_LIMIT_BYTES.free;
|
|
195
|
+
}
|
|
196
|
+
function storageQuotaMessage(incomingBytes, usedBytes, limitBytes) {
|
|
197
|
+
const free = Math.max(0, limitBytes - usedBytes);
|
|
198
|
+
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.`;
|
|
199
|
+
}
|
|
183
200
|
var MAX_DEPLOY_ASSET_FILE_BYTES = 25 * 1024 * 1024;
|
|
184
201
|
var DANGEROUS_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
185
202
|
"text/html",
|
|
@@ -266,6 +283,52 @@ function declaredLength(request, what) {
|
|
|
266
283
|
function isKeyWithinPrefix(key, prefix) {
|
|
267
284
|
return key.startsWith(prefix);
|
|
268
285
|
}
|
|
286
|
+
async function allocationUsage(bucket, prefix) {
|
|
287
|
+
let total = 0;
|
|
288
|
+
let cursor;
|
|
289
|
+
do {
|
|
290
|
+
const page = await bucket.list({ prefix, cursor, limit: 1e3 });
|
|
291
|
+
for (const object of page.objects) total += object.size;
|
|
292
|
+
cursor = page.truncated ? page.cursor : void 0;
|
|
293
|
+
} while (cursor !== void 0);
|
|
294
|
+
return total;
|
|
295
|
+
}
|
|
296
|
+
async function admitStorage(bucket, storage, incomingBytes, replacesKey) {
|
|
297
|
+
if (!storage) return null;
|
|
298
|
+
const limit = await storage.limitBytes();
|
|
299
|
+
if (limit === null) {
|
|
300
|
+
return fail(
|
|
301
|
+
503,
|
|
302
|
+
"The storage limit for this app could not be verified; nothing was uploaded. Retry shortly.",
|
|
303
|
+
"storage_limit_unavailable"
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
const stored = await allocationUsage(bucket, storage.prefix);
|
|
307
|
+
const replaced = replacesKey === null ? 0 : (await bucket.head(replacesKey))?.size ?? 0;
|
|
308
|
+
const used = stored - replaced;
|
|
309
|
+
if (used + incomingBytes <= limit) return null;
|
|
310
|
+
return fail(409, storageQuotaMessage(incomingBytes, used, limit), "storage_quota_exceeded", {
|
|
311
|
+
usedBytes: used,
|
|
312
|
+
limitBytes: limit
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
async function admitAssembledObject(bucket, storage, key, objectBytes) {
|
|
316
|
+
if (!storage) return null;
|
|
317
|
+
const limit = await storage.limitBytes();
|
|
318
|
+
if (limit === null) {
|
|
319
|
+
console.error("[files:quota] limit unavailable at complete; keeping", key);
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
const stored = await allocationUsage(bucket, storage.prefix);
|
|
323
|
+
if (stored <= limit) return null;
|
|
324
|
+
await bucket.delete(key).catch(() => void 0);
|
|
325
|
+
return fail(
|
|
326
|
+
409,
|
|
327
|
+
`${storageQuotaMessage(objectBytes, stored - objectBytes, limit)} The assembled upload was not kept.`,
|
|
328
|
+
"storage_quota_exceeded",
|
|
329
|
+
{ usedBytes: stored - objectBytes, limitBytes: limit }
|
|
330
|
+
);
|
|
331
|
+
}
|
|
269
332
|
function anchorKey(raw, prefix) {
|
|
270
333
|
const safe = sanitizeSubpath(raw);
|
|
271
334
|
if (safe === null) return fail(400, "Invalid key: traversal not allowed", "invalid_key");
|
|
@@ -355,10 +418,10 @@ function createScopedR2Handler(config) {
|
|
|
355
418
|
await request.body?.cancel().catch(() => void 0);
|
|
356
419
|
return fail(401, "Authentication required", "unauthorized");
|
|
357
420
|
}
|
|
358
|
-
return subpath === "upload" ? handleUpload(request, url, bucket, prefix, auth
|
|
421
|
+
return subpath === "upload" ? handleUpload(request, url, bucket, prefix, auth, scope) : handleMultipart(request, url, bucket, prefix, auth, scope, subpath);
|
|
359
422
|
}
|
|
360
423
|
if (!subpath && request.method === "GET") {
|
|
361
|
-
return handleList(bucket, prefix, url, scope);
|
|
424
|
+
return handleList(bucket, prefix, url, scope, auth);
|
|
362
425
|
}
|
|
363
426
|
if (subpath && request.method === "GET") {
|
|
364
427
|
if (!isKeyWithinPrefix(subpath, prefix)) {
|
|
@@ -392,7 +455,7 @@ function createScopedR2Handler(config) {
|
|
|
392
455
|
return Response.json({ error: "Method not allowed" }, { status: 405, headers: CORS_HEADERS });
|
|
393
456
|
};
|
|
394
457
|
}
|
|
395
|
-
async function handleUpload(request, url, bucket, prefix,
|
|
458
|
+
async function handleUpload(request, url, bucket, prefix, auth, scope) {
|
|
396
459
|
try {
|
|
397
460
|
const declared = overRequestBound(Number(request.headers.get("content-length")), 8 * 1024);
|
|
398
461
|
if (declared) return declared;
|
|
@@ -423,9 +486,12 @@ async function handleUpload(request, url, bucket, prefix, userId, scope) {
|
|
|
423
486
|
if (mime instanceof Response) return mime;
|
|
424
487
|
const located = locateUpload(url, prefix, fileName);
|
|
425
488
|
if (located instanceof Response) return located;
|
|
489
|
+
const replacesKey = url.searchParams.get("key") === null ? null : located.key;
|
|
490
|
+
const overQuota = await admitStorage(bucket, auth.storage, fileData.byteLength, replacesKey);
|
|
491
|
+
if (overQuota) return overQuota;
|
|
426
492
|
await bucket.put(located.key, fileData, {
|
|
427
493
|
httpMetadata: { contentType: mime.mimeType },
|
|
428
|
-
customMetadata: fileMetadata(fileName, userId)
|
|
494
|
+
customMetadata: fileMetadata(fileName, auth.userId)
|
|
429
495
|
});
|
|
430
496
|
return storedFileJson(located.key, fileName, url, scope);
|
|
431
497
|
} catch (err) {
|
|
@@ -500,17 +566,17 @@ async function streamPart(upload, partNumber, body, expectedBytes) {
|
|
|
500
566
|
}
|
|
501
567
|
return { ok: true, part: uploaded.value };
|
|
502
568
|
}
|
|
503
|
-
async function handleMultipart(request, url, bucket, prefix,
|
|
569
|
+
async function handleMultipart(request, url, bucket, prefix, auth, scope, subpath) {
|
|
504
570
|
if (subpath === "multipart" && request.method === "POST") {
|
|
505
|
-
return multipartInit(request, url, bucket, prefix,
|
|
571
|
+
return multipartInit(request, url, bucket, prefix, auth);
|
|
506
572
|
}
|
|
507
573
|
if (subpath === "multipart/part") return multipartPart(request, url, bucket, prefix);
|
|
508
574
|
if (subpath === "multipart/complete") {
|
|
509
|
-
return multipartComplete(request, url, bucket, prefix, scope);
|
|
575
|
+
return multipartComplete(request, url, bucket, prefix, auth, scope);
|
|
510
576
|
}
|
|
511
577
|
return multipartAbort(url, bucket, prefix);
|
|
512
578
|
}
|
|
513
|
-
async function multipartInit(request, url, bucket, prefix,
|
|
579
|
+
async function multipartInit(request, url, bucket, prefix, auth) {
|
|
514
580
|
const body = await readControlJson(request);
|
|
515
581
|
if (body instanceof Response) return body;
|
|
516
582
|
const size = body.size;
|
|
@@ -525,11 +591,14 @@ async function multipartInit(request, url, bucket, prefix, userId) {
|
|
|
525
591
|
if (mime instanceof Response) return mime;
|
|
526
592
|
const located = locateUpload(url, prefix, name);
|
|
527
593
|
if (located instanceof Response) return located;
|
|
594
|
+
const replacesKey = url.searchParams.get("key") === null ? null : located.key;
|
|
595
|
+
const overQuota = await admitStorage(bucket, auth.storage, size, replacesKey);
|
|
596
|
+
if (overQuota) return overQuota;
|
|
528
597
|
let upload;
|
|
529
598
|
try {
|
|
530
599
|
upload = await bucket.createMultipartUpload(located.key, {
|
|
531
600
|
httpMetadata: { contentType: mime.mimeType },
|
|
532
|
-
customMetadata: fileMetadata(name, userId)
|
|
601
|
+
customMetadata: fileMetadata(name, auth.userId)
|
|
533
602
|
});
|
|
534
603
|
} catch (error) {
|
|
535
604
|
return multipartFailure(error);
|
|
@@ -579,7 +648,7 @@ async function multipartPart(request, url, bucket, prefix) {
|
|
|
579
648
|
{ headers: CORS_HEADERS }
|
|
580
649
|
);
|
|
581
650
|
}
|
|
582
|
-
async function multipartComplete(request, url, bucket, prefix, scope) {
|
|
651
|
+
async function multipartComplete(request, url, bucket, prefix, auth, scope) {
|
|
583
652
|
const body = await readControlJson(request);
|
|
584
653
|
if (body instanceof Response) return body;
|
|
585
654
|
const session = resolveSession(url, prefix);
|
|
@@ -613,6 +682,8 @@ async function multipartComplete(request, url, bucket, prefix, scope) {
|
|
|
613
682
|
await bucket.delete(session.key).catch(() => void 0);
|
|
614
683
|
return tooBig;
|
|
615
684
|
}
|
|
685
|
+
const overQuota = await admitAssembledObject(bucket, auth.storage, session.key, object.size);
|
|
686
|
+
if (overQuota) return overQuota;
|
|
616
687
|
const name = object.customMetadata?.originalName ?? session.key.split("/").pop() ?? "file";
|
|
617
688
|
return storedFileJson(session.key, name, url, scope);
|
|
618
689
|
}
|
|
@@ -627,7 +698,7 @@ async function multipartAbort(url, bucket, prefix) {
|
|
|
627
698
|
}
|
|
628
699
|
return Response.json({ success: true, aborted: true }, { headers: CORS_HEADERS });
|
|
629
700
|
}
|
|
630
|
-
async function handleList(bucket, prefix, url, scope) {
|
|
701
|
+
async function handleList(bucket, prefix, url, scope, auth) {
|
|
631
702
|
const userPrefix = url.searchParams.get("prefix") || "";
|
|
632
703
|
const listPrefix = `${prefix}${userPrefix}`;
|
|
633
704
|
const limit = parseInt(url.searchParams.get("limit") || "100", 10);
|
|
@@ -643,7 +714,16 @@ async function handleList(bucket, prefix, url, scope) {
|
|
|
643
714
|
...obj.customMetadata
|
|
644
715
|
};
|
|
645
716
|
});
|
|
646
|
-
|
|
717
|
+
let storage;
|
|
718
|
+
if (auth.storage) {
|
|
719
|
+
storage = { usedBytes: await allocationUsage(bucket, auth.storage.prefix) };
|
|
720
|
+
const limitBytes = await auth.storage.limitBytes();
|
|
721
|
+
if (limitBytes !== null) storage.limitBytes = limitBytes;
|
|
722
|
+
}
|
|
723
|
+
return Response.json(
|
|
724
|
+
{ files, truncated: listed.truncated, ...storage ? { storage } : {} },
|
|
725
|
+
{ headers: CORS_HEADERS }
|
|
726
|
+
);
|
|
647
727
|
}
|
|
648
728
|
async function handleDownload(bucket, key, scope) {
|
|
649
729
|
const object = await bucket.get(key);
|
|
@@ -7909,6 +7989,7 @@ export {
|
|
|
7909
7989
|
ALLOWED_BINDING_TYPES,
|
|
7910
7990
|
APP_ID_RE,
|
|
7911
7991
|
APP_NAME_RULES,
|
|
7992
|
+
APP_STORAGE_LIMIT_BYTES,
|
|
7912
7993
|
AUTO_PROVISIONABLE_TYPES,
|
|
7913
7994
|
AUTO_PROVISION_SENTINEL,
|
|
7914
7995
|
BASE_USERS_SCHEMA,
|
|
@@ -8053,6 +8134,8 @@ export {
|
|
|
8053
8134
|
rowToData,
|
|
8054
8135
|
runMigrations,
|
|
8055
8136
|
saveYjsDoc,
|
|
8137
|
+
storageLimitForTier,
|
|
8138
|
+
storageQuotaMessage,
|
|
8056
8139
|
streamDeepSpaceAgent,
|
|
8057
8140
|
timingSafeEqualHex,
|
|
8058
8141
|
timingSafeEqualStrings,
|