@rebasepro/server 0.24.1-canary.gcb918fc → 0.25.0-canary.ge06257e

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.
Files changed (90) hide show
  1. package/dist/{GCSStorageController-fv6JAZ87.js → GCSStorageController-CAKa_EFW.js} +3 -3
  2. package/dist/{GCSStorageController-fv6JAZ87.js.map → GCSStorageController-CAKa_EFW.js.map} +1 -1
  3. package/dist/{S3StorageController-BNg-pXiY.js → S3StorageController-Kt-LJpf3.js} +3 -3
  4. package/dist/{S3StorageController-BNg-pXiY.js.map → S3StorageController-Kt-LJpf3.js.map} +1 -1
  5. package/dist/admin-roles-DI9RsgN3.js +79 -0
  6. package/dist/admin-roles-DI9RsgN3.js.map +1 -0
  7. package/dist/api/errors.d.ts +32 -0
  8. package/dist/api/live-schema-routes.d.ts +27 -3
  9. package/dist/api/types.d.ts +1 -0
  10. package/dist/auth/access.d.ts +11 -3
  11. package/dist/auth/adapter-middleware.d.ts +11 -0
  12. package/dist/auth/admin-roles.d.ts +18 -1
  13. package/dist/auth/jwt-crypto.d.ts +15 -0
  14. package/dist/auth/token-revocation.d.ts +18 -0
  15. package/dist/auth/verify-credential.d.ts +38 -15
  16. package/dist/{auth-GnONzteu.js → auth-xk37vrHO.js} +165 -83
  17. package/dist/auth-xk37vrHO.js.map +1 -0
  18. package/dist/backend/create-backend.d.ts +7 -3
  19. package/dist/backend/no-collections.d.ts +19 -0
  20. package/dist/backend/services.d.ts +9 -1
  21. package/dist/backend-DTAOsLQc.js.map +1 -1
  22. package/dist/{backup-BnvkSLHi.js → backup-CpaJWXSc.js} +7 -7
  23. package/dist/{backup-BnvkSLHi.js.map → backup-CpaJWXSc.js.map} +1 -1
  24. package/dist/boot/resource-adapters.d.ts +11 -14
  25. package/dist/boot/sources.d.ts +15 -0
  26. package/dist/{contract-routes-Ey6ftnKH.js → contract-routes-DiFwiLg2.js} +2 -2
  27. package/dist/{contract-routes-Ey6ftnKH.js.map → contract-routes-DiFwiLg2.js.map} +1 -1
  28. package/dist/{cron-loader-B-u0snqq.js → cron-loader-NHqRfKF0.js} +2 -2
  29. package/dist/{cron-loader-B-u0snqq.js.map → cron-loader-NHqRfKF0.js.map} +1 -1
  30. package/dist/{cron-routes-OD6BstUj.js → cron-routes-CPIhdasM.js} +5 -5
  31. package/dist/{cron-routes-OD6BstUj.js.map → cron-routes-CPIhdasM.js.map} +1 -1
  32. package/dist/{cron-scheduler-idtLPoN-.js → cron-scheduler-D1DT0nw5.js} +2 -2
  33. package/dist/{cron-scheduler-idtLPoN-.js.map → cron-scheduler-D1DT0nw5.js.map} +1 -1
  34. package/dist/{cron-store-CZ30ZBi-.js → cron-store-DEpVstUU.js} +3 -3
  35. package/dist/{cron-store-CZ30ZBi-.js.map → cron-store-DEpVstUU.js.map} +1 -1
  36. package/dist/{ddl-bootstrap-BaqMSa4Y.js → ddl-bootstrap-BNqi2NZe.js} +2 -2
  37. package/dist/{ddl-bootstrap-BaqMSa4Y.js.map → ddl-bootstrap-BNqi2NZe.js.map} +1 -1
  38. package/dist/env.d.ts +52 -0
  39. package/dist/{errors-D6_y86c5.js → errors-CHSJYpoy.js} +170 -10
  40. package/dist/errors-CHSJYpoy.js.map +1 -0
  41. package/dist/{function-loader-iKhOZatS.js → function-loader-DbCSoNJj.js} +2 -2
  42. package/dist/{function-loader-iKhOZatS.js.map → function-loader-DbCSoNJj.js.map} +1 -1
  43. package/dist/{function-routes-j9TwpvQe.js → function-routes-C0emkHtX.js} +3 -22
  44. package/dist/function-routes-C0emkHtX.js.map +1 -0
  45. package/dist/functions/guards.d.ts +3 -1
  46. package/dist/functions/index.js +81 -4
  47. package/dist/functions/index.js.map +1 -1
  48. package/dist/{history-recorder-BfWRwmPy.js → history-recorder-RZ1Ow4YY.js} +2 -2
  49. package/dist/{history-recorder-BfWRwmPy.js.map → history-recorder-RZ1Ow4YY.js.map} +1 -1
  50. package/dist/index.d.ts +1 -1
  51. package/dist/index.es.js +462 -207
  52. package/dist/index.es.js.map +1 -1
  53. package/dist/init/docs.d.ts +8 -0
  54. package/dist/{jobs-BCbflZj2.js → jobs-8G-_agld.js} +3 -3
  55. package/dist/{jobs-BCbflZj2.js.map → jobs-8G-_agld.js.map} +1 -1
  56. package/dist/{jwt-BlvuKoR6.js → jwt-YhciR93H.js} +28 -3
  57. package/dist/{jwt-BlvuKoR6.js.map → jwt-YhciR93H.js.map} +1 -1
  58. package/dist/{logger-D-S-hO5e.js → logger-Bt32sHFw.js} +17 -4
  59. package/dist/logger-Bt32sHFw.js.map +1 -0
  60. package/dist/{logs-routes-DAdv37GI.js → logs-routes-BM2cDory.js} +3 -3
  61. package/dist/{logs-routes-DAdv37GI.js.map → logs-routes-BM2cDory.js.map} +1 -1
  62. package/dist/{offline-QNQgsPOq-B2yGeXkn.js → offline-BrqCRgih-BtDB1ecy.js} +3 -3
  63. package/dist/{offline-QNQgsPOq-B2yGeXkn.js.map → offline-BrqCRgih-BtDB1ecy.js.map} +1 -1
  64. package/dist/{offline-store-Cm5sAX81-CALiO4Gn.js → offline-store-BhYXViha-BW5j1We_.js} +8 -3
  65. package/dist/offline-store-BhYXViha-BW5j1We_.js.map +1 -0
  66. package/dist/{openapi-generator-D9zUQmEp.js → openapi-generator-IKwDAQGO.js} +2 -2
  67. package/dist/{openapi-generator-D9zUQmEp.js.map → openapi-generator-IKwDAQGO.js.map} +1 -1
  68. package/dist/{proxy-CkTlMGmh.js → proxy-BEKbsil6.js} +2 -2
  69. package/dist/{proxy-CkTlMGmh.js.map → proxy-BEKbsil6.js.map} +1 -1
  70. package/dist/{query-parser-4WkMoA_K.js → query-parser-BnhugNwZ.js} +3 -3
  71. package/dist/{query-parser-4WkMoA_K.js.map → query-parser-BnhugNwZ.js.map} +1 -1
  72. package/dist/{request-timeout-Dnl2_OBc.js → request-timeout-LiGpCCP1.js} +3 -3
  73. package/dist/{request-timeout-Dnl2_OBc.js.map → request-timeout-LiGpCCP1.js.map} +1 -1
  74. package/dist/{schema-editor-routes-C11Tumyd.js → schema-editor-routes-zjVKreVQ.js} +2 -2
  75. package/dist/{schema-editor-routes-C11Tumyd.js.map → schema-editor-routes-zjVKreVQ.js.map} +1 -1
  76. package/dist/{soft-delete-params-DfV3vVN5.js → soft-delete-params-uHxJoETb.js} +2 -2
  77. package/dist/{soft-delete-params-DfV3vVN5.js.map → soft-delete-params-uHxJoETb.js.map} +1 -1
  78. package/dist/{sql-export-routes-ClRwzIsC.js → sql-export-routes-D86u7N9i.js} +3 -3
  79. package/dist/{sql-export-routes-ClRwzIsC.js.map → sql-export-routes-D86u7N9i.js.map} +1 -1
  80. package/dist/src-D5Y3N-S4.js.map +1 -1
  81. package/dist/storage/image-transform.d.ts +10 -0
  82. package/dist/{upload-validation-Dd-fnsbA.js → upload-validation-pXcDy_EQ.js} +2 -2
  83. package/dist/{upload-validation-Dd-fnsbA.js.map → upload-validation-pXcDy_EQ.js.map} +1 -1
  84. package/dist/utils/logger.d.ts +9 -0
  85. package/package.json +5 -5
  86. package/dist/auth-GnONzteu.js.map +0 -1
  87. package/dist/errors-D6_y86c5.js.map +0 -1
  88. package/dist/function-routes-j9TwpvQe.js.map +0 -1
  89. package/dist/logger-D-S-hO5e.js.map +0 -1
  90. package/dist/offline-store-Cm5sAX81-CALiO4Gn.js.map +0 -1
@@ -3,9 +3,9 @@ import __rebaseProcess from "process";
3
3
  globalThis.process ??= __rebaseProcess;
4
4
  __rebaseCreateRequire(import.meta.url);
5
5
  import { n as __exportAll } from "./rolldown-runtime-dW7B1o5h.js";
6
- import { t as ApiError } from "./errors-D6_y86c5.js";
6
+ import { t as ApiError } from "./errors-CHSJYpoy.js";
7
7
  import { c as listingPrefix, s as folderKey } from "./keys-COzQ2aE8.js";
8
- import { n as bufferWebStream, o as webStreamFrom, r as bytesView, t as validateUploadedFile } from "./upload-validation-Dd-fnsbA.js";
8
+ import { n as bufferWebStream, o as webStreamFrom, r as bytesView, t as validateUploadedFile } from "./upload-validation-pXcDy_EQ.js";
9
9
  //#region src/storage/GCSStorageController.ts
10
10
  var GCSStorageController_exports = /* @__PURE__ */ __exportAll({ GCSStorageController: () => GCSStorageController });
11
11
  var gcsModule = null;
@@ -399,4 +399,4 @@ function misconfiguredOr(error, bucket) {
399
399
  //#endregion
400
400
  export { GCSStorageController_exports as n, GCSStorageController as t };
401
401
 
402
- //# sourceMappingURL=GCSStorageController-fv6JAZ87.js.map
402
+ //# sourceMappingURL=GCSStorageController-CAKa_EFW.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"GCSStorageController-fv6JAZ87.js","names":[],"sources":["../src/storage/GCSStorageController.ts"],"sourcesContent":["/**\n * Google Cloud Storage controller (works with GCS and Firebase Storage)\n *\n * The `@google-cloud/storage` package is loaded lazily so it remains an\n * optional peer dependency — users who don't need GCS never pay for the\n * import cost or need it installed.\n */\n\nimport type { FileMetadata } from \"@google-cloud/storage\";\nimport {\n StorageController,\n GCSStorageConfig,\n DEFAULT_MAX_FILE_SIZE,\n type StoredObjectInfo,\n type StoredObjectRange,\n type PutObjectOptions,\n type StoredObjectStream\n} from \"./types\";\nimport { folderKey, listingPrefix } from \"./keys\";\nimport { bufferWebStream, bytesView, webStreamFrom } from \"./streams\";\nimport { ApiError } from \"../api/errors\";\nimport { validateUploadedFile } from \"./upload-validation\";\nimport {\n UploadFileProps,\n UploadFileResult,\n DownloadConfig,\n DownloadMetadata,\n StorageListResult,\n StorageReference\n} from \"@rebasepro/types\";\n\n// ---------------------------------------------------------------------------\n// Lazy import of @google-cloud/storage\n// ---------------------------------------------------------------------------\n\nlet gcsModule: typeof import(\"@google-cloud/storage\") | null = null;\n\nasync function loadGCS(): Promise<typeof import(\"@google-cloud/storage\")> {\n if (gcsModule) return gcsModule;\n try {\n gcsModule = await import(\"@google-cloud/storage\");\n return gcsModule;\n } catch {\n throw new Error(\n \"Google Cloud Storage is required for the GCS storage controller. \" +\n \"Install it with: pnpm add @google-cloud/storage\"\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// GCSStorageController\n// ---------------------------------------------------------------------------\n\n/**\n * Google Cloud Storage implementation of `StorageController`.\n *\n * Works with standard GCS buckets **and** Firebase Storage buckets\n * (e.g. `\"my-project.appspot.com\"`).\n */\nexport class GCSStorageController implements StorageController {\n private config: GCSStorageConfig;\n private client: InstanceType<typeof import(\"@google-cloud/storage\").Storage> | null = null;\n\n constructor(config: GCSStorageConfig) {\n this.config = config;\n }\n\n // ------------------------------------------------------------------\n // Lazy client initialisation\n // ------------------------------------------------------------------\n\n private async getClient(): Promise<InstanceType<typeof import(\"@google-cloud/storage\").Storage>> {\n if (this.client) return this.client;\n const { Storage } = await loadGCS();\n this.client = new Storage({\n projectId: this.config.projectId,\n keyFilename: this.config.keyFilename,\n credentials: this.config.credentials\n });\n return this.client;\n }\n\n // ------------------------------------------------------------------\n // StorageController interface\n // ------------------------------------------------------------------\n\n getType(): \"gcs\" {\n return \"gcs\";\n }\n\n /**\n * The configured bucket, and the logical `default` that maps to it.\n * @see S3StorageController.knownBuckets\n */\n knownBuckets(): string[] {\n return [\"default\", this.config.bucket];\n }\n\n async putObject({\n file,\n key,\n metadata,\n bucket\n }: UploadFileProps, options?: PutObjectOptions): Promise<UploadFileResult> {\n if (options?.uploadLimits !== false) this.validateFile(file);\n\n const usedBucket = this.getBucket(bucket);\n const client = await this.getClient();\n const gcsFile = client.bucket(usedBucket).file(key);\n\n // Streamed from the file a chunk at a time. Read into one buffer first,\n // it was held whole (twice, with the copy) for the length of the\n // upload. Handed over as a function so the client can open it again to\n // retry, which it does not do for a stream it was given once.\n await gcsFile.save(() => byteChunks(file), {\n contentType: file.type,\n metadata: metadata ? { metadata: this.flattenMetadata(metadata) } : undefined\n }).catch((error: unknown) => { throw misconfiguredOr(error, usedBucket); });\n\n return {\n key,\n bucket: usedBucket,\n storageUrl: `gs://${usedBucket}/${key}`\n };\n }\n\n /**\n * Read the object's metadata and nothing else — no signing.\n *\n * Signing needs a private key. With `keyFilename` or `credentials` the\n * client has one; on Cloud Run, GKE or GCE it has only the metadata\n * server's token, and falls back to the IAM Credentials `signBlob` API,\n * which the runtime account may call on itself only when it holds\n * `iam.serviceAccounts.signBlob` there. That is not a default grant.\n */\n async getMetadata(key: string, bucket?: string): Promise<DownloadMetadata | null> {\n // Handle gs:// and s3:// URLs\n let resolvedPath = key;\n let resolvedBucket = this.getBucket(bucket);\n\n const match = key.match(/^(gs|s3):\\/\\//);\n if (match) {\n const protocolLength = match[0].length;\n const withoutProtocol = key.substring(protocolLength);\n const firstSlash = withoutProtocol.indexOf(\"/\");\n if (firstSlash > 0) {\n resolvedBucket = withoutProtocol.substring(0, firstSlash);\n resolvedPath = withoutProtocol.substring(firstSlash + 1);\n }\n }\n\n try {\n const client = await this.getClient();\n const [fileMetadata] = await client.bucket(resolvedBucket).file(resolvedPath).getMetadata();\n\n return {\n bucket: resolvedBucket,\n fullPath: resolvedPath,\n name: resolvedPath.split(\"/\").pop() || resolvedPath,\n size: Number(fileMetadata.size) || 0,\n contentType: fileMetadata.contentType || \"application/octet-stream\",\n customMetadata: (fileMetadata.metadata as Record<string, string> | undefined) || {}\n };\n } catch (error: unknown) {\n if (isNotFoundError(error)) {\n return null;\n }\n throw misconfiguredOr(error, resolvedBucket);\n }\n }\n\n async getSignedUrl(key: string, bucket?: string): Promise<DownloadConfig> {\n // Metadata first, to check existence and populate the response.\n const metadata = await this.getMetadata(key, bucket);\n if (!metadata) {\n return {\n url: null,\n fileNotFound: true\n };\n }\n\n const client = await this.getClient();\n // Good for an hour. Server-side callers only: no route hands it out.\n const [url] = await client.bucket(metadata.bucket).file(metadata.fullPath).getSignedUrl({\n action: \"read\",\n expires: Date.now() + 3600 * 1000\n });\n\n return {\n url,\n metadata\n };\n }\n\n /**\n * The object's bytes, type and version — all three of ONE generation.\n *\n * Described first, then read pinned to the generation described, as\n * {@link getObjectStream} reads. It used to download and then ask for the\n * metadata: an overwrite between the two calls answered the old bytes\n * under the new generation's `updated`, and that `lastModified` keys the\n * routes' `ETag` and transform cache, so the stale bytes — or a rendition\n * of them — were served as current. A pinned read of a generation replaced\n * since it was described fails, and the object is described again.\n */\n async getObject(key: string, bucket?: string): Promise<File | null> {\n for (let attempt = 1; ; attempt++) {\n const located = await this.locate(key, bucket);\n if (!located) return null;\n\n let contents: Buffer;\n try {\n contents = await this.readDecompressed(located);\n } catch (error: unknown) {\n // Replaced or deleted since it was described. Three\n // overwrites inside one read is a write loop, not a race.\n if (isNotFoundError(error) && attempt < 3) continue;\n if (isNotFoundError(error)) {\n throw new Error(`The object \"${located.key}\" changed on every read; try again.`);\n }\n throw misconfiguredOr(error, located.bucket);\n }\n\n const contentType = located.meta.contentType || \"application/octet-stream\";\n // The object's own version — see `S3StorageController.getObject`.\n const updated = typeof located.meta.updated === \"string\" ? Date.parse(located.meta.updated) : NaN;\n return new File([bytesView(contents)], located.key.split(\"/\").pop() || located.key, {\n type: contentType,\n lastModified: Number.isNaN(updated) ? undefined : updated\n });\n }\n }\n\n /**\n * The bucket and key a call addresses: a `gs://` or `s3://` URL names its\n * own bucket, anything else is a key in the given (or configured) bucket.\n */\n private resolveLocation(key: string, bucket?: string): { bucket: string; key: string } {\n const match = key.match(/^(gs|s3):\\/\\//);\n if (match) {\n const withoutProtocol = key.substring(match[0].length);\n const firstSlash = withoutProtocol.indexOf(\"/\");\n if (firstSlash > 0) {\n return { bucket: withoutProtocol.substring(0, firstSlash), key: withoutProtocol.substring(firstSlash + 1) };\n }\n }\n return { bucket: this.getBucket(bucket), key };\n }\n\n /** The object's metadata and where it lives, or `null` when there is no such object. */\n private async locate(key: string, bucket?: string): Promise<{ bucket: string; key: string; meta: FileMetadata } | null> {\n const location = this.resolveLocation(key, bucket);\n const client = await this.getClient();\n try {\n const [meta] = await client.bucket(location.bucket).file(location.key).getMetadata();\n return { ...location, meta };\n } catch (error: unknown) {\n if (isNotFoundError(error)) return null;\n throw misconfiguredOr(error, location.bucket);\n }\n }\n\n /**\n * The generation just described, so an overwrite between describing an\n * object and reading it fails the read instead of sending the new bytes\n * under the old size and version.\n */\n private async pinnedFile(located: { bucket: string; key: string; meta: FileMetadata }) {\n const client = await this.getClient();\n const generation = located.meta.generation;\n return client.bucket(located.bucket).file(located.key, generation !== undefined ? { generation } : {});\n }\n\n /**\n * An object stored gzip-encoded (`gsutil cp -z`) is decompressed on the way\n * out, so its stored size is not the size served and a range of the one is\n * not a range of the other. Such an object is read whole, as every object\n * used to be; nothing this server writes is stored that way.\n */\n private async readDecompressed(located: { bucket: string; key: string; meta: FileMetadata }): Promise<Buffer> {\n const [contents] = await (await this.pinnedFile(located)).download();\n return contents;\n }\n\n /** See {@link StorageController.statObject}. One metadata request; no body is read. */\n async statObject(key: string, bucket?: string): Promise<StoredObjectInfo | null> {\n const located = await this.locate(key, bucket);\n if (!located) return null;\n const info = storedObjectInfo(located.meta);\n if (located.meta.contentEncoding === \"gzip\") {\n return { ...info, size: (await this.readDecompressed(located)).byteLength };\n }\n return info;\n }\n\n /**\n * See {@link StorageController.getObjectStream}. One metadata request, then\n * a read of exactly the requested bytes, handed on as they arrive.\n *\n * The metadata comes first, not after the download as `getObject` has it:\n * it is what pins the read to one generation.\n */\n async getObjectStream(\n key: string,\n options?: { bucket?: string; range?: StoredObjectRange }\n ): Promise<StoredObjectStream | null> {\n const located = await this.locate(key, options?.bucket);\n if (!located) return null;\n const info = storedObjectInfo(located.meta);\n const range = options?.range;\n\n if (located.meta.contentEncoding === \"gzip\") {\n const contents = await this.readDecompressed(located);\n const inRange = range && range.end < contents.byteLength && range.start <= range.end;\n return {\n ...info,\n size: contents.byteLength,\n stream: bufferWebStream(inRange ? contents.subarray(range.start, range.end + 1) : contents),\n ...(inRange ? { range } : {})\n };\n }\n\n const file = await this.pinnedFile(located);\n return {\n ...info,\n // Opened on the first read: nothing is requested from the bucket\n // for a stream the caller ends up not reading.\n stream: webStreamFrom(() => file.createReadStream(range ? { start: range.start, end: range.end } : {})),\n ...(range ? { range } : {})\n };\n }\n\n async deleteObject(key: string, bucket?: string): Promise<void> {\n // Handle gs:// and s3:// URLs\n let resolvedPath = key;\n let resolvedBucket = this.getBucket(bucket);\n\n const match = key.match(/^(gs|s3):\\/\\//);\n if (match) {\n const protocolLength = match[0].length;\n const withoutProtocol = key.substring(protocolLength);\n const firstSlash = withoutProtocol.indexOf(\"/\");\n if (firstSlash > 0) {\n resolvedBucket = withoutProtocol.substring(0, firstSlash);\n resolvedPath = withoutProtocol.substring(firstSlash + 1);\n }\n }\n\n const client = await this.getClient();\n const gcsFile = client.bucket(resolvedBucket).file(resolvedPath);\n\n // Not `ignoreNotFound`: the client ignores every 404 with it, a missing\n // bucket's included, so a typo in the bucket name deleted \"successfully\".\n try {\n await gcsFile.delete();\n } catch (error: unknown) {\n if (isNotFoundError(error)) return;\n throw misconfiguredOr(error, resolvedBucket);\n }\n }\n\n async listObjects(prefix: string, options?: {\n bucket?: string;\n maxResults?: number;\n pageToken?: string;\n }): Promise<StorageListResult> {\n const resolvedBucket = this.getBucket(options?.bucket);\n const client = await this.getClient();\n const gcsBucket = client.bucket(resolvedBucket);\n\n // Fetch files (objects) matching the prefix\n const folderPrefix = listingPrefix(prefix);\n const [listed, , filesApiResponse] = await gcsBucket.getFiles({\n prefix: folderPrefix,\n delimiter: \"/\",\n maxResults: options?.maxResults ?? 1000,\n pageToken: options?.pageToken,\n autoPaginate: false\n }).catch((error: unknown) => { throw misconfiguredOr(error, resolvedBucket); });\n\n // The folder's own marker object is not something in the folder —\n // see `S3StorageController.listObjects`.\n const files = listed.filter(file => file.name !== folderPrefix);\n const items: StorageReference[] = files.map(file => ({\n bucket: resolvedBucket,\n fullPath: file.name,\n name: file.name.split(\"/\").pop() || \"\",\n // Each listed object arrives with its full metadata, so a folder\n // can be shown without a metadata request per file.\n ...listedFacts(file.metadata),\n parent: null as never,\n root: null as never,\n toString: () => `gs://${resolvedBucket}/${file.name}`\n }));\n\n // Common prefixes (folder-like entries) come from the API response\n const apiPrefixes: string[] =\n (filesApiResponse as Record<string, unknown> | undefined)?.prefixes as string[] ?? [];\n\n const prefixes: StorageReference[] = apiPrefixes.map(p => {\n // Without the trailing slash GCS puts on a common prefix, so a\n // `fullPath` from a listing is a key any controller accepts.\n const folder = folderKey(p);\n return {\n bucket: resolvedBucket,\n fullPath: folder,\n name: folder.split(\"/\").pop() || \"\",\n parent: null as never,\n root: null as never,\n toString: () => `gs://${resolvedBucket}/${folder}`\n };\n });\n\n // The next page token is returned on the query object (second element)\n const nextPageToken =\n (filesApiResponse as Record<string, unknown> | undefined)?.nextPageToken as string | undefined;\n\n return {\n items,\n prefixes,\n nextPageToken\n };\n }\n\n // ------------------------------------------------------------------\n // Private helpers\n // ------------------------------------------------------------------\n\n /** See {@link StorageController.maxFileSize}. */\n maxFileSize(): number {\n return this.config.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;\n }\n\n /**\n * Validate file before upload\n */\n private validateFile(file: File): void {\n validateUploadedFile(file, this.maxFileSize(), this.config.allowedMimeTypes);\n }\n\n /**\n * Get the bucket name — either from parameter or config.\n */\n private getBucket(bucket?: string): string {\n // \"default\" is a logical bucket name used by local storage;\n // for GCS it should resolve to the configured bucket.\n if (!bucket || bucket === \"default\") return this.config.bucket;\n return bucket;\n }\n\n /**\n * Flatten nested metadata to string values (GCS custom metadata requirement)\n */\n private flattenMetadata(metadata: Record<string, unknown>): Record<string, string> {\n const flattened: Record<string, string> = {};\n for (const [key, value] of Object.entries(metadata)) {\n if (typeof value === \"string\") {\n flattened[key] = value;\n } else if (value !== undefined && value !== null) {\n flattened[key] = JSON.stringify(value);\n }\n }\n return flattened;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Error helpers\n// ---------------------------------------------------------------------------\n\n/** A file's bytes, a chunk at a time — what `File.save` pipes from. */\nasync function* byteChunks(file: Blob): AsyncGenerator<Uint8Array> {\n const reader = file.stream().getReader();\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) return;\n yield value;\n }\n } finally {\n reader.releaseLock();\n }\n}\n\n/** What a listing entry says about its object, from the metadata the listing carried. */\nfunction listedFacts(meta: FileMetadata | undefined): Pick<StorageReference, \"size\" | \"updated\" | \"contentType\"> {\n if (!meta) return {};\n const size = meta.size === undefined ? NaN : Number(meta.size);\n return {\n ...(Number.isFinite(size) ? { size } : {}),\n ...(typeof meta.updated === \"string\" ? { updated: meta.updated } : {}),\n ...(typeof meta.contentType === \"string\" && meta.contentType ? { contentType: meta.contentType } : {})\n };\n}\n\n/** What `statObject` reports, from an object's metadata. */\nfunction storedObjectInfo(meta: FileMetadata): StoredObjectInfo {\n // The object's own version — see `S3StorageController.getObject`.\n const updated = typeof meta.updated === \"string\" ? Date.parse(meta.updated) : NaN;\n return {\n size: Number(meta.size) || 0,\n contentType: meta.contentType || \"application/octet-stream\",\n lastModified: Number.isNaN(updated) ? Date.now() : updated\n };\n}\n\n/**\n * Check whether an error represents a 404 / Not Found from the GCS client.\n */\nfunction isNotFoundError(error: unknown): boolean {\n if (typeof error !== \"object\" || error === null) return false;\n // A missing bucket is a 404 too. Read as a missing object, a typo in the\n // bucket name made every file \"not found\" and every delete a success.\n if (isMissingBucketError(error)) return false;\n const err = error as Record<string, unknown>;\n if (err[\"code\"] === 404) return true;\n if (typeof err[\"message\"] === \"string\" && /not found/i.test(err[\"message\"])) return true;\n return false;\n}\n\n/** GCS's answer for a bucket that does not exist: a 404 whose message says so. */\nfunction isMissingBucketError(error: object): boolean {\n return \"message\" in error && typeof error.message === \"string\" && /bucket does not exist/i.test(error.message);\n}\n\n/**\n * A provider error that says this deployment's storage is misconfigured — the\n * bucket does not exist, or the credentials may not use it — as a\n * `502 STORAGE_MISCONFIGURED` naming the bucket; or the error as it was.\n *\n * These used to reach the caller as a bare 500 \"Internal Server Error\".\n */\nfunction misconfiguredOr(error: unknown, bucket: string): unknown {\n if (typeof error !== \"object\" || error === null) return error;\n if (isMissingBucketError(error)) {\n return new ApiError(502, \"STORAGE_MISCONFIGURED\",\n `The storage bucket \"${bucket}\" does not exist. Check the bucket name this source is configured with (GCS_BUCKET, or GCS_BUCKET__<KEY> for a named source).`,\n { bucket, reason: \"NoSuchBucket\" });\n }\n if (\"code\" in error && (error.code === 401 || error.code === 403)) {\n return new ApiError(502, \"STORAGE_MISCONFIGURED\",\n `The storage provider refused this deployment's credentials for bucket \"${bucket}\". Check the service account and its permissions on the bucket.`,\n { bucket, reason: \"AccessDenied\" });\n }\n return error;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"GCSStorageController-CAKa_EFW.js","names":[],"sources":["../src/storage/GCSStorageController.ts"],"sourcesContent":["/**\n * Google Cloud Storage controller (works with GCS and Firebase Storage)\n *\n * The `@google-cloud/storage` package is loaded lazily so it remains an\n * optional peer dependency — users who don't need GCS never pay for the\n * import cost or need it installed.\n */\n\nimport type { FileMetadata } from \"@google-cloud/storage\";\nimport {\n StorageController,\n GCSStorageConfig,\n DEFAULT_MAX_FILE_SIZE,\n type StoredObjectInfo,\n type StoredObjectRange,\n type PutObjectOptions,\n type StoredObjectStream\n} from \"./types\";\nimport { folderKey, listingPrefix } from \"./keys\";\nimport { bufferWebStream, bytesView, webStreamFrom } from \"./streams\";\nimport { ApiError } from \"../api/errors\";\nimport { validateUploadedFile } from \"./upload-validation\";\nimport {\n UploadFileProps,\n UploadFileResult,\n DownloadConfig,\n DownloadMetadata,\n StorageListResult,\n StorageReference\n} from \"@rebasepro/types\";\n\n// ---------------------------------------------------------------------------\n// Lazy import of @google-cloud/storage\n// ---------------------------------------------------------------------------\n\nlet gcsModule: typeof import(\"@google-cloud/storage\") | null = null;\n\nasync function loadGCS(): Promise<typeof import(\"@google-cloud/storage\")> {\n if (gcsModule) return gcsModule;\n try {\n gcsModule = await import(\"@google-cloud/storage\");\n return gcsModule;\n } catch {\n throw new Error(\n \"Google Cloud Storage is required for the GCS storage controller. \" +\n \"Install it with: pnpm add @google-cloud/storage\"\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// GCSStorageController\n// ---------------------------------------------------------------------------\n\n/**\n * Google Cloud Storage implementation of `StorageController`.\n *\n * Works with standard GCS buckets **and** Firebase Storage buckets\n * (e.g. `\"my-project.appspot.com\"`).\n */\nexport class GCSStorageController implements StorageController {\n private config: GCSStorageConfig;\n private client: InstanceType<typeof import(\"@google-cloud/storage\").Storage> | null = null;\n\n constructor(config: GCSStorageConfig) {\n this.config = config;\n }\n\n // ------------------------------------------------------------------\n // Lazy client initialisation\n // ------------------------------------------------------------------\n\n private async getClient(): Promise<InstanceType<typeof import(\"@google-cloud/storage\").Storage>> {\n if (this.client) return this.client;\n const { Storage } = await loadGCS();\n this.client = new Storage({\n projectId: this.config.projectId,\n keyFilename: this.config.keyFilename,\n credentials: this.config.credentials\n });\n return this.client;\n }\n\n // ------------------------------------------------------------------\n // StorageController interface\n // ------------------------------------------------------------------\n\n getType(): \"gcs\" {\n return \"gcs\";\n }\n\n /**\n * The configured bucket, and the logical `default` that maps to it.\n * @see S3StorageController.knownBuckets\n */\n knownBuckets(): string[] {\n return [\"default\", this.config.bucket];\n }\n\n async putObject({\n file,\n key,\n metadata,\n bucket\n }: UploadFileProps, options?: PutObjectOptions): Promise<UploadFileResult> {\n if (options?.uploadLimits !== false) this.validateFile(file);\n\n const usedBucket = this.getBucket(bucket);\n const client = await this.getClient();\n const gcsFile = client.bucket(usedBucket).file(key);\n\n // Streamed from the file a chunk at a time. Read into one buffer first,\n // it was held whole (twice, with the copy) for the length of the\n // upload. Handed over as a function so the client can open it again to\n // retry, which it does not do for a stream it was given once.\n await gcsFile.save(() => byteChunks(file), {\n contentType: file.type,\n metadata: metadata ? { metadata: this.flattenMetadata(metadata) } : undefined\n }).catch((error: unknown) => { throw misconfiguredOr(error, usedBucket); });\n\n return {\n key,\n bucket: usedBucket,\n storageUrl: `gs://${usedBucket}/${key}`\n };\n }\n\n /**\n * Read the object's metadata and nothing else — no signing.\n *\n * Signing needs a private key. With `keyFilename` or `credentials` the\n * client has one; on Cloud Run, GKE or GCE it has only the metadata\n * server's token, and falls back to the IAM Credentials `signBlob` API,\n * which the runtime account may call on itself only when it holds\n * `iam.serviceAccounts.signBlob` there. That is not a default grant.\n */\n async getMetadata(key: string, bucket?: string): Promise<DownloadMetadata | null> {\n // Handle gs:// and s3:// URLs\n let resolvedPath = key;\n let resolvedBucket = this.getBucket(bucket);\n\n const match = key.match(/^(gs|s3):\\/\\//);\n if (match) {\n const protocolLength = match[0].length;\n const withoutProtocol = key.substring(protocolLength);\n const firstSlash = withoutProtocol.indexOf(\"/\");\n if (firstSlash > 0) {\n resolvedBucket = withoutProtocol.substring(0, firstSlash);\n resolvedPath = withoutProtocol.substring(firstSlash + 1);\n }\n }\n\n try {\n const client = await this.getClient();\n const [fileMetadata] = await client.bucket(resolvedBucket).file(resolvedPath).getMetadata();\n\n return {\n bucket: resolvedBucket,\n fullPath: resolvedPath,\n name: resolvedPath.split(\"/\").pop() || resolvedPath,\n size: Number(fileMetadata.size) || 0,\n contentType: fileMetadata.contentType || \"application/octet-stream\",\n customMetadata: (fileMetadata.metadata as Record<string, string> | undefined) || {}\n };\n } catch (error: unknown) {\n if (isNotFoundError(error)) {\n return null;\n }\n throw misconfiguredOr(error, resolvedBucket);\n }\n }\n\n async getSignedUrl(key: string, bucket?: string): Promise<DownloadConfig> {\n // Metadata first, to check existence and populate the response.\n const metadata = await this.getMetadata(key, bucket);\n if (!metadata) {\n return {\n url: null,\n fileNotFound: true\n };\n }\n\n const client = await this.getClient();\n // Good for an hour. Server-side callers only: no route hands it out.\n const [url] = await client.bucket(metadata.bucket).file(metadata.fullPath).getSignedUrl({\n action: \"read\",\n expires: Date.now() + 3600 * 1000\n });\n\n return {\n url,\n metadata\n };\n }\n\n /**\n * The object's bytes, type and version — all three of ONE generation.\n *\n * Described first, then read pinned to the generation described, as\n * {@link getObjectStream} reads. It used to download and then ask for the\n * metadata: an overwrite between the two calls answered the old bytes\n * under the new generation's `updated`, and that `lastModified` keys the\n * routes' `ETag` and transform cache, so the stale bytes — or a rendition\n * of them — were served as current. A pinned read of a generation replaced\n * since it was described fails, and the object is described again.\n */\n async getObject(key: string, bucket?: string): Promise<File | null> {\n for (let attempt = 1; ; attempt++) {\n const located = await this.locate(key, bucket);\n if (!located) return null;\n\n let contents: Buffer;\n try {\n contents = await this.readDecompressed(located);\n } catch (error: unknown) {\n // Replaced or deleted since it was described. Three\n // overwrites inside one read is a write loop, not a race.\n if (isNotFoundError(error) && attempt < 3) continue;\n if (isNotFoundError(error)) {\n throw new Error(`The object \"${located.key}\" changed on every read; try again.`);\n }\n throw misconfiguredOr(error, located.bucket);\n }\n\n const contentType = located.meta.contentType || \"application/octet-stream\";\n // The object's own version — see `S3StorageController.getObject`.\n const updated = typeof located.meta.updated === \"string\" ? Date.parse(located.meta.updated) : NaN;\n return new File([bytesView(contents)], located.key.split(\"/\").pop() || located.key, {\n type: contentType,\n lastModified: Number.isNaN(updated) ? undefined : updated\n });\n }\n }\n\n /**\n * The bucket and key a call addresses: a `gs://` or `s3://` URL names its\n * own bucket, anything else is a key in the given (or configured) bucket.\n */\n private resolveLocation(key: string, bucket?: string): { bucket: string; key: string } {\n const match = key.match(/^(gs|s3):\\/\\//);\n if (match) {\n const withoutProtocol = key.substring(match[0].length);\n const firstSlash = withoutProtocol.indexOf(\"/\");\n if (firstSlash > 0) {\n return { bucket: withoutProtocol.substring(0, firstSlash), key: withoutProtocol.substring(firstSlash + 1) };\n }\n }\n return { bucket: this.getBucket(bucket), key };\n }\n\n /** The object's metadata and where it lives, or `null` when there is no such object. */\n private async locate(key: string, bucket?: string): Promise<{ bucket: string; key: string; meta: FileMetadata } | null> {\n const location = this.resolveLocation(key, bucket);\n const client = await this.getClient();\n try {\n const [meta] = await client.bucket(location.bucket).file(location.key).getMetadata();\n return { ...location, meta };\n } catch (error: unknown) {\n if (isNotFoundError(error)) return null;\n throw misconfiguredOr(error, location.bucket);\n }\n }\n\n /**\n * The generation just described, so an overwrite between describing an\n * object and reading it fails the read instead of sending the new bytes\n * under the old size and version.\n */\n private async pinnedFile(located: { bucket: string; key: string; meta: FileMetadata }) {\n const client = await this.getClient();\n const generation = located.meta.generation;\n return client.bucket(located.bucket).file(located.key, generation !== undefined ? { generation } : {});\n }\n\n /**\n * An object stored gzip-encoded (`gsutil cp -z`) is decompressed on the way\n * out, so its stored size is not the size served and a range of the one is\n * not a range of the other. Such an object is read whole, as every object\n * used to be; nothing this server writes is stored that way.\n */\n private async readDecompressed(located: { bucket: string; key: string; meta: FileMetadata }): Promise<Buffer> {\n const [contents] = await (await this.pinnedFile(located)).download();\n return contents;\n }\n\n /** See {@link StorageController.statObject}. One metadata request; no body is read. */\n async statObject(key: string, bucket?: string): Promise<StoredObjectInfo | null> {\n const located = await this.locate(key, bucket);\n if (!located) return null;\n const info = storedObjectInfo(located.meta);\n if (located.meta.contentEncoding === \"gzip\") {\n return { ...info, size: (await this.readDecompressed(located)).byteLength };\n }\n return info;\n }\n\n /**\n * See {@link StorageController.getObjectStream}. One metadata request, then\n * a read of exactly the requested bytes, handed on as they arrive.\n *\n * The metadata comes first, not after the download as `getObject` has it:\n * it is what pins the read to one generation.\n */\n async getObjectStream(\n key: string,\n options?: { bucket?: string; range?: StoredObjectRange }\n ): Promise<StoredObjectStream | null> {\n const located = await this.locate(key, options?.bucket);\n if (!located) return null;\n const info = storedObjectInfo(located.meta);\n const range = options?.range;\n\n if (located.meta.contentEncoding === \"gzip\") {\n const contents = await this.readDecompressed(located);\n const inRange = range && range.end < contents.byteLength && range.start <= range.end;\n return {\n ...info,\n size: contents.byteLength,\n stream: bufferWebStream(inRange ? contents.subarray(range.start, range.end + 1) : contents),\n ...(inRange ? { range } : {})\n };\n }\n\n const file = await this.pinnedFile(located);\n return {\n ...info,\n // Opened on the first read: nothing is requested from the bucket\n // for a stream the caller ends up not reading.\n stream: webStreamFrom(() => file.createReadStream(range ? { start: range.start, end: range.end } : {})),\n ...(range ? { range } : {})\n };\n }\n\n async deleteObject(key: string, bucket?: string): Promise<void> {\n // Handle gs:// and s3:// URLs\n let resolvedPath = key;\n let resolvedBucket = this.getBucket(bucket);\n\n const match = key.match(/^(gs|s3):\\/\\//);\n if (match) {\n const protocolLength = match[0].length;\n const withoutProtocol = key.substring(protocolLength);\n const firstSlash = withoutProtocol.indexOf(\"/\");\n if (firstSlash > 0) {\n resolvedBucket = withoutProtocol.substring(0, firstSlash);\n resolvedPath = withoutProtocol.substring(firstSlash + 1);\n }\n }\n\n const client = await this.getClient();\n const gcsFile = client.bucket(resolvedBucket).file(resolvedPath);\n\n // Not `ignoreNotFound`: the client ignores every 404 with it, a missing\n // bucket's included, so a typo in the bucket name deleted \"successfully\".\n try {\n await gcsFile.delete();\n } catch (error: unknown) {\n if (isNotFoundError(error)) return;\n throw misconfiguredOr(error, resolvedBucket);\n }\n }\n\n async listObjects(prefix: string, options?: {\n bucket?: string;\n maxResults?: number;\n pageToken?: string;\n }): Promise<StorageListResult> {\n const resolvedBucket = this.getBucket(options?.bucket);\n const client = await this.getClient();\n const gcsBucket = client.bucket(resolvedBucket);\n\n // Fetch files (objects) matching the prefix\n const folderPrefix = listingPrefix(prefix);\n const [listed, , filesApiResponse] = await gcsBucket.getFiles({\n prefix: folderPrefix,\n delimiter: \"/\",\n maxResults: options?.maxResults ?? 1000,\n pageToken: options?.pageToken,\n autoPaginate: false\n }).catch((error: unknown) => { throw misconfiguredOr(error, resolvedBucket); });\n\n // The folder's own marker object is not something in the folder —\n // see `S3StorageController.listObjects`.\n const files = listed.filter(file => file.name !== folderPrefix);\n const items: StorageReference[] = files.map(file => ({\n bucket: resolvedBucket,\n fullPath: file.name,\n name: file.name.split(\"/\").pop() || \"\",\n // Each listed object arrives with its full metadata, so a folder\n // can be shown without a metadata request per file.\n ...listedFacts(file.metadata),\n parent: null as never,\n root: null as never,\n toString: () => `gs://${resolvedBucket}/${file.name}`\n }));\n\n // Common prefixes (folder-like entries) come from the API response\n const apiPrefixes: string[] =\n (filesApiResponse as Record<string, unknown> | undefined)?.prefixes as string[] ?? [];\n\n const prefixes: StorageReference[] = apiPrefixes.map(p => {\n // Without the trailing slash GCS puts on a common prefix, so a\n // `fullPath` from a listing is a key any controller accepts.\n const folder = folderKey(p);\n return {\n bucket: resolvedBucket,\n fullPath: folder,\n name: folder.split(\"/\").pop() || \"\",\n parent: null as never,\n root: null as never,\n toString: () => `gs://${resolvedBucket}/${folder}`\n };\n });\n\n // The next page token is returned on the query object (second element)\n const nextPageToken =\n (filesApiResponse as Record<string, unknown> | undefined)?.nextPageToken as string | undefined;\n\n return {\n items,\n prefixes,\n nextPageToken\n };\n }\n\n // ------------------------------------------------------------------\n // Private helpers\n // ------------------------------------------------------------------\n\n /** See {@link StorageController.maxFileSize}. */\n maxFileSize(): number {\n return this.config.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;\n }\n\n /**\n * Validate file before upload\n */\n private validateFile(file: File): void {\n validateUploadedFile(file, this.maxFileSize(), this.config.allowedMimeTypes);\n }\n\n /**\n * Get the bucket name — either from parameter or config.\n */\n private getBucket(bucket?: string): string {\n // \"default\" is a logical bucket name used by local storage;\n // for GCS it should resolve to the configured bucket.\n if (!bucket || bucket === \"default\") return this.config.bucket;\n return bucket;\n }\n\n /**\n * Flatten nested metadata to string values (GCS custom metadata requirement)\n */\n private flattenMetadata(metadata: Record<string, unknown>): Record<string, string> {\n const flattened: Record<string, string> = {};\n for (const [key, value] of Object.entries(metadata)) {\n if (typeof value === \"string\") {\n flattened[key] = value;\n } else if (value !== undefined && value !== null) {\n flattened[key] = JSON.stringify(value);\n }\n }\n return flattened;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Error helpers\n// ---------------------------------------------------------------------------\n\n/** A file's bytes, a chunk at a time — what `File.save` pipes from. */\nasync function* byteChunks(file: Blob): AsyncGenerator<Uint8Array> {\n const reader = file.stream().getReader();\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) return;\n yield value;\n }\n } finally {\n reader.releaseLock();\n }\n}\n\n/** What a listing entry says about its object, from the metadata the listing carried. */\nfunction listedFacts(meta: FileMetadata | undefined): Pick<StorageReference, \"size\" | \"updated\" | \"contentType\"> {\n if (!meta) return {};\n const size = meta.size === undefined ? NaN : Number(meta.size);\n return {\n ...(Number.isFinite(size) ? { size } : {}),\n ...(typeof meta.updated === \"string\" ? { updated: meta.updated } : {}),\n ...(typeof meta.contentType === \"string\" && meta.contentType ? { contentType: meta.contentType } : {})\n };\n}\n\n/** What `statObject` reports, from an object's metadata. */\nfunction storedObjectInfo(meta: FileMetadata): StoredObjectInfo {\n // The object's own version — see `S3StorageController.getObject`.\n const updated = typeof meta.updated === \"string\" ? Date.parse(meta.updated) : NaN;\n return {\n size: Number(meta.size) || 0,\n contentType: meta.contentType || \"application/octet-stream\",\n lastModified: Number.isNaN(updated) ? Date.now() : updated\n };\n}\n\n/**\n * Check whether an error represents a 404 / Not Found from the GCS client.\n */\nfunction isNotFoundError(error: unknown): boolean {\n if (typeof error !== \"object\" || error === null) return false;\n // A missing bucket is a 404 too. Read as a missing object, a typo in the\n // bucket name made every file \"not found\" and every delete a success.\n if (isMissingBucketError(error)) return false;\n const err = error as Record<string, unknown>;\n if (err[\"code\"] === 404) return true;\n if (typeof err[\"message\"] === \"string\" && /not found/i.test(err[\"message\"])) return true;\n return false;\n}\n\n/** GCS's answer for a bucket that does not exist: a 404 whose message says so. */\nfunction isMissingBucketError(error: object): boolean {\n return \"message\" in error && typeof error.message === \"string\" && /bucket does not exist/i.test(error.message);\n}\n\n/**\n * A provider error that says this deployment's storage is misconfigured — the\n * bucket does not exist, or the credentials may not use it — as a\n * `502 STORAGE_MISCONFIGURED` naming the bucket; or the error as it was.\n *\n * These used to reach the caller as a bare 500 \"Internal Server Error\".\n */\nfunction misconfiguredOr(error: unknown, bucket: string): unknown {\n if (typeof error !== \"object\" || error === null) return error;\n if (isMissingBucketError(error)) {\n return new ApiError(502, \"STORAGE_MISCONFIGURED\",\n `The storage bucket \"${bucket}\" does not exist. Check the bucket name this source is configured with (GCS_BUCKET, or GCS_BUCKET__<KEY> for a named source).`,\n { bucket, reason: \"NoSuchBucket\" });\n }\n if (\"code\" in error && (error.code === 401 || error.code === 403)) {\n return new ApiError(502, \"STORAGE_MISCONFIGURED\",\n `The storage provider refused this deployment's credentials for bucket \"${bucket}\". Check the service account and its permissions on the bucket.`,\n { bucket, reason: \"AccessDenied\" });\n }\n return error;\n}\n"],"mappings":"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"}
@@ -3,9 +3,9 @@ import __rebaseProcess from "process";
3
3
  globalThis.process ??= __rebaseProcess;
4
4
  __rebaseCreateRequire(import.meta.url);
5
5
  import { n as __exportAll } from "./rolldown-runtime-dW7B1o5h.js";
6
- import { t as ApiError } from "./errors-D6_y86c5.js";
6
+ import { t as ApiError } from "./errors-CHSJYpoy.js";
7
7
  import { c as listingPrefix, s as folderKey } from "./keys-COzQ2aE8.js";
8
- import { o as webStreamFrom, t as validateUploadedFile } from "./upload-validation-Dd-fnsbA.js";
8
+ import { o as webStreamFrom, t as validateUploadedFile } from "./upload-validation-pXcDy_EQ.js";
9
9
  //#region src/storage/S3StorageController.ts
10
10
  var S3StorageController_exports = /* @__PURE__ */ __exportAll({ S3StorageController: () => S3StorageController });
11
11
  var _s3Module;
@@ -477,4 +477,4 @@ function isByteIterable(value) {
477
477
  //#endregion
478
478
  export { S3StorageController_exports as n, S3StorageController as t };
479
479
 
480
- //# sourceMappingURL=S3StorageController-BNg-pXiY.js.map
480
+ //# sourceMappingURL=S3StorageController-Kt-LJpf3.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"S3StorageController-BNg-pXiY.js","names":[],"sources":["../src/storage/S3StorageController.ts"],"sourcesContent":["/**\n * S3-compatible storage controller (works with AWS S3 and MinIO)\n */\n\nimport type { S3Client as S3ClientType, } from \"@aws-sdk/client-s3\";\nimport {\n DEFAULT_MAX_FILE_SIZE,\n S3StorageConfig,\n StorageController,\n type StoredObjectInfo,\n type StoredObjectRange,\n type PutObjectOptions,\n type StoredObjectStream\n} from \"./types\";\nimport { folderKey, listingPrefix } from \"./keys\";\nimport { webStreamFrom } from \"./streams\";\nimport { ApiError } from \"../api/errors\";\nimport { validateUploadedFile } from \"./upload-validation\";\nimport {\n DownloadConfig,\n DownloadMetadata,\n StorageListResult,\n StorageReference,\n UploadFileProps,\n UploadFileResult\n} from \"@rebasepro/types\";\n\nlet _s3Module: typeof import(\"@aws-sdk/client-s3\") | undefined;\nlet _presignerModule: typeof import(\"@aws-sdk/s3-request-presigner\") | undefined;\n\nasync function loadS3() {\n if (!_s3Module) {\n try {\n _s3Module = await import(\"@aws-sdk/client-s3\");\n } catch {\n throw new Error(\n \"@aws-sdk/client-s3 is required for S3 storage. \" +\n \"Install it: pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner\"\n );\n }\n }\n return _s3Module;\n}\n\nasync function loadPresigner() {\n if (!_presignerModule) {\n try {\n _presignerModule = await import(\"@aws-sdk/s3-request-presigner\");\n } catch {\n throw new Error(\n \"@aws-sdk/s3-request-presigner is required for S3 storage. \" +\n \"Install it: pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner\"\n );\n }\n }\n return _presignerModule;\n}\n\n/** Files up to this size are one `PutObject`; larger ones are uploaded in parts of (at least) this size. */\nconst MULTIPART_PART_SIZE = 8 * 1024 * 1024;\n/** S3's limit on the parts of one upload. */\nconst MAX_MULTIPART_PARTS = 10_000;\n\n/**\n * S3-compatible storage implementation\n * Works with AWS S3 and MinIO (with forcePathStyle option)\n */\nexport class S3StorageController implements StorageController {\n private config: S3StorageConfig;\n private _client: S3ClientType | undefined;\n\n constructor(config: S3StorageConfig) {\n this.config = config;\n }\n\n /**\n * Lazily create and cache the S3 client on first use\n */\n private async getClient(): Promise<S3ClientType> {\n if (!this._client) {\n const s3 = await loadS3();\n this._client = new s3.S3Client({\n region: this.config.region || \"us-east-1\",\n endpoint: this.config.endpoint,\n forcePathStyle: this.config.forcePathStyle ?? !!this.config.endpoint, // Auto-enable for custom endpoints (MinIO)\n credentials: {\n accessKeyId: this.config.accessKeyId,\n secretAccessKey: this.config.secretAccessKey\n }\n });\n }\n return this._client;\n }\n\n getType(): \"s3\" {\n return \"s3\";\n }\n\n /**\n * The configured bucket, and the logical `default` that maps to it.\n *\n * Any other name went straight to S3 as a bucket name, so the request\n * parameter was a way to address *any* bucket the deployment's credentials\n * can reach — and a mistyped one came back as \"file not found\".\n */\n knownBuckets(): string[] {\n return [\"default\", this.config.bucket];\n }\n\n /** See {@link StorageController.maxFileSize}. */\n maxFileSize(): number {\n return this.config.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;\n }\n\n /**\n * Validate file before upload\n */\n private validateFile(file: File): void {\n validateUploadedFile(file, this.maxFileSize(), this.config.allowedMimeTypes);\n }\n\n /**\n * Get the bucket name - either from parameter or config\n */\n private getBucket(bucket?: string): string {\n // \"default\" is a logical bucket name used by local storage;\n // for S3 it should resolve to the configured bucket.\n if (!bucket || bucket === \"default\") return this.config.bucket;\n return bucket;\n }\n\n async putObject({\n file,\n key,\n metadata,\n bucket\n }: UploadFileProps, options?: PutObjectOptions): Promise<UploadFileResult> {\n if (options?.uploadLimits !== false) this.validateFile(file);\n\n const usedBucket = this.getBucket(bucket);\n const s3 = await loadS3();\n const client = await this.getClient();\n const target = {\n Bucket: usedBucket,\n Key: key,\n ContentType: file.type,\n Metadata: metadata ? this.flattenMetadata(metadata) : undefined\n };\n\n if (file.size <= MULTIPART_PART_SIZE) {\n // One request from one buffer, as always: a small file is no bigger\n // than a part. `Buffer.from(arrayBuffer)` is a view, not a copy.\n await client.send(new s3.PutObjectCommand({ ...target, Body: Buffer.from(await file.arrayBuffer()) }))\n .catch((error: unknown) => { throw misconfiguredOr(error, usedBucket); });\n } else {\n await this.putMultipart(target, file);\n }\n\n return {\n key,\n bucket: usedBucket,\n storageUrl: `s3://${usedBucket}/${key}`\n };\n }\n\n /**\n * Upload a large file in parts, one part in memory at a time.\n *\n * Reading the whole file into one buffer held it twice over (the\n * `arrayBuffer()`, then the SDK's view and the multipart parser's copy\n * before it), and `PutObject` stops at 5 GB regardless. A slice of a\n * `File` reads only that range — from disk, for the disk-backed files TUS\n * and backups hand over — so memory is one part whatever the file's size.\n * A failure aborts the upload: no partial object appears at the key, and\n * no orphaned parts are kept (and billed).\n */\n private async putMultipart(\n target: { Bucket: string; Key: string; ContentType: string; Metadata?: Record<string, string> },\n file: Blob\n ): Promise<void> {\n const s3 = await loadS3();\n const client = await this.getClient();\n // At most 10,000 parts: a part grows past 8 MiB only beyond 80 GB.\n const partSize = Math.max(MULTIPART_PART_SIZE, Math.ceil(file.size / MAX_MULTIPART_PARTS));\n const { UploadId } = await client.send(new s3.CreateMultipartUploadCommand(target))\n .catch((error: unknown) => { throw misconfiguredOr(error, target.Bucket); });\n if (!UploadId) throw new Error(`S3 did not start a multipart upload for ${target.Key}.`);\n const { Bucket, Key } = target;\n try {\n const parts: { ETag?: string; PartNumber: number }[] = [];\n for (let offset = 0, PartNumber = 1; offset < file.size; offset += partSize, PartNumber++) {\n const Body = Buffer.from(await file.slice(offset, Math.min(offset + partSize, file.size)).arrayBuffer());\n const { ETag } = await client.send(new s3.UploadPartCommand({ Bucket, Key, UploadId, PartNumber, Body }));\n parts.push({ ETag, PartNumber });\n }\n await client.send(new s3.CompleteMultipartUploadCommand({ Bucket, Key, UploadId, MultipartUpload: { Parts: parts } }));\n } catch (error: unknown) {\n await client.send(new s3.AbortMultipartUploadCommand({ Bucket, Key, UploadId })).catch(() => undefined);\n throw misconfiguredOr(error, Bucket);\n }\n }\n\n /**\n * Flatten nested metadata to string values (S3 requirement)\n */\n private flattenMetadata(metadata: Record<string, unknown>): Record<string, string> {\n const flattened: Record<string, string> = {};\n for (const [key, value] of Object.entries(metadata)) {\n if (typeof value === \"string\") {\n flattened[key] = value;\n } else if (value !== undefined && value !== null) {\n flattened[key] = JSON.stringify(value);\n }\n }\n return flattened;\n }\n\n async getSignedUrl(key: string, bucket?: string): Promise<DownloadConfig> {\n // Handle s3:// and gs:// URLs for backward compatibility\n let resolvedPath = key;\n let resolvedBucket = this.getBucket(bucket);\n\n const match = key.match(/^(s3|gs):\\/\\//);\n if (match) {\n const protocolLength = match[0].length;\n const withoutProtocol = key.substring(protocolLength);\n const firstSlash = withoutProtocol.indexOf(\"/\");\n if (firstSlash > 0) {\n resolvedBucket = withoutProtocol.substring(0, firstSlash);\n resolvedPath = withoutProtocol.substring(firstSlash + 1);\n }\n }\n\n try {\n const s3 = await loadS3();\n const presigner = await loadPresigner();\n const client = await this.getClient();\n\n // First check if the object exists and get metadata\n const headCommand = new s3.HeadObjectCommand({\n Bucket: resolvedBucket,\n Key: resolvedPath\n });\n\n const headResult = await client.send(headCommand);\n\n // Generate a signed URL\n const getCommand = new s3.GetObjectCommand({\n Bucket: resolvedBucket,\n Key: resolvedPath\n });\n\n // Good for an hour. Server-side callers only: no route hands it out.\n const url = await presigner.getSignedUrl(client, getCommand, { expiresIn: 3600 });\n\n const metadata: DownloadMetadata = {\n bucket: resolvedBucket,\n fullPath: resolvedPath,\n name: resolvedPath.split(\"/\").pop() || resolvedPath,\n size: headResult.ContentLength || 0,\n contentType: headResult.ContentType || \"application/octet-stream\",\n customMetadata: headResult.Metadata || {}\n };\n\n return {\n url,\n metadata\n };\n } catch (error: unknown) {\n if (isS3NotFound(error)) {\n return {\n url: null,\n fileNotFound: true\n };\n }\n throw misconfiguredOr(error, resolvedBucket);\n }\n }\n\n async getObject(key: string, bucket?: string): Promise<File | null> {\n // Handle s3:// and gs:// URLs\n let resolvedPath = key;\n let resolvedBucket = this.getBucket(bucket);\n\n const match = key.match(/^(s3|gs):\\/\\//);\n if (match) {\n const protocolLength = match[0].length;\n const withoutProtocol = key.substring(protocolLength);\n const firstSlash = withoutProtocol.indexOf(\"/\");\n if (firstSlash > 0) {\n resolvedBucket = withoutProtocol.substring(0, firstSlash);\n resolvedPath = withoutProtocol.substring(firstSlash + 1);\n }\n }\n\n try {\n const s3 = await loadS3();\n const client = await this.getClient();\n\n const command = new s3.GetObjectCommand({\n Bucket: resolvedBucket,\n Key: resolvedPath\n });\n\n const response = await client.send(command);\n\n if (!response.Body) {\n return null;\n }\n\n // Convert stream to buffer\n const chunks: Uint8Array[] = [];\n for await (const chunk of response.Body as AsyncIterable<Uint8Array>) {\n chunks.push(chunk);\n }\n const buffer = Buffer.concat(chunks);\n\n const contentType = response.ContentType || \"application/octet-stream\";\n const fileName = resolvedPath.split(\"/\").pop() || resolvedPath;\n\n const blob = new Blob([buffer], { type: contentType });\n // The object's own version. Left out, `File` stamps `Date.now()`,\n // and the routes build the `ETag`, `Last-Modified` and the\n // transform cache key from this — so every read was a new version:\n // no 304, no cache hit, and a fresh rendition written per request.\n return new File([blob], fileName, {\n type: contentType,\n lastModified: response.LastModified?.getTime()\n });\n } catch (error: unknown) {\n if (isS3NotFound(error)) {\n return null;\n }\n throw misconfiguredOr(error, resolvedBucket);\n }\n }\n\n /**\n * The bucket and key a call addresses: an `s3://` or `gs://` URL names its\n * own bucket, anything else is a key in the given (or configured) bucket.\n */\n private resolveLocation(key: string, bucket?: string): { bucket: string; key: string } {\n const match = key.match(/^(s3|gs):\\/\\//);\n if (match) {\n const withoutProtocol = key.substring(match[0].length);\n const firstSlash = withoutProtocol.indexOf(\"/\");\n if (firstSlash > 0) {\n return { bucket: withoutProtocol.substring(0, firstSlash), key: withoutProtocol.substring(firstSlash + 1) };\n }\n }\n return { bucket: this.getBucket(bucket), key };\n }\n\n /**\n * See {@link StorageController.getMetadata}. One `HeadObject`. Without it,\n * `GET /metadata/*` fell back to `getSignedUrl`, which signed a URL per\n * call — ~134 µs of CPU — only for the route to throw it away.\n */\n async getMetadata(key: string, bucket?: string): Promise<DownloadMetadata | null> {\n const location = this.resolveLocation(key, bucket);\n const s3 = await loadS3();\n const client = await this.getClient();\n try {\n const head = await client.send(new s3.HeadObjectCommand({ Bucket: location.bucket, Key: location.key }));\n return {\n bucket: location.bucket,\n fullPath: location.key,\n name: location.key.split(\"/\").pop() || location.key,\n size: head.ContentLength || 0,\n contentType: head.ContentType || \"application/octet-stream\",\n customMetadata: head.Metadata || {}\n };\n } catch (error: unknown) {\n if (isS3NotFound(error)) return null;\n throw misconfiguredOr(error, location.bucket);\n }\n }\n\n /** See {@link StorageController.statObject}. One `HeadObject`; no body is read. */\n async statObject(key: string, bucket?: string): Promise<StoredObjectInfo | null> {\n const location = this.resolveLocation(key, bucket);\n const s3 = await loadS3();\n const client = await this.getClient();\n try {\n const head = await client.send(new s3.HeadObjectCommand({ Bucket: location.bucket, Key: location.key }));\n return {\n size: head.ContentLength ?? 0,\n contentType: head.ContentType || \"application/octet-stream\",\n // The object's version — see `getObject` on why a missing one\n // must not become a stable value.\n lastModified: head.LastModified?.getTime() ?? Date.now()\n };\n } catch (error: unknown) {\n if (isS3NotFound(error)) return null;\n throw misconfiguredOr(error, location.bucket);\n }\n }\n\n /**\n * See {@link StorageController.getObjectStream}. One `GetObject`, with a\n * `Range` when one is asked for, so only those bytes leave the bucket; the\n * body is handed on as it arrives rather than collected first.\n */\n async getObjectStream(\n key: string,\n options?: { bucket?: string; range?: StoredObjectRange }\n ): Promise<StoredObjectStream | null> {\n const location = this.resolveLocation(key, options?.bucket);\n const range = options?.range;\n const s3 = await loadS3();\n const client = await this.getClient();\n let response;\n try {\n response = await client.send(new s3.GetObjectCommand({\n Bucket: location.bucket,\n Key: location.key,\n ...(range ? { Range: `bytes=${range.start}-${range.end}` } : {})\n }));\n } catch (error: unknown) {\n if (isS3NotFound(error)) return null;\n // The object shrank below the range since it was described. Read\n // it whole: the caller serves a whole object as a 200, which HTTP\n // allows for any range request.\n if (range && isS3RangeNotSatisfiable(error)) return this.getObjectStream(key, { bucket: options?.bucket });\n throw misconfiguredOr(error, location.bucket);\n }\n const body = response.Body;\n if (!body) return null;\n\n // A ranged answer says the whole object's size after the slash of its\n // `Content-Range`. A store that ignored the `Range` sent no\n // `Content-Range` and the whole object, which is what this then says.\n const total = range ? /\\/(\\d+)\\s*$/.exec(response.ContentRange ?? \"\")?.[1] : undefined;\n const served = range && total !== undefined ? range : undefined;\n return {\n stream: isByteIterable(body) ? webStreamFrom(body) : body.transformToWebStream(),\n size: total !== undefined ? Number(total) : response.ContentLength ?? 0,\n contentType: response.ContentType || \"application/octet-stream\",\n lastModified: response.LastModified?.getTime() ?? Date.now(),\n ...(served ? { range: served } : {})\n };\n }\n\n async deleteObject(key: string, bucket?: string): Promise<void> {\n // Handle s3:// and gs:// URLs\n let resolvedPath = key;\n let resolvedBucket = this.getBucket(bucket);\n\n const match = key.match(/^(s3|gs):\\/\\//);\n if (match) {\n const protocolLength = match[0].length;\n const withoutProtocol = key.substring(protocolLength);\n const firstSlash = withoutProtocol.indexOf(\"/\");\n if (firstSlash > 0) {\n resolvedBucket = withoutProtocol.substring(0, firstSlash);\n resolvedPath = withoutProtocol.substring(firstSlash + 1);\n }\n }\n\n const s3 = await loadS3();\n const client = await this.getClient();\n\n const command = new s3.DeleteObjectCommand({\n Bucket: resolvedBucket,\n Key: resolvedPath\n });\n\n // Deleting something that is already gone is the outcome the caller\n // wanted, so it is not an error. AWS itself answers DeleteObject with\n // 204 for a key that never existed, but S3-compatible backends (MinIO,\n // Ceph, R2 in some configurations) do raise NoSuchKey — and the local\n // controller has always returned quietly in this case. Without this,\n // which of those a deployment happens to run decides whether\n // `DELETE /api/storage/:path` on a stale reference is a 200 or a 500.\n try {\n await client.send(command);\n } catch (error: unknown) {\n if (isS3NotFound(error)) {\n return;\n }\n throw misconfiguredOr(error, resolvedBucket);\n }\n }\n\n async listObjects(prefix: string, options?: {\n bucket?: string;\n maxResults?: number;\n pageToken?: string;\n }): Promise<StorageListResult> {\n const resolvedBucket = this.getBucket(options?.bucket);\n\n const s3 = await loadS3();\n const client = await this.getClient();\n\n const folderPrefix = listingPrefix(prefix);\n const command = new s3.ListObjectsV2Command({\n Bucket: resolvedBucket,\n Prefix: folderPrefix,\n MaxKeys: options?.maxResults ?? 1000,\n ContinuationToken: options?.pageToken,\n Delimiter: \"/\" // This gives us folder-like behavior\n });\n\n const response = await client.send(command)\n .catch((error: unknown) => { throw misconfiguredOr(error, resolvedBucket); });\n\n // The folder's own marker — the zero-byte `folder/` object that\n // `POST /folder` writes — is in `Contents` when the listed prefix is\n // that folder. It is how the folder exists, not something in it, and\n // listed it is an entry with an empty name. Local storage has no such\n // object to list.\n const objects = (response.Contents || []).filter(obj => obj.Key !== folderPrefix);\n const items: StorageReference[] = objects.map(obj => ({\n bucket: resolvedBucket,\n fullPath: obj.Key || \"\",\n name: (obj.Key || \"\").split(\"/\").pop() || \"\",\n // What the listing already carries, so a folder can be shown\n // without a HEAD per file. ListObjectsV2 has no content type.\n ...(typeof obj.Size === \"number\" ? { size: obj.Size } : {}),\n ...(obj.LastModified instanceof Date ? { updated: obj.LastModified.toISOString() } : {}),\n parent: null as never,\n root: null as never,\n toString: () => `s3://${resolvedBucket}/${obj.Key}`\n }));\n\n const prefixes: StorageReference[] = (response.CommonPrefixes || []).map(common => {\n // Without the trailing slash S3 puts on a common prefix, so a\n // `fullPath` from a listing is a key any controller accepts.\n const folder = folderKey(common.Prefix || \"\");\n return {\n bucket: resolvedBucket,\n fullPath: folder,\n name: folder.split(\"/\").pop() || \"\",\n parent: null as never,\n root: null as never,\n toString: () => `s3://${resolvedBucket}/${folder}`\n };\n });\n\n return {\n items,\n prefixes,\n nextPageToken: response.NextContinuationToken\n };\n }\n}\n\n/** The error name and HTTP status an SDK error carries, read without assuming its shape. */\nfunction s3ErrorFacts(error: unknown): { name?: string; status?: number } {\n if (typeof error !== \"object\" || error === null) return {};\n const name = \"name\" in error && typeof error.name === \"string\" ? error.name : undefined;\n const meta = \"$metadata\" in error ? error.$metadata : undefined;\n const status = typeof meta === \"object\" && meta !== null && \"httpStatusCode\" in meta && typeof meta.httpStatusCode === \"number\"\n ? meta.httpStatusCode\n : undefined;\n return { name, status };\n}\n\n/**\n * A missing object, as `GetObject` (`NoSuchKey`) and `HeadObject` (`NotFound`, no body) report it.\n *\n * Not a missing bucket, which is a 404 too: read as a missing object, a typo in\n * `S3_BUCKET` made every file of the deployment \"not found\", and a delete\n * report success. (A `HeadObject` on a missing bucket carries no error name, so\n * that one call still cannot tell the two apart.)\n */\nfunction isS3NotFound(error: unknown): boolean {\n const { name, status } = s3ErrorFacts(error);\n if (name === \"NoSuchBucket\") return false;\n return name === \"NoSuchKey\" || name === \"NotFound\" || status === 404;\n}\n\n/** What S3 answers when the deployment's configuration, not the request, is wrong. */\nconst MISCONFIGURATION_ERRORS = new Set([\"NoSuchBucket\", \"AccessDenied\", \"InvalidAccessKeyId\", \"SignatureDoesNotMatch\"]);\n\n/**\n * A provider error that says this deployment's storage is misconfigured, as a\n * `502 STORAGE_MISCONFIGURED` naming the bucket — or the error as it was.\n *\n * These used to reach the caller as a bare 500 \"Internal Server Error\", with\n * nothing to say the bucket or its credentials were the problem.\n */\nfunction misconfiguredOr(error: unknown, bucket: string): unknown {\n const { name } = s3ErrorFacts(error);\n if (!name || !MISCONFIGURATION_ERRORS.has(name)) return error;\n return new ApiError(\n 502,\n \"STORAGE_MISCONFIGURED\",\n name === \"NoSuchBucket\"\n ? `The storage bucket \"${bucket}\" does not exist. Check the bucket name this source is configured with (S3_BUCKET, or S3_BUCKET__<KEY> for a named source).`\n : `The storage provider refused this deployment's credentials for bucket \"${bucket}\" (${name}). Check the access key and its permissions on the bucket.`,\n { bucket, reason: name }\n );\n}\n\nfunction isS3RangeNotSatisfiable(error: unknown): boolean {\n const { name, status } = s3ErrorFacts(error);\n return name === \"InvalidRange\" || status === 416;\n}\n\n/** A Node response body — what the SDK hands over on Node — rather than a browser one. */\nfunction isByteIterable(value: unknown): value is AsyncIterable<Uint8Array> {\n return typeof value === \"object\" && value !== null && Symbol.asyncIterator in value;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"S3StorageController-Kt-LJpf3.js","names":[],"sources":["../src/storage/S3StorageController.ts"],"sourcesContent":["/**\n * S3-compatible storage controller (works with AWS S3 and MinIO)\n */\n\nimport type { S3Client as S3ClientType, } from \"@aws-sdk/client-s3\";\nimport {\n DEFAULT_MAX_FILE_SIZE,\n S3StorageConfig,\n StorageController,\n type StoredObjectInfo,\n type StoredObjectRange,\n type PutObjectOptions,\n type StoredObjectStream\n} from \"./types\";\nimport { folderKey, listingPrefix } from \"./keys\";\nimport { webStreamFrom } from \"./streams\";\nimport { ApiError } from \"../api/errors\";\nimport { validateUploadedFile } from \"./upload-validation\";\nimport {\n DownloadConfig,\n DownloadMetadata,\n StorageListResult,\n StorageReference,\n UploadFileProps,\n UploadFileResult\n} from \"@rebasepro/types\";\n\nlet _s3Module: typeof import(\"@aws-sdk/client-s3\") | undefined;\nlet _presignerModule: typeof import(\"@aws-sdk/s3-request-presigner\") | undefined;\n\nasync function loadS3() {\n if (!_s3Module) {\n try {\n _s3Module = await import(\"@aws-sdk/client-s3\");\n } catch {\n throw new Error(\n \"@aws-sdk/client-s3 is required for S3 storage. \" +\n \"Install it: pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner\"\n );\n }\n }\n return _s3Module;\n}\n\nasync function loadPresigner() {\n if (!_presignerModule) {\n try {\n _presignerModule = await import(\"@aws-sdk/s3-request-presigner\");\n } catch {\n throw new Error(\n \"@aws-sdk/s3-request-presigner is required for S3 storage. \" +\n \"Install it: pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner\"\n );\n }\n }\n return _presignerModule;\n}\n\n/** Files up to this size are one `PutObject`; larger ones are uploaded in parts of (at least) this size. */\nconst MULTIPART_PART_SIZE = 8 * 1024 * 1024;\n/** S3's limit on the parts of one upload. */\nconst MAX_MULTIPART_PARTS = 10_000;\n\n/**\n * S3-compatible storage implementation\n * Works with AWS S3 and MinIO (with forcePathStyle option)\n */\nexport class S3StorageController implements StorageController {\n private config: S3StorageConfig;\n private _client: S3ClientType | undefined;\n\n constructor(config: S3StorageConfig) {\n this.config = config;\n }\n\n /**\n * Lazily create and cache the S3 client on first use\n */\n private async getClient(): Promise<S3ClientType> {\n if (!this._client) {\n const s3 = await loadS3();\n this._client = new s3.S3Client({\n region: this.config.region || \"us-east-1\",\n endpoint: this.config.endpoint,\n forcePathStyle: this.config.forcePathStyle ?? !!this.config.endpoint, // Auto-enable for custom endpoints (MinIO)\n credentials: {\n accessKeyId: this.config.accessKeyId,\n secretAccessKey: this.config.secretAccessKey\n }\n });\n }\n return this._client;\n }\n\n getType(): \"s3\" {\n return \"s3\";\n }\n\n /**\n * The configured bucket, and the logical `default` that maps to it.\n *\n * Any other name went straight to S3 as a bucket name, so the request\n * parameter was a way to address *any* bucket the deployment's credentials\n * can reach — and a mistyped one came back as \"file not found\".\n */\n knownBuckets(): string[] {\n return [\"default\", this.config.bucket];\n }\n\n /** See {@link StorageController.maxFileSize}. */\n maxFileSize(): number {\n return this.config.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;\n }\n\n /**\n * Validate file before upload\n */\n private validateFile(file: File): void {\n validateUploadedFile(file, this.maxFileSize(), this.config.allowedMimeTypes);\n }\n\n /**\n * Get the bucket name - either from parameter or config\n */\n private getBucket(bucket?: string): string {\n // \"default\" is a logical bucket name used by local storage;\n // for S3 it should resolve to the configured bucket.\n if (!bucket || bucket === \"default\") return this.config.bucket;\n return bucket;\n }\n\n async putObject({\n file,\n key,\n metadata,\n bucket\n }: UploadFileProps, options?: PutObjectOptions): Promise<UploadFileResult> {\n if (options?.uploadLimits !== false) this.validateFile(file);\n\n const usedBucket = this.getBucket(bucket);\n const s3 = await loadS3();\n const client = await this.getClient();\n const target = {\n Bucket: usedBucket,\n Key: key,\n ContentType: file.type,\n Metadata: metadata ? this.flattenMetadata(metadata) : undefined\n };\n\n if (file.size <= MULTIPART_PART_SIZE) {\n // One request from one buffer, as always: a small file is no bigger\n // than a part. `Buffer.from(arrayBuffer)` is a view, not a copy.\n await client.send(new s3.PutObjectCommand({ ...target, Body: Buffer.from(await file.arrayBuffer()) }))\n .catch((error: unknown) => { throw misconfiguredOr(error, usedBucket); });\n } else {\n await this.putMultipart(target, file);\n }\n\n return {\n key,\n bucket: usedBucket,\n storageUrl: `s3://${usedBucket}/${key}`\n };\n }\n\n /**\n * Upload a large file in parts, one part in memory at a time.\n *\n * Reading the whole file into one buffer held it twice over (the\n * `arrayBuffer()`, then the SDK's view and the multipart parser's copy\n * before it), and `PutObject` stops at 5 GB regardless. A slice of a\n * `File` reads only that range — from disk, for the disk-backed files TUS\n * and backups hand over — so memory is one part whatever the file's size.\n * A failure aborts the upload: no partial object appears at the key, and\n * no orphaned parts are kept (and billed).\n */\n private async putMultipart(\n target: { Bucket: string; Key: string; ContentType: string; Metadata?: Record<string, string> },\n file: Blob\n ): Promise<void> {\n const s3 = await loadS3();\n const client = await this.getClient();\n // At most 10,000 parts: a part grows past 8 MiB only beyond 80 GB.\n const partSize = Math.max(MULTIPART_PART_SIZE, Math.ceil(file.size / MAX_MULTIPART_PARTS));\n const { UploadId } = await client.send(new s3.CreateMultipartUploadCommand(target))\n .catch((error: unknown) => { throw misconfiguredOr(error, target.Bucket); });\n if (!UploadId) throw new Error(`S3 did not start a multipart upload for ${target.Key}.`);\n const { Bucket, Key } = target;\n try {\n const parts: { ETag?: string; PartNumber: number }[] = [];\n for (let offset = 0, PartNumber = 1; offset < file.size; offset += partSize, PartNumber++) {\n const Body = Buffer.from(await file.slice(offset, Math.min(offset + partSize, file.size)).arrayBuffer());\n const { ETag } = await client.send(new s3.UploadPartCommand({ Bucket, Key, UploadId, PartNumber, Body }));\n parts.push({ ETag, PartNumber });\n }\n await client.send(new s3.CompleteMultipartUploadCommand({ Bucket, Key, UploadId, MultipartUpload: { Parts: parts } }));\n } catch (error: unknown) {\n await client.send(new s3.AbortMultipartUploadCommand({ Bucket, Key, UploadId })).catch(() => undefined);\n throw misconfiguredOr(error, Bucket);\n }\n }\n\n /**\n * Flatten nested metadata to string values (S3 requirement)\n */\n private flattenMetadata(metadata: Record<string, unknown>): Record<string, string> {\n const flattened: Record<string, string> = {};\n for (const [key, value] of Object.entries(metadata)) {\n if (typeof value === \"string\") {\n flattened[key] = value;\n } else if (value !== undefined && value !== null) {\n flattened[key] = JSON.stringify(value);\n }\n }\n return flattened;\n }\n\n async getSignedUrl(key: string, bucket?: string): Promise<DownloadConfig> {\n // Handle s3:// and gs:// URLs for backward compatibility\n let resolvedPath = key;\n let resolvedBucket = this.getBucket(bucket);\n\n const match = key.match(/^(s3|gs):\\/\\//);\n if (match) {\n const protocolLength = match[0].length;\n const withoutProtocol = key.substring(protocolLength);\n const firstSlash = withoutProtocol.indexOf(\"/\");\n if (firstSlash > 0) {\n resolvedBucket = withoutProtocol.substring(0, firstSlash);\n resolvedPath = withoutProtocol.substring(firstSlash + 1);\n }\n }\n\n try {\n const s3 = await loadS3();\n const presigner = await loadPresigner();\n const client = await this.getClient();\n\n // First check if the object exists and get metadata\n const headCommand = new s3.HeadObjectCommand({\n Bucket: resolvedBucket,\n Key: resolvedPath\n });\n\n const headResult = await client.send(headCommand);\n\n // Generate a signed URL\n const getCommand = new s3.GetObjectCommand({\n Bucket: resolvedBucket,\n Key: resolvedPath\n });\n\n // Good for an hour. Server-side callers only: no route hands it out.\n const url = await presigner.getSignedUrl(client, getCommand, { expiresIn: 3600 });\n\n const metadata: DownloadMetadata = {\n bucket: resolvedBucket,\n fullPath: resolvedPath,\n name: resolvedPath.split(\"/\").pop() || resolvedPath,\n size: headResult.ContentLength || 0,\n contentType: headResult.ContentType || \"application/octet-stream\",\n customMetadata: headResult.Metadata || {}\n };\n\n return {\n url,\n metadata\n };\n } catch (error: unknown) {\n if (isS3NotFound(error)) {\n return {\n url: null,\n fileNotFound: true\n };\n }\n throw misconfiguredOr(error, resolvedBucket);\n }\n }\n\n async getObject(key: string, bucket?: string): Promise<File | null> {\n // Handle s3:// and gs:// URLs\n let resolvedPath = key;\n let resolvedBucket = this.getBucket(bucket);\n\n const match = key.match(/^(s3|gs):\\/\\//);\n if (match) {\n const protocolLength = match[0].length;\n const withoutProtocol = key.substring(protocolLength);\n const firstSlash = withoutProtocol.indexOf(\"/\");\n if (firstSlash > 0) {\n resolvedBucket = withoutProtocol.substring(0, firstSlash);\n resolvedPath = withoutProtocol.substring(firstSlash + 1);\n }\n }\n\n try {\n const s3 = await loadS3();\n const client = await this.getClient();\n\n const command = new s3.GetObjectCommand({\n Bucket: resolvedBucket,\n Key: resolvedPath\n });\n\n const response = await client.send(command);\n\n if (!response.Body) {\n return null;\n }\n\n // Convert stream to buffer\n const chunks: Uint8Array[] = [];\n for await (const chunk of response.Body as AsyncIterable<Uint8Array>) {\n chunks.push(chunk);\n }\n const buffer = Buffer.concat(chunks);\n\n const contentType = response.ContentType || \"application/octet-stream\";\n const fileName = resolvedPath.split(\"/\").pop() || resolvedPath;\n\n const blob = new Blob([buffer], { type: contentType });\n // The object's own version. Left out, `File` stamps `Date.now()`,\n // and the routes build the `ETag`, `Last-Modified` and the\n // transform cache key from this — so every read was a new version:\n // no 304, no cache hit, and a fresh rendition written per request.\n return new File([blob], fileName, {\n type: contentType,\n lastModified: response.LastModified?.getTime()\n });\n } catch (error: unknown) {\n if (isS3NotFound(error)) {\n return null;\n }\n throw misconfiguredOr(error, resolvedBucket);\n }\n }\n\n /**\n * The bucket and key a call addresses: an `s3://` or `gs://` URL names its\n * own bucket, anything else is a key in the given (or configured) bucket.\n */\n private resolveLocation(key: string, bucket?: string): { bucket: string; key: string } {\n const match = key.match(/^(s3|gs):\\/\\//);\n if (match) {\n const withoutProtocol = key.substring(match[0].length);\n const firstSlash = withoutProtocol.indexOf(\"/\");\n if (firstSlash > 0) {\n return { bucket: withoutProtocol.substring(0, firstSlash), key: withoutProtocol.substring(firstSlash + 1) };\n }\n }\n return { bucket: this.getBucket(bucket), key };\n }\n\n /**\n * See {@link StorageController.getMetadata}. One `HeadObject`. Without it,\n * `GET /metadata/*` fell back to `getSignedUrl`, which signed a URL per\n * call — ~134 µs of CPU — only for the route to throw it away.\n */\n async getMetadata(key: string, bucket?: string): Promise<DownloadMetadata | null> {\n const location = this.resolveLocation(key, bucket);\n const s3 = await loadS3();\n const client = await this.getClient();\n try {\n const head = await client.send(new s3.HeadObjectCommand({ Bucket: location.bucket, Key: location.key }));\n return {\n bucket: location.bucket,\n fullPath: location.key,\n name: location.key.split(\"/\").pop() || location.key,\n size: head.ContentLength || 0,\n contentType: head.ContentType || \"application/octet-stream\",\n customMetadata: head.Metadata || {}\n };\n } catch (error: unknown) {\n if (isS3NotFound(error)) return null;\n throw misconfiguredOr(error, location.bucket);\n }\n }\n\n /** See {@link StorageController.statObject}. One `HeadObject`; no body is read. */\n async statObject(key: string, bucket?: string): Promise<StoredObjectInfo | null> {\n const location = this.resolveLocation(key, bucket);\n const s3 = await loadS3();\n const client = await this.getClient();\n try {\n const head = await client.send(new s3.HeadObjectCommand({ Bucket: location.bucket, Key: location.key }));\n return {\n size: head.ContentLength ?? 0,\n contentType: head.ContentType || \"application/octet-stream\",\n // The object's version — see `getObject` on why a missing one\n // must not become a stable value.\n lastModified: head.LastModified?.getTime() ?? Date.now()\n };\n } catch (error: unknown) {\n if (isS3NotFound(error)) return null;\n throw misconfiguredOr(error, location.bucket);\n }\n }\n\n /**\n * See {@link StorageController.getObjectStream}. One `GetObject`, with a\n * `Range` when one is asked for, so only those bytes leave the bucket; the\n * body is handed on as it arrives rather than collected first.\n */\n async getObjectStream(\n key: string,\n options?: { bucket?: string; range?: StoredObjectRange }\n ): Promise<StoredObjectStream | null> {\n const location = this.resolveLocation(key, options?.bucket);\n const range = options?.range;\n const s3 = await loadS3();\n const client = await this.getClient();\n let response;\n try {\n response = await client.send(new s3.GetObjectCommand({\n Bucket: location.bucket,\n Key: location.key,\n ...(range ? { Range: `bytes=${range.start}-${range.end}` } : {})\n }));\n } catch (error: unknown) {\n if (isS3NotFound(error)) return null;\n // The object shrank below the range since it was described. Read\n // it whole: the caller serves a whole object as a 200, which HTTP\n // allows for any range request.\n if (range && isS3RangeNotSatisfiable(error)) return this.getObjectStream(key, { bucket: options?.bucket });\n throw misconfiguredOr(error, location.bucket);\n }\n const body = response.Body;\n if (!body) return null;\n\n // A ranged answer says the whole object's size after the slash of its\n // `Content-Range`. A store that ignored the `Range` sent no\n // `Content-Range` and the whole object, which is what this then says.\n const total = range ? /\\/(\\d+)\\s*$/.exec(response.ContentRange ?? \"\")?.[1] : undefined;\n const served = range && total !== undefined ? range : undefined;\n return {\n stream: isByteIterable(body) ? webStreamFrom(body) : body.transformToWebStream(),\n size: total !== undefined ? Number(total) : response.ContentLength ?? 0,\n contentType: response.ContentType || \"application/octet-stream\",\n lastModified: response.LastModified?.getTime() ?? Date.now(),\n ...(served ? { range: served } : {})\n };\n }\n\n async deleteObject(key: string, bucket?: string): Promise<void> {\n // Handle s3:// and gs:// URLs\n let resolvedPath = key;\n let resolvedBucket = this.getBucket(bucket);\n\n const match = key.match(/^(s3|gs):\\/\\//);\n if (match) {\n const protocolLength = match[0].length;\n const withoutProtocol = key.substring(protocolLength);\n const firstSlash = withoutProtocol.indexOf(\"/\");\n if (firstSlash > 0) {\n resolvedBucket = withoutProtocol.substring(0, firstSlash);\n resolvedPath = withoutProtocol.substring(firstSlash + 1);\n }\n }\n\n const s3 = await loadS3();\n const client = await this.getClient();\n\n const command = new s3.DeleteObjectCommand({\n Bucket: resolvedBucket,\n Key: resolvedPath\n });\n\n // Deleting something that is already gone is the outcome the caller\n // wanted, so it is not an error. AWS itself answers DeleteObject with\n // 204 for a key that never existed, but S3-compatible backends (MinIO,\n // Ceph, R2 in some configurations) do raise NoSuchKey — and the local\n // controller has always returned quietly in this case. Without this,\n // which of those a deployment happens to run decides whether\n // `DELETE /api/storage/:path` on a stale reference is a 200 or a 500.\n try {\n await client.send(command);\n } catch (error: unknown) {\n if (isS3NotFound(error)) {\n return;\n }\n throw misconfiguredOr(error, resolvedBucket);\n }\n }\n\n async listObjects(prefix: string, options?: {\n bucket?: string;\n maxResults?: number;\n pageToken?: string;\n }): Promise<StorageListResult> {\n const resolvedBucket = this.getBucket(options?.bucket);\n\n const s3 = await loadS3();\n const client = await this.getClient();\n\n const folderPrefix = listingPrefix(prefix);\n const command = new s3.ListObjectsV2Command({\n Bucket: resolvedBucket,\n Prefix: folderPrefix,\n MaxKeys: options?.maxResults ?? 1000,\n ContinuationToken: options?.pageToken,\n Delimiter: \"/\" // This gives us folder-like behavior\n });\n\n const response = await client.send(command)\n .catch((error: unknown) => { throw misconfiguredOr(error, resolvedBucket); });\n\n // The folder's own marker — the zero-byte `folder/` object that\n // `POST /folder` writes — is in `Contents` when the listed prefix is\n // that folder. It is how the folder exists, not something in it, and\n // listed it is an entry with an empty name. Local storage has no such\n // object to list.\n const objects = (response.Contents || []).filter(obj => obj.Key !== folderPrefix);\n const items: StorageReference[] = objects.map(obj => ({\n bucket: resolvedBucket,\n fullPath: obj.Key || \"\",\n name: (obj.Key || \"\").split(\"/\").pop() || \"\",\n // What the listing already carries, so a folder can be shown\n // without a HEAD per file. ListObjectsV2 has no content type.\n ...(typeof obj.Size === \"number\" ? { size: obj.Size } : {}),\n ...(obj.LastModified instanceof Date ? { updated: obj.LastModified.toISOString() } : {}),\n parent: null as never,\n root: null as never,\n toString: () => `s3://${resolvedBucket}/${obj.Key}`\n }));\n\n const prefixes: StorageReference[] = (response.CommonPrefixes || []).map(common => {\n // Without the trailing slash S3 puts on a common prefix, so a\n // `fullPath` from a listing is a key any controller accepts.\n const folder = folderKey(common.Prefix || \"\");\n return {\n bucket: resolvedBucket,\n fullPath: folder,\n name: folder.split(\"/\").pop() || \"\",\n parent: null as never,\n root: null as never,\n toString: () => `s3://${resolvedBucket}/${folder}`\n };\n });\n\n return {\n items,\n prefixes,\n nextPageToken: response.NextContinuationToken\n };\n }\n}\n\n/** The error name and HTTP status an SDK error carries, read without assuming its shape. */\nfunction s3ErrorFacts(error: unknown): { name?: string; status?: number } {\n if (typeof error !== \"object\" || error === null) return {};\n const name = \"name\" in error && typeof error.name === \"string\" ? error.name : undefined;\n const meta = \"$metadata\" in error ? error.$metadata : undefined;\n const status = typeof meta === \"object\" && meta !== null && \"httpStatusCode\" in meta && typeof meta.httpStatusCode === \"number\"\n ? meta.httpStatusCode\n : undefined;\n return { name, status };\n}\n\n/**\n * A missing object, as `GetObject` (`NoSuchKey`) and `HeadObject` (`NotFound`, no body) report it.\n *\n * Not a missing bucket, which is a 404 too: read as a missing object, a typo in\n * `S3_BUCKET` made every file of the deployment \"not found\", and a delete\n * report success. (A `HeadObject` on a missing bucket carries no error name, so\n * that one call still cannot tell the two apart.)\n */\nfunction isS3NotFound(error: unknown): boolean {\n const { name, status } = s3ErrorFacts(error);\n if (name === \"NoSuchBucket\") return false;\n return name === \"NoSuchKey\" || name === \"NotFound\" || status === 404;\n}\n\n/** What S3 answers when the deployment's configuration, not the request, is wrong. */\nconst MISCONFIGURATION_ERRORS = new Set([\"NoSuchBucket\", \"AccessDenied\", \"InvalidAccessKeyId\", \"SignatureDoesNotMatch\"]);\n\n/**\n * A provider error that says this deployment's storage is misconfigured, as a\n * `502 STORAGE_MISCONFIGURED` naming the bucket — or the error as it was.\n *\n * These used to reach the caller as a bare 500 \"Internal Server Error\", with\n * nothing to say the bucket or its credentials were the problem.\n */\nfunction misconfiguredOr(error: unknown, bucket: string): unknown {\n const { name } = s3ErrorFacts(error);\n if (!name || !MISCONFIGURATION_ERRORS.has(name)) return error;\n return new ApiError(\n 502,\n \"STORAGE_MISCONFIGURED\",\n name === \"NoSuchBucket\"\n ? `The storage bucket \"${bucket}\" does not exist. Check the bucket name this source is configured with (S3_BUCKET, or S3_BUCKET__<KEY> for a named source).`\n : `The storage provider refused this deployment's credentials for bucket \"${bucket}\" (${name}). Check the access key and its permissions on the bucket.`,\n { bucket, reason: name }\n );\n}\n\nfunction isS3RangeNotSatisfiable(error: unknown): boolean {\n const { name, status } = s3ErrorFacts(error);\n return name === \"InvalidRange\" || status === 416;\n}\n\n/** A Node response body — what the SDK hands over on Node — rather than a browser one. */\nfunction isByteIterable(value: unknown): value is AsyncIterable<Uint8Array> {\n return typeof value === \"object\" && value !== null && Symbol.asyncIterator in value;\n}\n"],"mappings":"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"}
@@ -0,0 +1,79 @@
1
+ import { createRequire as __rebaseCreateRequire } from "module";
2
+ import __rebaseProcess from "process";
3
+ globalThis.process ??= __rebaseProcess;
4
+ __rebaseCreateRequire(import.meta.url);
5
+ //#region src/auth/admin-roles.ts
6
+ /**
7
+ * The admin role and scope matching, for code that may not import
8
+ * `@rebasepro/types`.
9
+ *
10
+ * The custom-functions surface (`@rebasepro/server/functions`) must bundle for
11
+ * a runtime with no Node built-ins and imports nothing but `hono`, so its
12
+ * guards cannot reach the canonical definitions in `@rebasepro/types`
13
+ * (`ADMIN_ROLE`, `hasAdminRole`, `scopeGrants`). These are the same rules,
14
+ * restated, and `test/admin-roles.test.ts` holds them equal.
15
+ *
16
+ * Everything else imports the `@rebasepro/types` versions — except
17
+ * {@link scopeGuardProblem}, which both `requireScope` guards ask, so the root
18
+ * one and the functions one refuse the same scope names in the same words.
19
+ */
20
+ /** The one built-in role. It holds every scope. */
21
+ var ADMIN_ROLE_NAME = "admin";
22
+ /** Does this list of roles include the admin role? */
23
+ function holdsAdminRole(roles) {
24
+ return !!roles?.includes(ADMIN_ROLE_NAME);
25
+ }
26
+ /** Every scope Rebase defines (`BUILT_IN_SCOPES`), and what each takes as a target (`SCOPE_TARGETS`). */
27
+ var BUILT_IN_SCOPE_TARGETS = {
28
+ "data:read": true,
29
+ "data:write": true,
30
+ "data:delete": true,
31
+ "storage:read": true,
32
+ "storage:write": true,
33
+ "storage:delete": true,
34
+ "functions:invoke": true,
35
+ "users:read": false,
36
+ "users:write": false,
37
+ "schema:read": false,
38
+ "schema:write": false,
39
+ "database:read": false,
40
+ "database:write": false,
41
+ "backups:read": false,
42
+ "cron:read": false,
43
+ "cron:write": false,
44
+ "logs:read": false,
45
+ "keys:read": false,
46
+ "keys:write": false
47
+ };
48
+ /** Every scope Rebase defines, in `BUILT_IN_SCOPES` order. */
49
+ var BUILT_IN_SCOPE_NAMES = Object.keys(BUILT_IN_SCOPE_TARGETS);
50
+ var SCOPE_PART = /^[a-z][a-z0-9-]*$/;
51
+ /**
52
+ * Why a guard naming `scope` (and `target`, when it is fixed) could never
53
+ * pass, or null when it could.
54
+ *
55
+ * Without `declared`, it judges what holds on every backend, so a guard can be
56
+ * refused as it is registered: the name's shape, a target written into the
57
+ * scope instead of passed beside it, a name under one of Rebase's own
58
+ * resources that is not one of its scopes — the typo `data:raed` — and a
59
+ * target on a built-in scope that takes none. With `declared`, the app scopes
60
+ * the backend answering the request declares (`auth.scopes`), it also judges
61
+ * an app scope, which nobody holds unless it is declared.
62
+ */
63
+ function scopeGuardProblem(scope, target, declared) {
64
+ const call = target === void 0 ? `requireScope("${scope}")` : `requireScope("${scope}", "${target}")`;
65
+ const parts = typeof scope === "string" ? scope.split(":") : [];
66
+ if (parts.length < 2 || !SCOPE_PART.test(parts[0]) || !SCOPE_PART.test(parts[1])) return `${call}: "${scope}" is not a scope name. Name a scope resource:action, lower-case — "users:read", or one the app declares, such as "project:deploy".`;
67
+ const name = `${parts[0]}:${parts[1]}`;
68
+ if (parts.length > 2) return `${call} writes a target into the scope. Pass it as the second argument: requireScope("${name}", "${parts.slice(2).join(":")}").`;
69
+ const builtIn = Object.prototype.hasOwnProperty.call(BUILT_IN_SCOPE_TARGETS, name);
70
+ const resourceScopes = BUILT_IN_SCOPE_NAMES.filter((known) => known.startsWith(`${parts[0]}:`));
71
+ if (!builtIn && resourceScopes.length > 0) return `${call}: "${name}" is not one of Rebase's scopes, so nobody could hold it. The ${parts[0]} scopes are ${resourceScopes.join(", ")}.`;
72
+ if (builtIn && target !== void 0 && !BUILT_IN_SCOPE_TARGETS[name]) return `${call}: "${name}" takes no target. Guard with requireScope("${name}").`;
73
+ if (!builtIn && declared && !declared.includes(name)) return `${call}: this backend declares no "${name}" scope, so nobody holds it — an administrator neither. Declare it under auth.scopes on the users collection: "${name}": { label: "…" }.`;
74
+ return null;
75
+ }
76
+ //#endregion
77
+ export { scopeGuardProblem as n, holdsAdminRole as t };
78
+
79
+ //# sourceMappingURL=admin-roles-DI9RsgN3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin-roles-DI9RsgN3.js","names":[],"sources":["../src/auth/admin-roles.ts"],"sourcesContent":["/**\n * The admin role and scope matching, for code that may not import\n * `@rebasepro/types`.\n *\n * The custom-functions surface (`@rebasepro/server/functions`) must bundle for\n * a runtime with no Node built-ins and imports nothing but `hono`, so its\n * guards cannot reach the canonical definitions in `@rebasepro/types`\n * (`ADMIN_ROLE`, `hasAdminRole`, `scopeGrants`). These are the same rules,\n * restated, and `test/admin-roles.test.ts` holds them equal.\n *\n * Everything else imports the `@rebasepro/types` versions — except\n * {@link scopeGuardProblem}, which both `requireScope` guards ask, so the root\n * one and the functions one refuse the same scope names in the same words.\n */\n\n/** The one built-in role. It holds every scope. */\nexport const ADMIN_ROLE_NAME = \"admin\";\n\n/** Does this list of roles include the admin role? */\nexport function holdsAdminRole(roles: readonly string[] | null | undefined): boolean {\n return !!roles?.includes(ADMIN_ROLE_NAME);\n}\n\n/**\n * Does a set of held scopes grant `scope`, optionally on one `target`? The\n * unqualified grant covers every target; `scope:target` covers its own.\n */\nexport function heldScopesGrant(held: readonly string[], scope: string, target?: string): boolean {\n for (const entry of held) {\n if (entry === scope) return true;\n if (target !== undefined && entry === `${scope}:${target}`) return true;\n }\n return false;\n}\n\n/** Every scope Rebase defines (`BUILT_IN_SCOPES`), and what each takes as a target (`SCOPE_TARGETS`). */\nconst BUILT_IN_SCOPE_TARGETS: Readonly<Record<string, boolean>> = {\n \"data:read\": true,\n \"data:write\": true,\n \"data:delete\": true,\n \"storage:read\": true,\n \"storage:write\": true,\n \"storage:delete\": true,\n \"functions:invoke\": true,\n \"users:read\": false,\n \"users:write\": false,\n \"schema:read\": false,\n \"schema:write\": false,\n \"database:read\": false,\n \"database:write\": false,\n \"backups:read\": false,\n \"cron:read\": false,\n \"cron:write\": false,\n \"logs:read\": false,\n \"keys:read\": false,\n \"keys:write\": false\n};\n\n/** Every scope Rebase defines, in `BUILT_IN_SCOPES` order. */\nexport const BUILT_IN_SCOPE_NAMES: readonly string[] = Object.keys(BUILT_IN_SCOPE_TARGETS);\n\nconst SCOPE_PART = /^[a-z][a-z0-9-]*$/;\n\n/**\n * Why a guard naming `scope` (and `target`, when it is fixed) could never\n * pass, or null when it could.\n *\n * Without `declared`, it judges what holds on every backend, so a guard can be\n * refused as it is registered: the name's shape, a target written into the\n * scope instead of passed beside it, a name under one of Rebase's own\n * resources that is not one of its scopes — the typo `data:raed` — and a\n * target on a built-in scope that takes none. With `declared`, the app scopes\n * the backend answering the request declares (`auth.scopes`), it also judges\n * an app scope, which nobody holds unless it is declared.\n */\nexport function scopeGuardProblem(scope: string, target?: string, declared?: readonly string[]): string | null {\n const call = target === undefined ? `requireScope(\"${scope}\")` : `requireScope(\"${scope}\", \"${target}\")`;\n const parts = typeof scope === \"string\" ? scope.split(\":\") : [];\n if (parts.length < 2 || !SCOPE_PART.test(parts[0]) || !SCOPE_PART.test(parts[1])) {\n return `${call}: \"${scope}\" is not a scope name. Name a scope resource:action, lower-case — ` +\n \"\\\"users:read\\\", or one the app declares, such as \\\"project:deploy\\\".\";\n }\n const name = `${parts[0]}:${parts[1]}`;\n if (parts.length > 2) {\n return `${call} writes a target into the scope. Pass it as the second argument: ` +\n `requireScope(\"${name}\", \"${parts.slice(2).join(\":\")}\").`;\n }\n const builtIn = Object.prototype.hasOwnProperty.call(BUILT_IN_SCOPE_TARGETS, name);\n const resourceScopes = BUILT_IN_SCOPE_NAMES.filter(known => known.startsWith(`${parts[0]}:`));\n if (!builtIn && resourceScopes.length > 0) {\n return `${call}: \"${name}\" is not one of Rebase's scopes, so nobody could hold it. ` +\n `The ${parts[0]} scopes are ${resourceScopes.join(\", \")}.`;\n }\n if (builtIn && target !== undefined && !BUILT_IN_SCOPE_TARGETS[name]) {\n return `${call}: \"${name}\" takes no target. Guard with requireScope(\"${name}\").`;\n }\n if (!builtIn && declared && !declared.includes(name)) {\n return `${call}: this backend declares no \"${name}\" scope, so nobody holds it — an administrator neither. ` +\n `Declare it under auth.scopes on the users collection: \"${name}\": { label: \"…\" }.`;\n }\n return null;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAgBA,IAAa,kBAAkB;;AAG/B,SAAgB,eAAe,OAAsD;CACjF,OAAO,CAAC,CAAC,OAAO,SAAS,eAAe;AAC5C;;AAeA,IAAM,yBAA4D;CAC9D,aAAa;CACb,cAAc;CACd,eAAe;CACf,gBAAgB;CAChB,iBAAiB;CACjB,kBAAkB;CAClB,oBAAoB;CACpB,cAAc;CACd,eAAe;CACf,eAAe;CACf,gBAAgB;CAChB,iBAAiB;CACjB,kBAAkB;CAClB,gBAAgB;CAChB,aAAa;CACb,cAAc;CACd,aAAa;CACb,aAAa;CACb,cAAc;AAClB;;AAGA,IAAa,uBAA0C,OAAO,KAAK,sBAAsB;AAEzF,IAAM,aAAa;;;;;;;;;;;;;AAcnB,SAAgB,kBAAkB,OAAe,QAAiB,UAA6C;CAC3G,MAAM,OAAO,WAAW,KAAA,IAAY,iBAAiB,MAAM,MAAM,iBAAiB,MAAM,MAAM,OAAO;CACrG,MAAM,QAAQ,OAAO,UAAU,WAAW,MAAM,MAAM,GAAG,IAAI,CAAC;CAC9D,IAAI,MAAM,SAAS,KAAK,CAAC,WAAW,KAAK,MAAM,EAAE,KAAK,CAAC,WAAW,KAAK,MAAM,EAAE,GAC3E,OAAO,GAAG,KAAK,KAAK,MAAM;CAG9B,MAAM,OAAO,GAAG,MAAM,GAAG,GAAG,MAAM;CAClC,IAAI,MAAM,SAAS,GACf,OAAO,GAAG,KAAK,iFACM,KAAK,MAAM,MAAM,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE;CAE7D,MAAM,UAAU,OAAO,UAAU,eAAe,KAAK,wBAAwB,IAAI;CACjF,MAAM,iBAAiB,qBAAqB,QAAO,UAAS,MAAM,WAAW,GAAG,MAAM,GAAG,EAAE,CAAC;CAC5F,IAAI,CAAC,WAAW,eAAe,SAAS,GACpC,OAAO,GAAG,KAAK,KAAK,KAAK,gEACd,MAAM,GAAG,cAAc,eAAe,KAAK,IAAI,EAAE;CAEhE,IAAI,WAAW,WAAW,KAAA,KAAa,CAAC,uBAAuB,OAC3D,OAAO,GAAG,KAAK,KAAK,KAAK,8CAA8C,KAAK;CAEhF,IAAI,CAAC,WAAW,YAAY,CAAC,SAAS,SAAS,IAAI,GAC/C,OAAO,GAAG,KAAK,8BAA8B,KAAK,iHACY,KAAK;CAEvE,OAAO;AACX"}
@@ -32,6 +32,29 @@ export declare function schemaDriftRemedy(): {
32
32
  * nobody has does.
33
33
  */
34
34
  export declare function isDataException(error: unknown): boolean;
35
+ /**
36
+ * Did the request fail because the database could not be reached at all — not
37
+ * because it refused the statement, and not because some other service is down?
38
+ *
39
+ * Read off the error the database driver raised, wherever it sits in the cause
40
+ * chain:
41
+ *
42
+ * - a SQLSTATE that means the connection was refused or dropped (class `08`,
43
+ * `57P01`–`57P03`). The database's own word, so it settles the question
44
+ * either way: a SQLSTATE of any other class is the database answering;
45
+ * - node-postgres's own sentence for a socket that closed under a query;
46
+ * - a socket failure (`ECONNREFUSED`, `ENOTFOUND`, …), but only where it can
47
+ * only be the database's: thrown as it is, which is how node-postgres hands
48
+ * back a pool connection it could not open, or as the cause of a failed
49
+ * Drizzle query (the wrapper that carries the statement). Under any other
50
+ * wrapper it belongs to another client: `fetch failed` is a `fetch()` to
51
+ * some other host, and an S3 call that cannot connect carries the AWS SDK's
52
+ * `$metadata`. Answering those "the database is unreachable" would send an
53
+ * operator to a database that is fine.
54
+ */
55
+ export declare function isDatabaseUnreachable(error: unknown): boolean;
56
+ /** What a caller is told when the database cannot be reached. Retryable, and not about them. */
57
+ export declare const DATABASE_UNAVAILABLE_MESSAGE: string;
35
58
  /**
36
59
  * Standardized API error class.
37
60
  * Throw this from any route handler — the errorHandler middleware
@@ -81,6 +104,15 @@ export declare class ApiError extends Error {
81
104
  static conflict(message: string, code?: string, details?: unknown): ApiError;
82
105
  static internal(message: string, code?: string): ApiError;
83
106
  static serviceUnavailable(message: string, code?: string): ApiError;
107
+ /**
108
+ * `503 DATABASE_UNAVAILABLE`: the database could not be reached. What
109
+ * every door answers then — the data API, sign-in, the admin routes and
110
+ * the realtime socket — so a client sees one retryable answer, sent with
111
+ * `Retry-After`, rather than a permission failure or a crash.
112
+ *
113
+ * @see isDatabaseUnreachable
114
+ */
115
+ static databaseUnavailable(message?: string): ApiError;
84
116
  }
85
117
  /**
86
118
  * Canonical error response shape:
@@ -29,7 +29,7 @@
29
29
  * — `applySchemaChange` re-checks rather than trusting the plan it is handed.
30
30
  */
31
31
  import { Hono } from "hono";
32
- import { type CollectionPatch, type CollectionConfig, type DatabaseAdmin, type SchemaCommitPaths } from "@rebasepro/types";
32
+ import { type AuthAdapter, type CollectionPatch, type CollectionConfig, type DatabaseAdmin, type SchemaCommitPaths } from "@rebasepro/types";
33
33
  import { HonoEnv } from "./types.js";
34
34
  import { type SchemaEditRepository } from "../schema-edit/apply-schema-change.js";
35
35
  import { type SchemaEditPolicy } from "../schema-edit/schema-edit-permissions.js";
@@ -110,7 +110,27 @@ export interface LiveSchemaRoutesConfig {
110
110
  * commit, a commit needs an author, and a credential is not an author.
111
111
  */
112
112
  policy?: SchemaEditPolicy;
113
+ /**
114
+ * The account behind a uid, for the commit's author line.
115
+ *
116
+ * The request's user is what its access token carries, and the built-in
117
+ * token carries a uid and roles — no name, no address — so without this a
118
+ * commit is authored by the uid. Absent, or answering nothing, the commit
119
+ * falls back to what the request carries.
120
+ */
121
+ resolveAuthorIdentity?: (uid: string) => Promise<AccountIdentity | null>;
122
+ }
123
+ /** What a commit's author line is made of. */
124
+ export interface AccountIdentity {
125
+ displayName?: string | null;
126
+ email?: string | null;
113
127
  }
128
+ /**
129
+ * The account behind a uid, read through the auth in use: its user-management
130
+ * lookup when it has one — a plain read — else `resolveUser`. `null` when the
131
+ * auth has neither, or knows no such account.
132
+ */
133
+ export declare function readAccountIdentity(adapter: Pick<AuthAdapter, "userManagement" | "resolveUser"> | undefined, uid: string): Promise<AccountIdentity | null>;
114
134
  /**
115
135
  * What the panel posts about one collection.
116
136
  *
@@ -143,11 +163,15 @@ export interface ProposedChange {
143
163
  * `undefined` when the request carries no user — a service key, say — which
144
164
  * lets the repository fall back to its own git config rather than inventing a
145
165
  * name for somebody.
166
+ *
167
+ * The name and address the request carries win; what it lacks is read from
168
+ * the account through `resolveAccount`. A lookup that fails costs the commit
169
+ * its name, never the commit: the author falls back to the uid.
146
170
  */
147
- export declare function commitAuthor(user: unknown): {
171
+ export declare function commitAuthor(user: unknown, resolveAccount?: (uid: string) => Promise<AccountIdentity | null>): Promise<{
148
172
  name: string;
149
173
  email: string;
150
- } | undefined;
174
+ } | undefined>;
151
175
  /**
152
176
  * The change as a patch against the collection as it is.
153
177
  *
@@ -81,6 +81,7 @@ export type HonoEnv = {
81
81
  errorSummary?: {
82
82
  code: string;
83
83
  message: string;
84
+ expected?: boolean;
84
85
  };
85
86
  /**
86
87
  * Set by `requestLogger` before the handler runs, so the error handler
@@ -122,13 +122,21 @@ export declare function scopeMissing(c: Context<HonoEnv>, scope: string, target?
122
122
  /**
123
123
  * Refuse a request whose caller does not hold `scope`.
124
124
  *
125
+ * `scope` is a built-in scope or one this backend declares under
126
+ * `auth.scopes`. A name no caller could ever hold — not a scope name, a typo
127
+ * of a built-in one (`data:raed`) — throws as the route is registered; an app
128
+ * scope the backend answering does not declare is answered
129
+ * `500 SCOPE_NOT_DECLARED`, naming the declaration to add.
130
+ *
125
131
  * `target` narrows the check to one collection, bucket, function or app
126
- * resource, and may be read from the request. Mount after authentication: a
127
- * request with no caller is refused as unauthenticated, not as unscoped.
132
+ * resource, and may be read from the request. A guard, not an authenticator:
133
+ * it judges the caller an auth middleware identified, so mount it after one —
134
+ * `requireAuth`, or a route group's own. A request with no caller is refused
135
+ * as unauthenticated, not as unscoped.
128
136
  *
129
137
  * @example
130
138
  * ```ts
131
- * app.post("/deploy/:project", requireScope("project:deploy", c => c.req.param("project")), handler);
139
+ * app.post("/deploy/:project", requireAuth, requireScope("project:deploy", c => c.req.param("project")), handler);
132
140
  * ```
133
141
  */
134
142
  export declare function requireScope(scope: string, target?: string | ((c: Context<HonoEnv>) => string | undefined)): MiddlewareHandler<HonoEnv>;