@protobase/server 0.30.0 → 0.30.2

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.
@@ -15,7 +15,7 @@ export type AdminOptions = {
15
15
  basePath?: string;
16
16
  /**
17
17
  * The app's publishable and secret keys, with which supabase-js calls the PostgREST-compatible `/rest/v1` and the
18
- * Realtime-compatible `/realtime/v1` (see `ApiKeysOptions`); default `PROTOBASE_PUBLISHABLE_KEY` and
18
+ * Realtime-compatible `/realtime/v1` and the Storage-compatible `/storage/v1` (see `ApiKeysOptions`); default `PROTOBASE_PUBLISHABLE_KEY` and
19
19
  * `PROTOBASE_SECRET_KEY`. Without either, or with `false`, the app serves neither.
20
20
  */
21
21
  apiKeys?: ApiKeysOptions | false;
@@ -0,0 +1,27 @@
1
+ //#region src/api-keys/caller-settings.ts
2
+ /**
3
+ * The transaction settings a request admitted by key (`/rest/v1`, `/storage/v1`) runs with, as Supabase's PostgREST
4
+ * sets them: `role`, which is `SET LOCAL ROLE`, to `anon` or `authenticated`, and the caller's claims in `request.jwt.claims` and its user in
5
+ * `request.jwt.claim.sub`, which `auth.uid()`, `auth.role()` and `auth.jwt()` read. The `role` claim names the
6
+ * database role, as on Supabase, whatever the token's own says. The app's role, of the secret key, sets no role: the
7
+ * connection's own owns the tables. A signed-in user is the actor change capture records, when `actorSetting` is set.
8
+ */
9
+ const callerSettings = (caller, actorSetting) => {
10
+ const role = caller.role === "app" ? "service_role" : caller.role;
11
+ const sub = caller.role === "authenticated" ? caller.claims.sub : void 0;
12
+ const actor = sub === void 0 ? void 0 : {
13
+ id: sub,
14
+ roles: []
15
+ };
16
+ return {
17
+ ...caller.role !== "app" && { role: caller.role },
18
+ "request.jwt.claims": JSON.stringify({
19
+ ...caller.claims,
20
+ role
21
+ }),
22
+ ...sub !== void 0 && { "request.jwt.claim.sub": sub },
23
+ ...actor && actorSetting && { [actorSetting]: JSON.stringify(actor) }
24
+ };
25
+ };
26
+ //#endregion
27
+ export { callerSettings };
@@ -72,7 +72,7 @@ export type CreateAdminInput = {
72
72
  formsDifficulty?: Difficulty;
73
73
  /** @internal The users comments name, mention and mail, which tests fake; from `auth` otherwise. */
74
74
  commentUsers?: CommentUsers;
75
- /** @internal Verifies a user's token on `/rest/v1` and `/realtime/v1` and gives its claims, which tests fake; those `/auth/v1` signs otherwise. */
75
+ /** @internal Verifies a user's token on `/rest/v1`, `/realtime/v1` and `/storage/v1` and gives its claims, which tests fake; those `/auth/v1` signs otherwise. */
76
76
  verifyToken?: (token: string) => Promise<JWTPayload>;
77
77
  /**
78
78
  * The host's WebSocket upgrade, such as `@hono/node-server`'s, with which an app with keys and live resources serves
@@ -17,6 +17,7 @@ import { apiKeyCaller } from "./api-keys/resolve-caller.js";
17
17
  import { jwtVerifier } from "./auth/jwt.js";
18
18
  import { realtimeApp } from "./realtime/realtime-app.js";
19
19
  import { restRoutes } from "./rest/rest-routes.js";
20
+ import { storageRoutes } from "./storage/storage-routes.js";
20
21
  import { readAppName } from "./app-name.js";
21
22
  import { goTruePath } from "./gotrue/keys.js";
22
23
  import { authBasePath } from "./better-auth/auth-base-path.js";
@@ -139,11 +140,13 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
139
140
  pages: pageModels.map((entry) => entry.name)
140
141
  });
141
142
  const apiKeys = apiKeySettings(options.apiKeys, globalThis.process?.env ?? {});
143
+ const servesKeys = Boolean(apiKeys.publishable || apiKeys.secret);
142
144
  const files = createFilesRuntime({
143
145
  ...filesOptions && { options: filesOptions },
144
146
  env: globalThis.process?.env ?? {},
145
147
  models: registry.entries.map((entry) => entry.model),
146
- systemPath
148
+ systemPath,
149
+ storage: servesKeys
147
150
  });
148
151
  const table = webhooksOptions && webhookTable(webhooksOptions, auth);
149
152
  const withAi = withAiFields || hasAiActions(viewModels);
@@ -286,7 +289,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
286
289
  report
287
290
  });
288
291
  if (appModels.some((model) => model.live)) app.route(systemPath || "/", guarded((live) => void live.route("/", liveRoutes(feed, (session) => visibleNotices(deps, session))), withAnonymous(authenticate, options.roles)));
289
- if (apiKeys.publishable || apiKeys.secret) {
292
+ if (servesKeys) {
290
293
  const verify = verifyToken ?? (auth ? jwtVerifier(goTrueTokens({ auth })) : () => Promise.reject(unauthorized("This app signs no user tokens")));
291
294
  const caller = apiKeyCaller({
292
295
  keys: apiKeys,
@@ -304,6 +307,21 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
304
307
  caller,
305
308
  report
306
309
  }));
310
+ if (files?.storage) {
311
+ const storage = {
312
+ db,
313
+ statementTimeoutMs: deps.statementTimeoutMs,
314
+ ...deps.actorSetting && { actorSetting: deps.actorSetting },
315
+ store: files.providers[files.storage.provider].store,
316
+ signer: files.signer,
317
+ maxBytes: files.storage.maxBytes,
318
+ caller
319
+ };
320
+ app.route("/storage/v1", storageRoutes({
321
+ deps: storage,
322
+ report
323
+ }));
324
+ }
307
325
  }
308
326
  app.route(basePath, api);
309
327
  app.route(`${deps.systemPath}/meta`, metaApp);
@@ -7,6 +7,7 @@ import { fetchRecord } from "../records.js";
7
7
  import { readTransaction } from "../transactions.js";
8
8
  //#region src/files/download.ts
9
9
  const gone = () => notFound("There is no such file");
10
+ /** One `bytes=start-end` range, inclusive; `undefined` for none, `false` for one that cannot be served. */
10
11
  const rangeOf = (header, size) => {
11
12
  if (!header) return void 0;
12
13
  const match = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
@@ -106,4 +107,4 @@ const serveFile = async (deps, request, provider, path) => {
106
107
  });
107
108
  };
108
109
  //#endregion
109
- export { serveFile };
110
+ export { rangeOf, serveFile };
@@ -5,6 +5,11 @@ export type FileLimits = {
5
5
  maxBytes?: number | string;
6
6
  maxFiles?: number;
7
7
  };
8
+ /** What `store` holds, in bytes and files, leaving out its own `.cleanup/` entries. */
9
+ export declare const storeUsage: (store: FileStore) => Promise<{
10
+ bytes: number;
11
+ files: number;
12
+ }>;
8
13
  /**
9
14
  * `store` refusing a file with `507` once it would hold more than `maxBytes` or `maxFiles`. Only files count, not the
10
15
  * store's own `.cleanup/` entries. What the store holds is counted with `list` on the first upload and again when the
@@ -4,6 +4,14 @@ import { parseSize } from "@protobase/schema/internal";
4
4
  //#region src/files/limits.ts
5
5
  /** How long a count of what a store holds is trusted before it is taken again. */
6
6
  const recountMs = 3e5;
7
+ /** What `store` holds, in bytes and files, leaving out its own `.cleanup/` entries. */
8
+ const storeUsage = async (store) => {
9
+ const files = (await store.list("")).filter((object) => isFilePath(object.path));
10
+ return {
11
+ bytes: files.reduce((sum, object) => sum + object.size, 0),
12
+ files: files.length
13
+ };
14
+ };
7
15
  const limitReached = () => new HttpProblem(507, "storage-full", "Insufficient Storage", "The file storage is full; delete files to make room");
8
16
  var LimitReached = class extends Error {};
9
17
  const checkedFiles = (value) => {
@@ -27,10 +35,8 @@ const limitedFileStore = (store, limits, now = Date.now) => {
27
35
  };
28
36
  const usage = async () => {
29
37
  if (counted && now() - counted.at < 3e5) return counted;
30
- const files = (await store.list("")).filter((object) => isFilePath(object.path));
31
38
  counted = {
32
- bytes: files.reduce((sum, object) => sum + object.size, 0),
33
- files: files.length,
39
+ ...await storeUsage(store),
34
40
  at: now()
35
41
  };
36
42
  return counted;
@@ -76,4 +82,4 @@ const limitedFileStore = (store, limits, now = Date.now) => {
76
82
  };
77
83
  };
78
84
  //#endregion
79
- export { limitedFileStore, recountMs };
85
+ export { limitedFileStore, recountMs, storeUsage };
@@ -1,6 +1,7 @@
1
1
  import { HttpProblem } from "../problem.js";
2
2
  import { parseDuration } from "./duration.js";
3
3
  import { limitedFileStore } from "./limits.js";
4
+ import { parseSize } from "@protobase/schema/internal";
4
5
  //#region src/files/local-files.ts
5
6
  const node = async () => ({
6
7
  fs: await import("node:fs/promises"),
@@ -99,7 +100,8 @@ const localFiles = ({ dir, public: isPublic = false, publicUrl, retention, maxBy
99
100
  }),
100
101
  public: isPublic || publicUrl !== void 0,
101
102
  ...publicUrl !== void 0 && { publicUrl: publicUrl.replace(/\/$/, "") },
102
- ...retention !== void 0 && { retention: parseDuration(retention, "retention") }
103
+ ...retention !== void 0 && { retention: parseDuration(retention, "retention") },
104
+ ...maxBytes !== void 0 && { maxBytes: parseSize(maxBytes) }
103
105
  });
104
106
  //#endregion
105
107
  export { localFileStore, localFiles };
@@ -22,5 +22,14 @@ export type FilesOptions = {
22
22
  * public files from: `['images.example.com']`. Images from any other host load with `referrerPolicy="no-referrer"`.
23
23
  */
24
24
  trustedHosts?: string[];
25
+ /**
26
+ * Where `supabase.storage` keeps the bytes of its objects, which the app serves at `/storage/v1` when it has an API key
27
+ * (see `apiKeys`): `provider` names one of the providers, default `private`, and `maxFileSize` caps every upload, as
28
+ * `50 MB` (the default) or a number of bytes; a bucket's own limit can only be lower. `false` serves no `/storage/v1`.
29
+ */
30
+ storage?: {
31
+ provider?: string;
32
+ maxFileSize?: number | string;
33
+ } | false;
25
34
  };
26
35
  //#endregion
@@ -1,21 +1,29 @@
1
1
  import { fileColumns, runCleanup } from "./cleanup-runner.js";
2
2
  import { storageCleanupSchedule } from "./cleanup-schedule.js";
3
3
  import { resolveProviders, retentionOf } from "./providers.js";
4
+ import { storageProviderName } from "./storage-provider.js";
4
5
  import { createSigner } from "./signer.js";
5
6
  import { trustedImageHosts } from "./trusted-hosts.js";
7
+ import { parseSize } from "@protobase/schema/internal";
6
8
  //#region src/files/runtime.ts
7
9
  /** How long an upload ticket stays usable, so a form left open still saves. */
8
10
  const ticketSeconds = 86400;
9
11
  const imageFields = (models) => models.flatMap((model) => Object.values(model.fields).flatMap(({ name, file }) => file && (file.variants || file.original || file.placeholder !== void 0) ? [`${model.name}.${name}`] : []));
10
12
  /**
11
- * The runtime, when a resource has a file field or the config names providers. It fails at startup when a field's
13
+ * The runtime, when a resource has a file field, the config names providers or the app serves `/storage/v1`. It fails at startup when a field's
12
14
  * provider is not configured, nothing signs uploads, or a field makes image variants or placeholders without sharp.
13
15
  */
14
- const createFilesRuntime = ({ options = {}, env, models, systemPath }) => {
15
- const used = [...new Set(models.flatMap((model) => Object.values(model.fields).flatMap((field) => field.file ? [field.file.provider] : [])))];
16
+ const createFilesRuntime = ({ options = {}, env, models, systemPath, storage: servesStorage = false }) => {
17
+ const storageName = servesStorage ? storageProviderName(options) : void 0;
18
+ const storage = storageName && options.storage !== false ? {
19
+ provider: storageName,
20
+ maxBytes: parseSize(options.storage?.maxFileSize ?? "50 MB")
21
+ } : void 0;
22
+ const fieldProviders = models.flatMap((model) => Object.values(model.fields).flatMap((field) => field.file ? [field.file.provider] : []));
23
+ const used = [.../* @__PURE__ */ new Set([...fieldProviders, ...storage ? [storage.provider] : []])];
16
24
  if (used.length === 0 && !options.providers) return void 0;
17
25
  const secret = options.secret ?? env.PROTOBASE_FILES_SECRET ?? env.BETTER_AUTH_SECRET;
18
- if (!secret) throw new Error("File fields need a secret to sign uploads: set BETTER_AUTH_SECRET, PROTOBASE_FILES_SECRET or files.secret");
26
+ if (!secret) throw new Error("File fields and /storage/v1 need a secret to sign uploads: set BETTER_AUTH_SECRET, PROTOBASE_FILES_SECRET or files.secret");
19
27
  const images = imageFields(models);
20
28
  if (images.length > 0 && !options.sharp) throw new Error(`${images.join(", ")} make image variants or placeholders, which need sharp: add it to the project (bun add sharp), then import sharp from 'sharp' in the config and pass files: { sharp }`);
21
29
  const providers = resolveProviders(options, env, used);
@@ -28,7 +36,8 @@ const createFilesRuntime = ({ options = {}, env, models, systemPath }) => {
28
36
  path: `${systemPath}/files`,
29
37
  columns: fileColumns(models),
30
38
  trustedHosts: trustedImageHosts(providers, options.trustedHosts),
31
- ...options.sharp && { sharp: options.sharp }
39
+ ...options.sharp && { sharp: options.sharp },
40
+ ...storage && { storage }
32
41
  };
33
42
  };
34
43
  /** Runs the scheduled deletes that are due (see `runCleanup`). */
@@ -1,6 +1,7 @@
1
1
  import { contentDisposition, servedType } from "./media-types.js";
2
2
  import { parseDuration } from "./duration.js";
3
3
  import { limitedFileStore } from "./limits.js";
4
+ import { parseSize } from "@protobase/schema/internal";
4
5
  //#region src/files/s3-files.ts
5
6
  const codeOf = (error) => error instanceof Error ? error.code : void 0;
6
7
  /** A store in a bucket, under `prefix`. Downloads get presigned links, so their bytes never pass through the app. */
@@ -106,7 +107,8 @@ const s3Files = (options) => {
106
107
  }),
107
108
  public: isPublic || publicUrl !== void 0,
108
109
  ...publicUrl !== void 0 && { publicUrl: `${publicUrl.replace(/\/$/, "")}${folder === "" ? "" : `/${folder}`}` },
109
- ...retention !== void 0 && { retention: parseDuration(retention, "retention") }
110
+ ...retention !== void 0 && { retention: parseDuration(retention, "retention") },
111
+ ...maxBytes !== void 0 && { maxBytes: parseSize(maxBytes) }
110
112
  };
111
113
  };
112
114
  //#endregion
@@ -0,0 +1,12 @@
1
+ import { FileProvider } from "./store.js";
2
+ import { FilesOptions } from "./options.js";
3
+ //#region src/files/storage-provider.d.ts
4
+ /**
5
+ * The provider `/storage/v1` keeps object bytes in, as the app resolves it from the config and the environment, for a
6
+ * command that writes them, such as `protobase import supabase`. Fails when `files.storage` is `false`.
7
+ */
8
+ export declare const storageProvider: (options: FilesOptions, env: Record<string, string | undefined>) => {
9
+ name: string;
10
+ provider: FileProvider;
11
+ };
12
+ //#endregion
@@ -0,0 +1,18 @@
1
+ import { resolveProviders } from "./providers.js";
2
+ //#region src/files/storage-provider.ts
3
+ /** The provider `/storage/v1` keeps object bytes in: `files.storage.provider`, default `private`. */
4
+ const storageProviderName = (options) => options.storage === false ? void 0 : options.storage?.provider ?? "private";
5
+ /**
6
+ * The provider `/storage/v1` keeps object bytes in, as the app resolves it from the config and the environment, for a
7
+ * command that writes them, such as `protobase import supabase`. Fails when `files.storage` is `false`.
8
+ */
9
+ const storageProvider = (options, env) => {
10
+ const name = storageProviderName(options);
11
+ if (!name) throw new Error("files.storage is false in protobase.config.ts, so the app keeps no Storage objects");
12
+ return {
13
+ name,
14
+ provider: resolveProviders(options, env, [name])[name]
15
+ };
16
+ };
17
+ //#endregion
18
+ export { storageProvider, storageProviderName };
@@ -42,5 +42,7 @@ export type FileProvider = {
42
42
  publicUrl?: string;
43
43
  /** Milliseconds between a file leaving its row and its bytes being deleted; default `files.retention` (0 when public). */
44
44
  retention?: number;
45
+ /** The most bytes its store takes (see `limitedFileStore`); unset for no limit. */
46
+ maxBytes?: number;
45
47
  };
46
48
  //#endregion
package/dist/index.d.ts CHANGED
@@ -86,10 +86,12 @@ import { startErrorReporting } from "./error-reporting/error-reporter.js";
86
86
  import { FunctionEnv, PublicFunctionEnv, functionApp } from "./functions/function-app.js";
87
87
  import { SignatureOptions, StandardWebhookOptions, verifySignature, verifyStandardWebhook } from "./functions/verify-signature.js";
88
88
  import { requireEnv } from "./functions/require-env.js";
89
- import { FileLimits, limitedFileStore } from "./files/limits.js";
89
+ import { FileLimits, limitedFileStore, storeUsage } from "./files/limits.js";
90
90
  import { LocalFilesOptions, localFileStore, localFiles } from "./files/local-files.js";
91
91
  import { StoreFileInput, storeFile } from "./files/store-file.js";
92
92
  import { S3Bucket, S3FilesOptions, S3ListPage, S3Object, s3FileStore, s3Files } from "./files/s3-files.js";
93
+ import { storageProvider } from "./files/storage-provider.js";
94
+ import { objectKey } from "./storage/object-key.js";
93
95
  import { FileCleanupInput, createFileCleanup } from "./files/file-cleanup.js";
94
96
  import { FileObject, FileVariantObject } from "./files/present.js";
95
97
  import { FileRef, formatUri, variantPath } from "./files/uri.js";
@@ -100,4 +102,4 @@ import { isSchedule } from "./jobs/schedule.js";
100
102
  import { DrainResult, JobsRuntime, StartJobsOptions, startJobs } from "./jobs/runtime.js";
101
103
  import { jobsDrainApp } from "./jobs/drain-route.js";
102
104
  import { checkJobsSchema, jobsSchema, jobsSchemaMigration } from "./jobs/schema.js";
103
- export { type Admin, type AdminAuth, type AdminOptions, type ApiFunction, type ApiKeys, type ApiKeysOptions, type AppIcons, type AppIdentity, type ApprovalAnswer, type ApprovalRequest, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type Authenticator, type BetterAuthAuthenticatorOptions, type Caller, type CallerOptions, type CapturedRow, type ChatMessage, type ChatTextPart, type CheckAiInput, type CleanupEntry, type CleanupReport, type CleanupSchedule, type CoalescedChange, type CommentItem, type CommentUsers, type Completion, type CompletionOptions, type Conversation, type ConversationRecord, type ConversationStore, type ConversationsOptions, type CorsOptions, type CorsOrigins, type CreateAdminInput, type CreateAuthOptions, type DrainResult, type EmailComponent, type EmailOptions, type EmailTemplate, type EnqueueOptions, type ErrorReporter, type ErrorReportingOptions, type ErrorReportingSettings, type EventData, type EventsOptions, FatalJobError, type FileCleanupInput, type FileLimits, type FileObject, type FileProvider, type FileRef, type FileStore, type FileVariantObject, type FilesOptions, type FunctionApp, type FunctionCaller, type FunctionContext, type FunctionEnv, type FunctionHandler, type FunctionOptions, type FunctionRecords, type FunctionSource, type HistoryOptions, HttpProblem, type IssueTokenOptions, type JobContext, type JobDefinition, type JobOptions, type JobsRuntime, type JwtOptions, type JwtVerifyOptions, type LinkedAccount, type ListParams, type LocalFilesOptions, type MailMessage, type Mailer, type MapLayer, type MapOptions, type MapSettings, type MemberInput, MissingCredentials, type MobileConfig, ModelError, type ModelSettings, type NewOrganization, type NewUser, type OperatorProvider, type OrganizationRole, type OrganizationsOptions, type PipelineHook, type PlatformSignIn, type PluginOption, type PluginRef, type PngIcon, type ProjectConfig, type ProtectedFunctionOptions, type PublicFunctionContext, type PublicFunctionEnv, type PublicFunctionHandler, type PublicFunctionOptions, type QueryOptions, type QueryResult, type ReasoningDetail, type ReasoningEffort, type RecordKeyInput, type RecordPage, type RenderedEmail, type ResourceChange, type ResourceSource, type RoleDefinition, type RolesInput, type RunContext, type RuntimeOptions, type S3Bucket, type S3FilesOptions, type S3ListPage, type S3Object, type ScanGuardMode, type ScheduledDelete, type Session, type SessionOrganization, type Sharp, type SignInPolicy, type SignInRule, type SignatureOptions, type SourceOption, type SourceRef, type StaffAccessRule, type StaffSignIn, type StandardSchema, type StandardWebhookOptions, type StartJobsOptions, type StoreFileInput, type StoredMember, type StoredObject, type StoredOrganization, type StoredUser, type SubscriptionDefinition, type SubscriptionEvent, type SubscriptionOptions, type TenantValue, type ToolCall, type ToolContext, type ToolRecord, type ToolRecords, type ToolSpec, type TrustedApp, type TurnOptions, type User, type UserCreatedHook, type ViewSource, type WebhooksOptions, type WidgetOptions, type WriteEvent, type WriteOperation, addAppRoleToOwners, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, badRequest, beforeSignInPluginsVariable, betterAuthAuthenticator, betterAuthTokens, changesMigration, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultAppName, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteUser, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, handleDroppedConnections, hasUsers, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listOrganizations, listUsers, localFileStore, localFiles, mapSettings, mapStyleUrl, mapStyleVariable, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, parseColumns, parseRoles, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, s3Files, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, storeFile, streamChatCompletion, subscription, thumbHash, unauthorized, variantPath, verifySignature, verifyStandardWebhook, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps };
105
+ export { type Admin, type AdminAuth, type AdminOptions, type ApiFunction, type ApiKeys, type ApiKeysOptions, type AppIcons, type AppIdentity, type ApprovalAnswer, type ApprovalRequest, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type Authenticator, type BetterAuthAuthenticatorOptions, type Caller, type CallerOptions, type CapturedRow, type ChatMessage, type ChatTextPart, type CheckAiInput, type CleanupEntry, type CleanupReport, type CleanupSchedule, type CoalescedChange, type CommentItem, type CommentUsers, type Completion, type CompletionOptions, type Conversation, type ConversationRecord, type ConversationStore, type ConversationsOptions, type CorsOptions, type CorsOrigins, type CreateAdminInput, type CreateAuthOptions, type DrainResult, type EmailComponent, type EmailOptions, type EmailTemplate, type EnqueueOptions, type ErrorReporter, type ErrorReportingOptions, type ErrorReportingSettings, type EventData, type EventsOptions, FatalJobError, type FileCleanupInput, type FileLimits, type FileObject, type FileProvider, type FileRef, type FileStore, type FileVariantObject, type FilesOptions, type FunctionApp, type FunctionCaller, type FunctionContext, type FunctionEnv, type FunctionHandler, type FunctionOptions, type FunctionRecords, type FunctionSource, type HistoryOptions, HttpProblem, type IssueTokenOptions, type JobContext, type JobDefinition, type JobOptions, type JobsRuntime, type JwtOptions, type JwtVerifyOptions, type LinkedAccount, type ListParams, type LocalFilesOptions, type MailMessage, type Mailer, type MapLayer, type MapOptions, type MapSettings, type MemberInput, MissingCredentials, type MobileConfig, ModelError, type ModelSettings, type NewOrganization, type NewUser, type OperatorProvider, type OrganizationRole, type OrganizationsOptions, type PipelineHook, type PlatformSignIn, type PluginOption, type PluginRef, type PngIcon, type ProjectConfig, type ProtectedFunctionOptions, type PublicFunctionContext, type PublicFunctionEnv, type PublicFunctionHandler, type PublicFunctionOptions, type QueryOptions, type QueryResult, type ReasoningDetail, type ReasoningEffort, type RecordKeyInput, type RecordPage, type RenderedEmail, type ResourceChange, type ResourceSource, type RoleDefinition, type RolesInput, type RunContext, type RuntimeOptions, type S3Bucket, type S3FilesOptions, type S3ListPage, type S3Object, type ScanGuardMode, type ScheduledDelete, type Session, type SessionOrganization, type Sharp, type SignInPolicy, type SignInRule, type SignatureOptions, type SourceOption, type SourceRef, type StaffAccessRule, type StaffSignIn, type StandardSchema, type StandardWebhookOptions, type StartJobsOptions, type StoreFileInput, type StoredMember, type StoredObject, type StoredOrganization, type StoredUser, type SubscriptionDefinition, type SubscriptionEvent, type SubscriptionOptions, type TenantValue, type ToolCall, type ToolContext, type ToolRecord, type ToolRecords, type ToolSpec, type TrustedApp, type TurnOptions, type User, type UserCreatedHook, type ViewSource, type WebhooksOptions, type WidgetOptions, type WriteEvent, type WriteOperation, addAppRoleToOwners, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, badRequest, beforeSignInPluginsVariable, betterAuthAuthenticator, betterAuthTokens, changesMigration, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultAppName, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteUser, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, handleDroppedConnections, hasUsers, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listOrganizations, listUsers, localFileStore, localFiles, mapSettings, mapStyleUrl, mapStyleVariable, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, objectKey, parseColumns, parseRoles, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, s3Files, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, thumbHash, unauthorized, variantPath, verifySignature, verifyStandardWebhook, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps };
package/dist/index.js CHANGED
@@ -21,6 +21,7 @@ import { webauthnPath } from "./better-auth/origins.js";
21
21
  import { apiKeySettings, apiKeyVariables } from "./api-keys/api-key-settings.js";
22
22
  import { apiKeyCaller } from "./api-keys/resolve-caller.js";
23
23
  import { jwtAuthenticator, jwtVerifier } from "./auth/jwt.js";
24
+ import { objectKey } from "./storage/object-key.js";
24
25
  import { defaultAppName } from "./app-name.js";
25
26
  import { defaultSignInPolicy, signInPolicyMethods } from "./better-auth/sign-in-policy.js";
26
27
  import { parseRoles } from "./better-auth/parse-roles.js";
@@ -40,9 +41,10 @@ import { newWebhookSecret } from "./webhooks/standard-webhooks.js";
40
41
  import { subscription } from "./webhooks/subscription.js";
41
42
  import { errorReportingSettings, errorReportingVariable } from "./error-reporting/error-reporting-settings.js";
42
43
  import { storageCleanupSchedule } from "./files/cleanup-schedule.js";
43
- import { limitedFileStore } from "./files/limits.js";
44
+ import { limitedFileStore, storeUsage } from "./files/limits.js";
44
45
  import { localFileStore, localFiles } from "./files/local-files.js";
45
46
  import { s3FileStore, s3Files } from "./files/s3-files.js";
47
+ import { storageProvider } from "./files/storage-provider.js";
46
48
  import { isBackgroundWork, runInBackground } from "./background-work.js";
47
49
  import { mapSettings, mapStyleUrl, mapStyleVariable } from "./maps/map-settings.js";
48
50
  import { beforeSignInPluginsVariable, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable } from "./plugins/plugin-settings.js";
@@ -75,4 +77,4 @@ import { aspectRatio, fileSize, fileType, imageSize, processor } from "./files/p
75
77
  import { defineEmail } from "./mail/define-email.js";
76
78
  import { startJobs } from "./jobs/runtime.js";
77
79
  import { jobsDrainApp } from "./jobs/drain-route.js";
78
- export { FatalJobError, HttpProblem, MissingCredentials, ModelError, addAppRoleToOwners, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, badRequest, beforeSignInPluginsVariable, betterAuthAuthenticator, betterAuthTokens, changesMigration, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultAppName, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteUser, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, handleDroppedConnections, hasUsers, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listOrganizations, listUsers, localFileStore, localFiles, mapSettings, mapStyleUrl, mapStyleVariable, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, parseColumns, parseRoles, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, s3Files, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, storeFile, streamChatCompletion, subscription, thumbHash, unauthorized, variantPath, verifySignature, verifyStandardWebhook, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps };
80
+ export { FatalJobError, HttpProblem, MissingCredentials, ModelError, addAppRoleToOwners, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, badRequest, beforeSignInPluginsVariable, betterAuthAuthenticator, betterAuthTokens, changesMigration, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultAppName, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteUser, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, handleDroppedConnections, hasUsers, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listOrganizations, listUsers, localFileStore, localFiles, mapSettings, mapStyleUrl, mapStyleVariable, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, objectKey, parseColumns, parseRoles, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, s3Files, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, thumbHash, unauthorized, variantPath, verifySignature, verifyStandardWebhook, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps };
@@ -0,0 +1,78 @@
1
+ import { isDatabaseError } from "../error-handler.js";
2
+ import { deleteBucket, findBucket, insertBucket, listBuckets, updateBucket } from "./bucket-sql.js";
3
+ import { bucketNotEmpty, bucketNotFound, invalidRequest } from "./storage-error.js";
4
+ import { checkedBucketId, objectKey } from "./object-key.js";
5
+ import { deleteBucketObjects } from "./object-sql.js";
6
+ import { asCaller } from "./storage-db.js";
7
+ import { jsonBody } from "./storage-context.js";
8
+ import { ownerOf } from "./write-object.js";
9
+ import { Hono } from "hono";
10
+ import { parseSize } from "@protobase/schema/internal";
11
+ //#region src/storage/bucket-routes.ts
12
+ const bucketJson = (bucket) => ({
13
+ ...bucket,
14
+ type: "STANDARD"
15
+ });
16
+ const sizeLimit = (value) => {
17
+ if (value === null) return null;
18
+ if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) return value;
19
+ if (typeof value === "string" && /^\s*\d+(\.\d+)?\s*(b|kb|mb|gb)?\s*$/i.test(value)) return parseSize(value);
20
+ throw invalidRequest("file_size_limit must be a number of bytes or a size such as 1MB");
21
+ };
22
+ const bucketWrite = (body) => {
23
+ const types = body.allowed_mime_types;
24
+ if (types !== void 0 && types !== null && !(Array.isArray(types) && types.every((type) => typeof type === "string"))) throw invalidRequest("allowed_mime_types must be a list of media types");
25
+ if (body.public !== void 0 && typeof body.public !== "boolean") throw invalidRequest("public must be true or false");
26
+ return {
27
+ ...body.public !== void 0 && { public: body.public },
28
+ ...body.file_size_limit !== void 0 && { file_size_limit: sizeLimit(body.file_size_limit) },
29
+ ...types !== void 0 && { allowed_mime_types: types }
30
+ };
31
+ };
32
+ /** `supabase.storage`'s bucket calls, each as the caller, so the restored `storage.buckets` policies decide. */
33
+ const bucketRoutes = (deps) => {
34
+ const app = new Hono();
35
+ app.get("/bucket", async (c) => c.json((await asCaller(deps, await deps.caller(c.req.raw), true, listBuckets)).map(bucketJson)));
36
+ app.get("/bucket/:id", async (c) => {
37
+ const id = checkedBucketId(c.req.param("id"));
38
+ const bucket = await asCaller(deps, await deps.caller(c.req.raw), true, (trx) => findBucket(trx, id));
39
+ if (!bucket) throw bucketNotFound();
40
+ return c.json(bucketJson(bucket));
41
+ });
42
+ app.post("/bucket", async (c) => {
43
+ const body = await jsonBody(c);
44
+ const name = checkedBucketId(typeof body.name === "string" ? body.name : "");
45
+ const id = typeof body.id === "string" ? checkedBucketId(body.id) : name;
46
+ const caller = await deps.caller(c.req.raw);
47
+ await asCaller(deps, caller, false, (trx) => insertBucket(trx, {
48
+ id,
49
+ name,
50
+ owner: ownerOf(caller),
51
+ ...bucketWrite(body)
52
+ }));
53
+ return c.json({ name: id });
54
+ });
55
+ app.put("/bucket/:id", async (c) => {
56
+ const id = checkedBucketId(c.req.param("id"));
57
+ const write = bucketWrite(await jsonBody(c));
58
+ if (!await asCaller(deps, await deps.caller(c.req.raw), false, (trx) => updateBucket(trx, id, write))) throw bucketNotFound();
59
+ return c.json({ message: "Successfully updated" });
60
+ });
61
+ app.post("/bucket/:id/empty", async (c) => {
62
+ const id = checkedBucketId(c.req.param("id"));
63
+ const deleted = await asCaller(deps, await deps.caller(c.req.raw), false, (trx) => deleteBucketObjects(trx, id));
64
+ for (const row of deleted) await deps.store.delete(await objectKey(row.bucket_id, row.name, row.version));
65
+ return c.json({ message: "Successfully emptied" });
66
+ });
67
+ app.delete("/bucket/:id", async (c) => {
68
+ const id = checkedBucketId(c.req.param("id"));
69
+ if (!await asCaller(deps, await deps.caller(c.req.raw), false, (trx) => deleteBucket(trx, id)).catch((error) => {
70
+ if (isDatabaseError(error) && error.code === "23503") throw bucketNotEmpty();
71
+ throw error;
72
+ })) throw bucketNotFound();
73
+ return c.json({ message: "Successfully deleted" });
74
+ });
75
+ return app;
76
+ };
77
+ //#endregion
78
+ export { bucketRoutes };
@@ -0,0 +1,29 @@
1
+ import { sql } from "kysely";
2
+ //#region src/storage/bucket-sql.ts
3
+ const columns = sql`id, name, owner, owner_id, public, file_size_limit::float8 as file_size_limit, allowed_mime_types, created_at, updated_at`;
4
+ /** The bucket `id`, as the transaction's role may see it. */
5
+ const findBucket = async (trx, id) => {
6
+ const { rows } = await sql`select ${columns} from storage.buckets where id = ${id}`.execute(trx);
7
+ return rows[0];
8
+ };
9
+ /** Every bucket the transaction's role may see, by name. */
10
+ const listBuckets = async (trx) => (await sql`select ${columns} from storage.buckets order by name collate "C"`.execute(trx)).rows;
11
+ /** Adds a bucket; RLS refuses it with 42501, a taken id or name with 23505. */
12
+ const insertBucket = async (trx, bucket) => {
13
+ await sql`
14
+ insert into storage.buckets (id, name, owner_id, public, file_size_limit, allowed_mime_types)
15
+ values (${bucket.id}, ${bucket.name}, ${bucket.owner}, ${bucket.public ?? false}, ${bucket.file_size_limit ?? null}, ${bucket.allowed_mime_types ?? null})`.execute(trx);
16
+ };
17
+ /** Changes the bucket `id`; whether the transaction's role could. */
18
+ const updateBucket = async (trx, id, write) => {
19
+ const sets = Object.entries(write).filter(([, value]) => value !== void 0).map(([column, value]) => sql`${sql.ref(column)} = ${value}`);
20
+ if (sets.length === 0) return await findBucket(trx, id) !== void 0;
21
+ return (await sql`update storage.buckets set ${sql.join(sets)} where id = ${id}`.execute(trx)).numAffectedRows === 1n;
22
+ };
23
+ /** Deletes the bucket `id`; whether the transaction's role could. A bucket that still has objects fails with 23503. */
24
+ const deleteBucket = async (trx, id) => {
25
+ await sql`select set_config('storage.allow_delete_query', 'true', true)`.execute(trx);
26
+ return (await sql`delete from storage.buckets where id = ${id}`.execute(trx)).numAffectedRows === 1n;
27
+ };
28
+ //#endregion
29
+ export { deleteBucket, findBucket, insertBucket, listBuckets, updateBucket };
@@ -0,0 +1,29 @@
1
+ import { sql } from "kysely";
2
+ //#region src/storage/list-sql.ts
3
+ const likeEscaped = (text) => text.replace(/[\\%_]/g, (char) => `\\${char}`);
4
+ /**
5
+ * One level of `bucket` below the folder `prefix` (`''` for the top, else ending in `/`), as the transaction's role may
6
+ * see it: the objects directly in it and the folders that hold objects deeper down, whose names start with `search`
7
+ * (case-insensitive). Folders sort with the objects by name; by any other column they come first, as in Supabase.
8
+ */
9
+ const listFolder = async (trx, bucket, query) => {
10
+ const start = [...query.prefix].length + 1;
11
+ const order = sql.raw(query.order === "desc" ? "desc" : "asc");
12
+ const { rows } = await sql`
13
+ with level as (
14
+ select split_part(substr(name, ${start}), '/', 1) as entry, strpos(substr(name, ${start}), '/') > 0 as folder, id, updated_at, created_at, last_accessed_at, metadata
15
+ from storage.objects
16
+ where bucket_id = ${bucket} and name like ${`${likeEscaped(query.prefix)}%`} and substr(name, ${start}) ilike ${`${likeEscaped(query.search)}%`}
17
+ ), entries as (
18
+ select distinct on (entry) entry as name, folder, case when folder then null else id end as id,
19
+ case when folder then null else updated_at end as updated_at, case when folder then null else created_at end as created_at,
20
+ case when folder then null else last_accessed_at end as last_accessed_at, case when folder then null else metadata end as metadata
21
+ from level order by entry, folder desc
22
+ )
23
+ select name, id, updated_at, created_at, last_accessed_at, metadata from entries
24
+ order by ${query.column === "name" ? sql`name collate "C" ${order}` : sql`folder desc, ${sql.ref(query.column)} ${order} nulls last, name collate "C"`}
25
+ limit ${query.limit} offset ${query.offset}`.execute(trx);
26
+ return rows;
27
+ };
28
+ //#endregion
29
+ export { listFolder };
@@ -0,0 +1,63 @@
1
+ import { accessDenied, alreadyExists, objectNotFound } from "./storage-error.js";
2
+ import { objectKey } from "./object-key.js";
3
+ import { findObject, renameObject } from "./object-sql.js";
4
+ import { asApp, asCaller, becomeCaller } from "./storage-db.js";
5
+ import { ownerOf, writeObject } from "./write-object.js";
6
+ //#region src/storage/move-copy.ts
7
+ /** The object as the caller may see it, else 404 `NoSuchKey`. */
8
+ const visibleObject = async (deps, caller, at) => {
9
+ const row = await asCaller(deps, caller, true, (trx) => findObject(trx, at.bucket, at.name));
10
+ if (!row) throw objectNotFound();
11
+ return row;
12
+ };
13
+ const copyBytes = async (store, row, key) => {
14
+ const body = await store.read(await objectKey(row.bucket_id, row.name, row.version));
15
+ if (!body) throw objectNotFound();
16
+ return store.put(key, body);
17
+ };
18
+ /**
19
+ * Copies an object the caller may see to `to`, with its type, cache control and metadata, as a new object the caller
20
+ * must be allowed to write there (see `writeObject`); `upsert` lets it replace one.
21
+ */
22
+ const copyObject = async (deps, caller, from, to, upsert) => {
23
+ const source = await visibleObject(deps, caller, from);
24
+ return writeObject(deps, {
25
+ ...to,
26
+ caller,
27
+ owner: ownerOf(caller),
28
+ upsert
29
+ }, {
30
+ type: source.metadata?.mimetype ?? "application/octet-stream",
31
+ cacheControl: source.metadata?.cacheControl ?? "max-age=3600",
32
+ userMetadata: source.user_metadata,
33
+ store: (key) => copyBytes(deps.store, source, key)
34
+ });
35
+ };
36
+ /**
37
+ * Moves an object the caller may see to `to`: its bytes are copied under a new version, then its row is renamed as the
38
+ * caller, so the update policies decide on the old and the new name. When that fails, the copy is deleted; once it
39
+ * commits, the old bytes are.
40
+ */
41
+ const moveObject = async (deps, caller, from, to) => {
42
+ const source = await visibleObject(deps, caller, from);
43
+ const version = crypto.randomUUID();
44
+ const key = await objectKey(to.bucket, to.name, version);
45
+ await copyBytes(deps.store, source, key);
46
+ await asApp(deps, async (trx) => {
47
+ if (!await findObject(trx, from.bucket, from.name, true)) throw objectNotFound();
48
+ if (await findObject(trx, to.bucket, to.name, true)) throw alreadyExists();
49
+ await becomeCaller(trx, deps, caller);
50
+ if (!await renameObject(trx, from, {
51
+ ...to,
52
+ version,
53
+ owner: ownerOf(caller)
54
+ })) throw accessDenied();
55
+ }).catch(async (error) => {
56
+ await deps.store.delete(key);
57
+ throw error;
58
+ });
59
+ await deps.store.delete(await objectKey(source.bucket_id, source.name, source.version));
60
+ return source.id;
61
+ };
62
+ //#endregion
63
+ export { copyObject, moveObject, visibleObject };
@@ -0,0 +1,47 @@
1
+ //#region src/storage/multipart.ts
2
+ const encoder = new TextEncoder();
3
+ const decoder = new TextDecoder();
4
+ const indexOf = (haystack, needle, from) => {
5
+ for (let at = haystack.indexOf(needle[0], from); at !== -1 && at + needle.length <= haystack.length; at = haystack.indexOf(needle[0], at + 1)) {
6
+ let index = 1;
7
+ while (index < needle.length && haystack[at + index] === needle[index]) index += 1;
8
+ if (index === needle.length) return at;
9
+ }
10
+ return -1;
11
+ };
12
+ /** The boundary of a `multipart/form-data` Content-Type, else `undefined`. */
13
+ const boundaryOf = (contentType) => /;\s*boundary=(?:"([^"]+)"|([^;\s]+))/i.exec(contentType)?.slice(1).find(Boolean);
14
+ const headerValue = (headers, name) => new RegExp(`^${name}:\\s*(.*)$`, "im").exec(headers)?.[1]?.trim();
15
+ const parameter = (disposition, name) => new RegExp(`;\\s*${name}="([^"]*)"`, "i").exec(disposition)?.[1];
16
+ /**
17
+ * The parts of a whole `multipart/form-data` body, every one kept, also a file part with an empty field name, as
18
+ * storage-js sends a `Blob`: Bun's `formData()` drops it. Undefined when the body does not parse.
19
+ */
20
+ const multipartParts = (bytes, boundary) => {
21
+ const delimiter = encoder.encode(`--${boundary}`);
22
+ const next = encoder.encode(`\r\n--${boundary}`);
23
+ const parts = [];
24
+ let at = indexOf(bytes, delimiter, 0);
25
+ if (at === -1) return void 0;
26
+ at += delimiter.length;
27
+ while (decoder.decode(bytes.subarray(at, at + 2)) !== "--") {
28
+ if (decoder.decode(bytes.subarray(at, at + 2)) !== "\r\n") return void 0;
29
+ const headersEnd = indexOf(bytes, encoder.encode("\r\n\r\n"), at + 2);
30
+ const end = headersEnd === -1 ? -1 : indexOf(bytes, next, headersEnd + 4);
31
+ if (end === -1) return void 0;
32
+ const headers = decoder.decode(bytes.subarray(at + 2, headersEnd));
33
+ const disposition = headerValue(headers, "content-disposition") ?? "";
34
+ const filename = parameter(disposition, "filename");
35
+ const type = headerValue(headers, "content-type");
36
+ parts.push({
37
+ name: parameter(disposition, "name") ?? "",
38
+ ...filename !== void 0 && { filename },
39
+ ...type && { type },
40
+ body: bytes.subarray(headersEnd + 4, end)
41
+ });
42
+ at = end + next.length;
43
+ }
44
+ return parts;
45
+ };
46
+ //#endregion
47
+ export { boundaryOf, multipartParts };