@rebasepro/server 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/README.md +1 -1
  2. package/bin/rebase-server.js +4 -2
  3. package/dist/{GCSStorageController-CjrA4PMo.js → GCSStorageController-BSiP1c-f.js} +22 -8
  4. package/dist/GCSStorageController-BSiP1c-f.js.map +1 -0
  5. package/dist/{S3StorageController-B6pKDNVj.js → S3StorageController-CAwFRgjV.js} +19 -7
  6. package/dist/S3StorageController-CAwFRgjV.js.map +1 -0
  7. package/dist/api/ast-schema-editor.d.ts +92 -1
  8. package/dist/api/errors.d.ts +9 -0
  9. package/dist/api/live-schema-routes.d.ts +38 -8
  10. package/dist/api/logs-routes.d.ts +39 -1
  11. package/dist/api/openapi-generator.d.ts +17 -0
  12. package/dist/api/rest/api-generator.d.ts +44 -10
  13. package/dist/api/rest/write-validation.d.ts +2 -2
  14. package/dist/api/types.d.ts +17 -1
  15. package/dist/{ast-schema-editor-Mvr50v_S.js → ast-schema-editor-CWqS_sLJ.js} +309 -11
  16. package/dist/ast-schema-editor-CWqS_sLJ.js.map +1 -0
  17. package/dist/auth/access.d.ts +105 -0
  18. package/dist/auth/adapter-middleware.d.ts +2 -1
  19. package/dist/auth/address-ownership.d.ts +16 -1
  20. package/dist/auth/admin-roles-route.d.ts +4 -2
  21. package/dist/auth/admin-roles.d.ts +17 -20
  22. package/dist/auth/admin-users-route.d.ts +1 -0
  23. package/dist/auth/api-keys/api-key-middleware.d.ts +56 -55
  24. package/dist/auth/api-keys/api-key-routes.d.ts +41 -11
  25. package/dist/auth/api-keys/api-key-store.d.ts +31 -8
  26. package/dist/auth/api-keys/api-key-types.d.ts +14 -16
  27. package/dist/auth/api-keys/http-operation.d.ts +19 -0
  28. package/dist/auth/api-keys/index.d.ts +11 -11
  29. package/dist/auth/api-keys/key-grant.d.ts +41 -0
  30. package/dist/auth/api-keys/legacy-permissions.d.ts +33 -0
  31. package/dist/auth/auth-hooks.d.ts +46 -7
  32. package/dist/auth/builtin-auth-adapter.d.ts +8 -0
  33. package/dist/auth/cookie-utils.d.ts +7 -0
  34. package/dist/auth/deliverable-address.d.ts +6 -0
  35. package/dist/auth/email-change-routes.d.ts +41 -0
  36. package/dist/auth/expired-token-sweep.d.ts +67 -0
  37. package/dist/auth/impersonation.d.ts +110 -0
  38. package/dist/auth/index.d.ts +4 -2
  39. package/dist/auth/interfaces.d.ts +110 -59
  40. package/dist/auth/jwt.d.ts +49 -3
  41. package/dist/auth/magic-link-routes.d.ts +2 -6
  42. package/dist/auth/mfa-routes.d.ts +2 -9
  43. package/dist/auth/middleware.d.ts +17 -5
  44. package/dist/auth/otp-routes.d.ts +2 -6
  45. package/dist/auth/passwordless-signup.d.ts +27 -0
  46. package/dist/auth/platform-token.d.ts +122 -0
  47. package/dist/auth/rate-limiter.d.ts +41 -0
  48. package/dist/auth/routes.d.ts +45 -0
  49. package/dist/auth/scope-routes.d.ts +22 -0
  50. package/dist/auth/session-routes.d.ts +11 -6
  51. package/dist/auth/token-revocation.d.ts +50 -1
  52. package/dist/auth/verify-credential.d.ts +28 -0
  53. package/dist/{auth-B-GIMpDG.js → auth-DMLngxn_.js} +2159 -569
  54. package/dist/auth-DMLngxn_.js.map +1 -0
  55. package/dist/backend-DTAOsLQc.js.map +1 -1
  56. package/dist/backup/backup-common.d.ts +10 -0
  57. package/dist/backup/backup-routes.d.ts +24 -4
  58. package/dist/backup/backup-schedule.d.ts +33 -0
  59. package/dist/backup/backup-storage.d.ts +14 -0
  60. package/dist/backup/index.d.ts +2 -0
  61. package/dist/backup-CN0s50D2.js +444 -0
  62. package/dist/backup-CN0s50D2.js.map +1 -0
  63. package/dist/boot/bundle.d.ts +19 -0
  64. package/dist/boot/env.d.ts +49 -4
  65. package/dist/boot/security-headers.d.ts +26 -0
  66. package/dist/boot/static-routing.d.ts +56 -0
  67. package/dist/collection_patch-BRu-BvDv.js +472 -0
  68. package/dist/collection_patch-BRu-BvDv.js.map +1 -0
  69. package/dist/{contract-routes-CbFjuBwa.js → contract-routes-fz8i4pxs.js} +17 -4
  70. package/dist/contract-routes-fz8i4pxs.js.map +1 -0
  71. package/dist/cron/cron-scheduler.d.ts +25 -20
  72. package/dist/cron/cron-store.d.ts +6 -2
  73. package/dist/{cron-loader-DfTj2Hbi.js → cron-loader-CwaANlOG.js} +4 -4
  74. package/dist/cron-loader-CwaANlOG.js.map +1 -0
  75. package/dist/{cron-routes-eE8nif_b.js → cron-routes-Bc-SB0Se.js} +10 -7
  76. package/dist/cron-routes-Bc-SB0Se.js.map +1 -0
  77. package/dist/{cron-scheduler-B0pLfAix.js → cron-scheduler-CYQgco86.js} +52 -34
  78. package/dist/cron-scheduler-CYQgco86.js.map +1 -0
  79. package/dist/{cron-store-TcoGz-xS.js → cron-store-D2Q9-Aco.js} +10 -15
  80. package/dist/cron-store-D2Q9-Aco.js.map +1 -0
  81. package/dist/{ddl-bootstrap-C6mo0Kmz.js → ddl-bootstrap-BaqMSa4Y.js} +2 -2
  82. package/dist/{ddl-bootstrap-C6mo0Kmz.js.map → ddl-bootstrap-BaqMSa4Y.js.map} +1 -1
  83. package/dist/email/index.d.ts +2 -2
  84. package/dist/email/templates.d.ts +22 -0
  85. package/dist/email/types.d.ts +26 -0
  86. package/dist/env.d.ts +1 -2
  87. package/dist/{errors-DWsX4yTd.js → errors-D6_y86c5.js} +102 -8
  88. package/dist/errors-D6_y86c5.js.map +1 -0
  89. package/dist/{function-loader-xnbDAPfa.js → function-loader-D7o5Epjj.js} +2 -2
  90. package/dist/{function-loader-xnbDAPfa.js.map → function-loader-D7o5Epjj.js.map} +1 -1
  91. package/dist/{function-routes-Chet4-lB.js → function-routes-CaNG4waN.js} +24 -12
  92. package/dist/function-routes-CaNG4waN.js.map +1 -0
  93. package/dist/functions/context.d.ts +17 -6
  94. package/dist/functions/guards.d.ts +22 -5
  95. package/dist/functions/index.d.ts +2 -2
  96. package/dist/functions/index.js +90 -36
  97. package/dist/functions/index.js.map +1 -1
  98. package/dist/{history-recorder-B4MpJfJK.js → history-recorder-Nr8zLvoU.js} +4 -4
  99. package/dist/{history-recorder-B4MpJfJK.js.map → history-recorder-Nr8zLvoU.js.map} +1 -1
  100. package/dist/{history-store-BhxWOuz9.js → history-store-rcAm_xFR.js} +2 -2
  101. package/dist/{history-store-BhxWOuz9.js.map → history-store-rcAm_xFR.js.map} +1 -1
  102. package/dist/index.d.ts +8 -2
  103. package/dist/index.es.js +3084 -753
  104. package/dist/index.es.js.map +1 -1
  105. package/dist/init/health.d.ts +17 -2
  106. package/dist/init/shutdown.d.ts +10 -0
  107. package/dist/init.d.ts +54 -0
  108. package/dist/{jobs-CazMYhyy.js → jobs-DqYNfquG.js} +5 -5
  109. package/dist/{jobs-CazMYhyy.js.map → jobs-DqYNfquG.js.map} +1 -1
  110. package/dist/{jwt-DnQHNFCl.js → jwt-R6bSPMjk.js} +39 -15
  111. package/dist/{jwt-DnQHNFCl.js.map → jwt-R6bSPMjk.js.map} +1 -1
  112. package/dist/{keys-CogCQpxG.js → keys-GAVZqbqx.js} +3 -17
  113. package/dist/{keys-CogCQpxG.js.map → keys-GAVZqbqx.js.map} +1 -1
  114. package/dist/{logger-DO2PZc4i.js → logger-D-S-hO5e.js} +26 -3
  115. package/dist/logger-D-S-hO5e.js.map +1 -0
  116. package/dist/{logs-routes-Bj4TYYUl.js → logs-routes-DAdv37GI.js} +48 -8
  117. package/dist/logs-routes-DAdv37GI.js.map +1 -0
  118. package/dist/mcp/consent-page.d.ts +1 -1
  119. package/dist/mcp/mcp-routes.d.ts +7 -0
  120. package/dist/mcp/mcp-tools.d.ts +15 -9
  121. package/dist/mcp/oauth-metadata.d.ts +21 -16
  122. package/dist/mcp/oauth-routes.d.ts +7 -1
  123. package/dist/{openapi-generator-O_O24MAT.js → openapi-generator-DAq_XVDu.js} +104 -13
  124. package/dist/openapi-generator-DAq_XVDu.js.map +1 -0
  125. package/dist/{proxy-Czngl3p9.js → proxy-qRlqeUmO.js} +2 -2
  126. package/dist/{proxy-Czngl3p9.js.map → proxy-qRlqeUmO.js.map} +1 -1
  127. package/dist/{query-parser-DGRVFNM3.js → query-parser-BgiKJKvc.js} +6 -56
  128. package/dist/query-parser-BgiKJKvc.js.map +1 -0
  129. package/dist/{request-timeout-C_4C2BeR.js → request-timeout-DgH7j8qO.js} +3 -3
  130. package/dist/{request-timeout-C_4C2BeR.js.map → request-timeout-DgH7j8qO.js.map} +1 -1
  131. package/dist/schema-edit/apply-schema-change.d.ts +63 -3
  132. package/dist/schema-edit/project-root.d.ts +3 -2
  133. package/dist/schema-edit/remote-source.d.ts +9 -4
  134. package/dist/{schema-editor-routes-C5-lh_jO.js → schema-editor-routes-oIyuWl3L.js} +12 -7
  135. package/dist/schema-editor-routes-oIyuWl3L.js.map +1 -0
  136. package/dist/serve-spa.d.ts +58 -0
  137. package/dist/services/routed-realtime-service.d.ts +11 -0
  138. package/dist/soft-delete-params-BWPilMPF.js +59 -0
  139. package/dist/soft-delete-params-BWPilMPF.js.map +1 -0
  140. package/dist/{src-vkcwKXbT.js → src-CatHFUym.js} +439 -20
  141. package/dist/src-CatHFUym.js.map +1 -0
  142. package/dist/{src-pmvW7BFx.js → src-I3aG1PcY.js} +252 -70
  143. package/dist/src-I3aG1PcY.js.map +1 -0
  144. package/dist/storage/GCSStorageController.d.ts +2 -0
  145. package/dist/storage/LocalStorageController.d.ts +2 -0
  146. package/dist/storage/S3StorageController.d.ts +2 -0
  147. package/dist/storage/index.d.ts +2 -2
  148. package/dist/storage/property-limits.d.ts +41 -6
  149. package/dist/storage/request-keys.d.ts +15 -0
  150. package/dist/storage/requested-object.d.ts +74 -0
  151. package/dist/storage/routes.d.ts +36 -18
  152. package/dist/storage/tus-handler.d.ts +30 -5
  153. package/dist/storage/types.d.ts +19 -0
  154. package/dist/types-BfKcm9do.js.map +1 -1
  155. package/dist/utils/logger.d.ts +12 -0
  156. package/package.json +5 -5
  157. package/dist/GCSStorageController-CjrA4PMo.js.map +0 -1
  158. package/dist/S3StorageController-B6pKDNVj.js.map +0 -1
  159. package/dist/admin-roles-vYdp_Pil.js +0 -36
  160. package/dist/admin-roles-vYdp_Pil.js.map +0 -1
  161. package/dist/admin_block-DxKLmdiv.js +0 -206
  162. package/dist/admin_block-DxKLmdiv.js.map +0 -1
  163. package/dist/ast-schema-editor-Mvr50v_S.js.map +0 -1
  164. package/dist/auth/api-keys/api-key-permission-guard.d.ts +0 -65
  165. package/dist/auth-B-GIMpDG.js.map +0 -1
  166. package/dist/backup-D7YR94N3.js +0 -253
  167. package/dist/backup-D7YR94N3.js.map +0 -1
  168. package/dist/contract-routes-CbFjuBwa.js.map +0 -1
  169. package/dist/cron-loader-DfTj2Hbi.js.map +0 -1
  170. package/dist/cron-routes-eE8nif_b.js.map +0 -1
  171. package/dist/cron-scheduler-B0pLfAix.js.map +0 -1
  172. package/dist/cron-store-TcoGz-xS.js.map +0 -1
  173. package/dist/errors-DWsX4yTd.js.map +0 -1
  174. package/dist/function-routes-Chet4-lB.js.map +0 -1
  175. package/dist/logger-DO2PZc4i.js.map +0 -1
  176. package/dist/logs-routes-Bj4TYYUl.js.map +0 -1
  177. package/dist/openapi-generator-O_O24MAT.js.map +0 -1
  178. package/dist/query-parser-DGRVFNM3.js.map +0 -1
  179. package/dist/schema-editor-routes-C5-lh_jO.js.map +0 -1
  180. package/dist/src-pmvW7BFx.js.map +0 -1
  181. package/dist/src-vkcwKXbT.js.map +0 -1
@@ -43,6 +43,8 @@ export declare class GCSStorageController implements StorageController {
43
43
  maxResults?: number;
44
44
  pageToken?: string;
45
45
  }): Promise<StorageListResult>;
46
+ /** See {@link StorageController.maxFileSize}. */
47
+ maxFileSize(): number;
46
48
  /**
47
49
  * Validate file before upload
48
50
  */
@@ -63,6 +63,8 @@ export declare class LocalStorageController implements StorageController {
63
63
  * depend on it having run.
64
64
  */
65
65
  private getFullPath;
66
+ /** See {@link StorageController.maxFileSize}. */
67
+ maxFileSize(): number;
66
68
  /**
67
69
  * Validate file before upload
68
70
  */
@@ -24,6 +24,8 @@ export declare class S3StorageController implements StorageController {
24
24
  * can reach — and a mistyped one came back as "file not found".
25
25
  */
26
26
  knownBuckets(): string[];
27
+ /** See {@link StorageController.maxFileSize}. */
28
+ maxFileSize(): number;
27
29
  /**
28
30
  * Validate file before upload
29
31
  */
@@ -21,8 +21,8 @@ export * from "./storage-registry.js";
21
21
  export { parseTransformOptions, transformImage, isTransformableImage, TransformCache } from "./image-transform.js";
22
22
  export type { ImageTransformOptions } from "./image-transform.js";
23
23
  export { TusHandler } from "./tus-handler.js";
24
- export { createUploadConstraintResolver, assertUploadWithinPropertyLimits, isAcceptedFile, readUploadPropertyContext, UPLOAD_COLLECTION_FIELD, UPLOAD_PROPERTY_FIELD } from "./property-limits.js";
25
- export type { UploadConstraints, ResolveUploadConstraints } from "./property-limits.js";
24
+ export { createUploadConstraintResolver, createUploadPathResolver, assertUploadWithinPropertyLimits, assertUploadWithinPathLimits, isAcceptedFile, readUploadPropertyContext, UPLOAD_COLLECTION_FIELD, UPLOAD_PROPERTY_FIELD } from "./property-limits.js";
25
+ export type { UploadConstraints, ResolveUploadConstraints, ResolveUploadPathConstraints } from "./property-limits.js";
26
26
  import { BackendStorageConfig, StorageController } from "./types.js";
27
27
  /**
28
28
  * Create a storage controller from a config object.
@@ -12,12 +12,14 @@ import type { CollectionConfig } from "@rebasepro/types";
12
12
  * executable in the avatar bucket, and the config that said otherwise was never
13
13
  * consulted.
14
14
  *
15
- * The limits are per *property*, so enforcing them needs to know which property
16
- * a request is for. Uploads carry that as context (a form field on `POST
17
- * /upload`, `Upload-Metadata` on the resumable path); when it is absent the
18
- * global cap is still the ceiling, exactly as before. That is the honest
19
- * fallback: an upload that names no property has no property limit to check,
20
- * and refusing every context-less upload would break every existing client.
15
+ * The limits are per *property*, and the server finds the property two ways.
16
+ * By where the file lands: a property's `storagePath` is where its files go, so
17
+ * every upload into it meets the property's limits (`createUploadPathResolver`)
18
+ * — the panel never named its property, and a direct caller need not. And by
19
+ * name, when the upload carries context (a form field on `POST /upload`,
20
+ * `Upload-Metadata` on the resumable path), which covers a property whose
21
+ * `storagePath` is a function. A key no property claims, uploaded with no
22
+ * context, meets the source's own limit alone.
21
23
  */
22
24
  /** What a property says a file may be. */
23
25
  export interface UploadConstraints {
@@ -39,6 +41,39 @@ export type ResolveUploadConstraints = (collectionSlug: string, propertyPath: st
39
41
  * to choose.
40
42
  */
41
43
  export declare function createUploadConstraintResolver(collections: readonly CollectionConfig[]): ResolveUploadConstraints;
44
+ /**
45
+ * The limits of every property whose storage path a key falls in.
46
+ *
47
+ * @param storageId the source the upload is written to, canonical.
48
+ * @param key the canonical key the upload is written to.
49
+ */
50
+ export type ResolveUploadPathConstraints = (storageId: string, key: string) => UploadConstraints[];
51
+ /**
52
+ * The storage-path rules of the collections this backend serves.
53
+ *
54
+ * A property's `maxSize` and `acceptedFiles` used to be checked only when an
55
+ * upload named the property — which the panel never did, and a direct caller
56
+ * simply does not — so the limits the docs called "enforced by the server"
57
+ * held for nobody. A property's `storagePath` is where its files go, so it is
58
+ * also where its limits hold: every upload into that path, through any door,
59
+ * meets them.
60
+ *
61
+ * A path computed by a function cannot be read without running it, and has no
62
+ * rule; neither has a property that declares no limits.
63
+ */
64
+ export declare function createUploadPathResolver(collections: readonly CollectionConfig[]): ResolveUploadPathConstraints;
65
+ /**
66
+ * Refuse an upload that no property claiming its path would accept.
67
+ *
68
+ * Several properties may share a path; the file is for one of them, so it is
69
+ * enough that one accepts it. When none does, the first one's refusal is the
70
+ * answer, naming its property.
71
+ */
72
+ export declare function assertUploadWithinPathLimits(constraints: readonly UploadConstraints[], file: {
73
+ size?: number;
74
+ type?: string;
75
+ name?: string;
76
+ }): void;
42
77
  /** Is this file one the property accepts? */
43
78
  export declare function isAcceptedFile(accepted: readonly string[] | undefined, contentType: string | undefined, fileName: string | undefined): boolean;
44
79
  /**
@@ -10,6 +10,7 @@
10
10
  * write path's idea of an acceptable key and bucket the same.
11
11
  */
12
12
  import { ApiError } from "../api/errors.js";
13
+ import { type RequestedStorageObject } from "./requested-object.js";
13
14
  import type { StorageController } from "./types.js";
14
15
  /**
15
16
  * Canonicalize a caller-supplied storage key, answering 400 when it names
@@ -22,6 +23,20 @@ import type { StorageController } from "./types.js";
22
23
  * See `keys.ts` for why an unacceptable key is refused rather than repaired.
23
24
  */
24
25
  export declare function canonicalKeyOrBadRequest(key: string): string;
26
+ /**
27
+ * The object a `/file/*`, `/metadata/*` or `DELETE /file/*` request's wildcard
28
+ * path names, answering 400 when it names none.
29
+ *
30
+ * Derived by {@link requestedStorageObject}, the function `publicObjectAuth`
31
+ * and `fileTokenAuth` derive it with, so the key the route acts on is the key
32
+ * they decided about.
33
+ */
34
+ export declare function requestedObjectOrBadRequest(wildcard: string): RequestedStorageObject;
35
+ /**
36
+ * {@link requestedObjectOrBadRequest} for a path that is already text rather
37
+ * than a URL path — the folder route's JSON body.
38
+ */
39
+ export declare function objectOfPathOrBadRequest(path: string): RequestedStorageObject;
25
40
  /**
26
41
  * Canonicalize a caller-supplied bucket name, answering 400 when it is not one.
27
42
  *
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Which object a storage request's URL path names.
3
+ *
4
+ * Three parties read the path of a `GET /file/*`, `GET /metadata/*` or
5
+ * `DELETE /file/*`: the route, which serves or deletes an object;
6
+ * `publicObjectAuth`, which lets an anonymous caller through when that object
7
+ * is public; and `fileTokenAuth`, which compares it with the path a download
8
+ * token grants. A decision is only about the object acted on if it is made on
9
+ * the same key, so all three derive it here — the wildcard, the decoding, the
10
+ * bucket segment and the canonical key — and none of them spells a step of its
11
+ * own.
12
+ *
13
+ * They used to. `publicObjectAuth` judged the raw path with a check that strips
14
+ * everything up to a `://`, while the route canonicalized it, folding `//` to
15
+ * `/`: `GET /file/notes://public/secret.txt` was public to the one and served
16
+ * the private key `notes:/public/secret.txt` from the other, with the authorize
17
+ * hook never asked.
18
+ *
19
+ * Nothing here parses a scheme. A request path is a key, optionally behind the
20
+ * one bucket segment the routes recognise; a `://` in it is a `:` followed by
21
+ * an empty segment, and canonicalization folds the empty segment away.
22
+ */
23
+ /** The object a request path names. */
24
+ export interface RequestedStorageObject {
25
+ /** The bucket. Always `"default"`: the only bucket a request path can name. */
26
+ bucket: string;
27
+ /** The canonical key — the one the hook is asked about and the controller acts on. */
28
+ key: string;
29
+ }
30
+ /**
31
+ * The wildcard part of a storage route's path, still percent-encoded.
32
+ *
33
+ * Hono's `c.req.param('*')` does not work reliably in sub-routers mounted
34
+ * via `app.route(prefix, subRouter)`. Instead it is derived from the
35
+ * fully-resolved `c.req.path` and `c.req.routePath`.
36
+ *
37
+ * For a route `/metadata/*` mounted at `/api/storage`, a request to
38
+ * `/api/storage/metadata/default/file.jpg` yields routePath
39
+ * `/api/storage/metadata/*`. The prefix (everything before `/*`) is stripped,
40
+ * plus one character for the trailing `/`, to obtain `default/file.jpg`.
41
+ */
42
+ export declare function storageRequestWildcard(c: {
43
+ req: {
44
+ path: string;
45
+ routePath: string;
46
+ };
47
+ }): string;
48
+ /**
49
+ * The object a decoded path names: a key, behind an optional `default/` bucket
50
+ * segment (matched case-insensitively). Throws {@link InvalidStorageKeyError}
51
+ * when the key cannot be canonicalized.
52
+ *
53
+ * For a path that is already text — the folder route's JSON body. A URL path
54
+ * goes through {@link requestedStorageObject}, which decodes it first.
55
+ */
56
+ export declare function storageObjectOfPath(decodedPath: string): RequestedStorageObject;
57
+ /**
58
+ * The object a request's wildcard path names, or {@link InvalidStorageKeyError}.
59
+ *
60
+ * The wildcard arrives as Hono leaves it — `decodeURI`d, with the reserved
61
+ * characters (`/`, `:`, `%25` among them) still escaped — so one
62
+ * `decodeURIComponent` yields the path the client encoded. A `%` that does not
63
+ * begin an escape names no key, and is refused like any other key that cannot be
64
+ * canonicalized rather than thrown as a `URIError`.
65
+ */
66
+ export declare function requestedStorageObject(wildcard: string): RequestedStorageObject;
67
+ /**
68
+ * {@link requestedStorageObject}, or `null` when the path names no object.
69
+ *
70
+ * For the middleware, which must fail closed without an exception: a path that
71
+ * names no object is neither public nor covered by any download token, and the
72
+ * route answers it with a 400 of its own.
73
+ */
74
+ export declare function tryRequestedStorageObject(wildcard: string): RequestedStorageObject | null;
@@ -10,20 +10,35 @@ import { Hono } from "hono";
10
10
  import { StorageController, type StorageAuthorize, type StorageAuthorizeData } from "./types.js";
11
11
  import { type StorageRegistry } from "./storage-registry.js";
12
12
  import { type StorageSourceDefinition, type AuthAdapter } from "@rebasepro/types";
13
- import { type ResolveUploadConstraints } from "./property-limits.js";
13
+ import { type ResolveUploadConstraints, type ResolveUploadPathConstraints } from "./property-limits.js";
14
14
  import { HonoEnv } from "../api/types.js";
15
15
  import { type StorageTrigger } from "./triggers.js";
16
16
  import { type RenditionCacheConfig } from "./rendition-cache.js";
17
+ /**
18
+ * The policy every SVG response carries.
19
+ *
20
+ * An SVG embedded with `<img>` never runs script. Opened directly — a link, a
21
+ * new tab, an `<iframe>` — it is a document on the API origin, and its
22
+ * `<script>` would run there, next to the refresh cookie. `sandbox` gives that
23
+ * document an opaque origin with scripts disabled, and `default-src 'none'`
24
+ * stops it loading anything; `style-src 'unsafe-inline'` keeps the inline
25
+ * styles most SVGs are drawn with. It is what GitHub serves user content
26
+ * under. Serving SVG as a download instead made every SVG logo a broken
27
+ * thumbnail in the panel.
28
+ */
29
+ export declare const SVG_CONTENT_SECURITY_POLICY = "default-src 'none'; style-src 'unsafe-inline'; sandbox";
17
30
  /**
18
31
  * Decide what to actually serve for a stored content type.
19
32
  *
20
- * Returns the type to send and whether to force a download. The check is an
33
+ * Returns the type to send, whether to force a download, and the
34
+ * `Content-Security-Policy` the response must carry, if any. The check is an
21
35
  * allowlist rather than a blocklist of dangerous types: the set of types a
22
36
  * browser will execute grows, and the set we want to render inline does not.
23
37
  */
24
38
  export declare function resolveServedContentType(storedContentType: string): {
25
39
  contentType: string;
26
40
  attachment: boolean;
41
+ contentSecurityPolicy?: string;
27
42
  };
28
43
  export interface StorageRoutesConfig {
29
44
  /**
@@ -73,6 +88,13 @@ export interface StorageRoutesConfig {
73
88
  * to the global cap exactly as before.
74
89
  */
75
90
  uploadConstraints?: ResolveUploadConstraints;
91
+ /**
92
+ * The limits of the properties whose `storagePath` an upload's key falls
93
+ * in, by source and key — see `createUploadPathResolver`. Applied to every
94
+ * upload, whether or not it names a property, so a property's limits hold
95
+ * for every file written where its files go.
96
+ */
97
+ uploadPathConstraints?: ResolveUploadPathConstraints;
76
98
  /**
77
99
  * When provided, storage routes delegate auth to this adapter instead
78
100
  * of the built-in JWT module. This mirrors how data routes use
@@ -105,25 +127,21 @@ export interface StorageRoutesConfig {
105
127
  * Run something when an object lands, or when one goes. See `triggers.ts`.
106
128
  */
107
129
  triggers?: StorageTrigger[];
130
+ /**
131
+ * How long the download token `/metadata` mints for a private object stays
132
+ * valid, in seconds. {@link DEFAULT_DOWNLOAD_TOKEN_TTL_SECONDS} unless set;
133
+ * at most {@link MAX_DOWNLOAD_TOKEN_TTL_SECONDS}.
134
+ */
135
+ downloadTokenTtlSeconds?: number;
108
136
  }
137
+ /** Five minutes: long enough to render a page of thumbnails, short enough to be worthless in a log. */
138
+ export declare const DEFAULT_DOWNLOAD_TOKEN_TTL_SECONDS = 300;
109
139
  /**
110
- * Extract the wildcard portion of a route path from the full request path.
111
- *
112
- * Hono's `c.req.param('*')` does not work reliably in sub-routers mounted
113
- * via `app.route(prefix, subRouter)`. Instead we derive the wildcard value
114
- * from the fully-resolved `c.req.path` and `c.req.routePath`.
115
- *
116
- * For a route `/metadata/*` mounted at `/api/storage`, a request to
117
- * `/api/storage/metadata/default/file.jpg` yields routePath
118
- * `/api/storage/metadata/*`. We strip the prefix (everything before `/*`)
119
- * plus one character for the trailing `/` to obtain `default/file.jpg`.
140
+ * A week. The token travels in a URL — into access logs, Referer headers and
141
+ * browser history — so a lifetime past this is a permanent link to a private
142
+ * file. A file that must be linked forever belongs under the public prefix.
120
143
  */
121
- export declare function extractWildcardPath(c: {
122
- req: {
123
- path: string;
124
- routePath: string;
125
- };
126
- }): string;
144
+ export declare const MAX_DOWNLOAD_TOKEN_TTL_SECONDS: number;
127
145
  /**
128
146
  * Create storage REST API routes
129
147
  */
@@ -10,7 +10,7 @@
10
10
  import type { Context } from "hono";
11
11
  import type { StorageController } from "./types.js";
12
12
  import { type StorageRegistry } from "./storage-registry.js";
13
- import { type ResolveUploadConstraints } from "./property-limits.js";
13
+ import { type ResolveUploadConstraints, type ResolveUploadPathConstraints } from "./property-limits.js";
14
14
  /**
15
15
  * TUS resumable upload handler.
16
16
  *
@@ -65,6 +65,12 @@ export declare class TusHandler {
65
65
  * accept is refused before the first chunk rather than after the last.
66
66
  */
67
67
  private uploadConstraints?;
68
+ /**
69
+ * The limits of the properties whose storage path the upload's key
70
+ * falls in — checked whether or not the upload names a property, as
71
+ * `POST /upload` checks them.
72
+ */
73
+ private uploadPathConstraints?;
68
74
  private uploads;
69
75
  private tusDir;
70
76
  private cleanupTimer?;
@@ -123,7 +129,13 @@ export declare class TusHandler {
123
129
  * and the metadata's content type, so a file the property will not
124
130
  * accept is refused before the first chunk rather than after the last.
125
131
  */
126
- uploadConstraints?: ResolveUploadConstraints | undefined);
132
+ uploadConstraints?: ResolveUploadConstraints | undefined,
133
+ /**
134
+ * The limits of the properties whose storage path the upload's key
135
+ * falls in — checked whether or not the upload names a property, as
136
+ * `POST /upload` checks them.
137
+ */
138
+ uploadPathConstraints?: ResolveUploadPathConstraints | undefined);
127
139
  /** Ensure the temp directory exists. */
128
140
  private ensureDir;
129
141
  /** Start periodic cleanup of stale uploads. */
@@ -136,8 +148,21 @@ export declare class TusHandler {
136
148
  * Format: `key base64value,key2 base64value2`
137
149
  */
138
150
  private parseMetadata;
139
- /** `OPTIONS /tus` — TUS capability discovery. */
140
- options(): Response;
151
+ /** The controller an upload to this storage source is written to. */
152
+ private targetFor;
153
+ /**
154
+ * The largest upload this source takes: the source's own limit, under the
155
+ * deployment's and the protocol ceiling. One function for what `OPTIONS`
156
+ * advertises and what `create` enforces, which used to be 5 GB and 50 MB.
157
+ */
158
+ private limitFor;
159
+ /**
160
+ * `OPTIONS /tus` — TUS capability discovery.
161
+ *
162
+ * `Tus-Max-Size` is the limit `create` holds this source to, for the source
163
+ * named by `?storageId` (the default one otherwise).
164
+ */
165
+ options(c?: Context): Response;
141
166
  /** `POST /tus` — Create a new upload. */
142
167
  create(c: Context): Promise<Response>;
143
168
  /**
@@ -162,7 +187,7 @@ export declare class TusHandler {
162
187
  private ownedUpload;
163
188
  /** `HEAD /tus/:id` — Query upload progress. */
164
189
  head(c: Context, id: string): Response;
165
- /** `PATCH /tus/:id` — Append data to an upload. */
190
+ /** `PATCH /tus/:id` — Write one chunk of an upload, at its offset. */
166
191
  patch(c: Context, id: string): Promise<Response>;
167
192
  /** `DELETE /tus/:id` — Cancel and remove an upload. */
168
193
  delete(c: Context, id: string): Promise<Response>;
@@ -23,6 +23,14 @@ export interface LocalStorageConfig {
23
23
  * production, where an unbound object store stays unbound.
24
24
  */
25
25
  standsInFor?: string;
26
+ /**
27
+ * Set when nothing chose this backend: no `STORAGE_TYPE` and no declared
28
+ * engine, so local disk is only the default a source falls back to.
29
+ * Production drops a local backend either way. This only decides how that
30
+ * is reported: an implicit default being dropped means the project uses no
31
+ * storage, which is not a misconfiguration.
32
+ */
33
+ implicit?: boolean;
26
34
  }
27
35
  /**
28
36
  * S3-compatible storage configuration (works with AWS S3 and MinIO)
@@ -159,6 +167,17 @@ export interface StorageController {
159
167
  * when the caller names none, and each provider maps it to its own.
160
168
  */
161
169
  knownBuckets?(): string[];
170
+ /**
171
+ * The largest file this controller accepts, in bytes.
172
+ *
173
+ * Every door that takes a file reads it, so the number a client is told is
174
+ * the number it is held to: the upload route checks it before anything is
175
+ * stored, the resumable route checks it at creation — before the first
176
+ * chunk rather than after the last — and advertises it as `Tus-Max-Size`,
177
+ * and the server's upload body limit is the largest of them. A controller
178
+ * that does not implement this is held to the deployment-wide limit alone.
179
+ */
180
+ maxFileSize?(): number;
162
181
  }
163
182
  /**
164
183
  * Default maximum file size (50MB)
@@ -1 +1 @@
1
- {"version":3,"file":"types-BfKcm9do.js","names":[],"sources":["../src/storage/types.ts"],"sourcesContent":["/**\n * Storage configuration and types for Rebase backend\n */\n\nimport { StorageSource, UploadFileProps, UploadFileResult, DownloadConfig, DownloadMetadata, StorageListResult, StorageReference } from \"@rebasepro/types\";\n\n/**\n * Local filesystem storage configuration\n */\nexport interface LocalStorageConfig {\n type: \"local\";\n /** Base directory for file storage (e.g., './uploads') */\n basePath: string;\n /** Maximum file size in bytes (default: 50MB) */\n maxFileSize?: number;\n /** Allowed MIME types (if not set, all types allowed) */\n allowedMimeTypes?: string[];\n /** Base URL for generating download URLs (default: auto-detected from request) */\n baseUrl?: string;\n /**\n * Set when this local backend stands in for an object store the source\n * declared and nothing bound — `bucket(\"media\", { engine: \"s3\" })` with no\n * `S3_BUCKET__MEDIA` in a development process. Names the engine it stands\n * in for, so boot can say so and `rebase status` can show it. Never set in\n * production, where an unbound object store stays unbound.\n */\n standsInFor?: string;\n}\n\n/**\n * S3-compatible storage configuration (works with AWS S3 and MinIO)\n */\nexport interface S3StorageConfig {\n type: \"s3\";\n /** S3 bucket name */\n bucket: string;\n /** AWS region (e.g., 'us-east-1') */\n region?: string;\n /** Custom endpoint URL (required for MinIO, Cloudflare R2, Hetzner Object Storage) */\n endpoint?: string;\n /** AWS access key ID */\n accessKeyId: string;\n /** AWS secret access key */\n secretAccessKey: string;\n /** Use path-style URLs (required for MinIO) */\n forcePathStyle?: boolean;\n /** Maximum file size in bytes (default: 50MB) */\n maxFileSize?: number;\n /** Allowed MIME types (if not set, all types allowed) */\n allowedMimeTypes?: string[];\n /** URL expiration time in seconds for signed URLs (default: 3600) */\n signedUrlExpiration?: number;\n}\n\n/**\n * Google Cloud Storage configuration (works with GCS and Firebase Storage)\n */\nexport interface GCSStorageConfig {\n type: \"gcs\";\n /** GCS bucket name (e.g. \"my-project.appspot.com\" for Firebase Storage) */\n bucket: string;\n /** GCP project ID (optional, auto-detected from credentials) */\n projectId?: string;\n /** Path to service account JSON key file */\n keyFilename?: string;\n /** Service account credentials object (alternative to keyFilename) */\n credentials?: { client_email: string; private_key: string; project_id?: string };\n /** Maximum file size in bytes (default: 50MB) */\n maxFileSize?: number;\n /** Allowed MIME types */\n allowedMimeTypes?: string[];\n /** Signed URL expiration in seconds (default: 3600) */\n signedUrlExpiration?: number;\n}\n\n/**\n * Storage configuration — local filesystem, S3-compatible, or Google Cloud Storage.\n *\n * **Built-in providers:**\n * - `local` — Zero-config filesystem storage. Great for dev and single-server deployments (Hetzner, bare metal).\n * - `s3` — Any S3-compatible provider. AWS S3, Cloudflare R2, MinIO, Hetzner Object Storage,\n * Backblaze B2, DigitalOcean Spaces, and even GCS (via its S3-compatible interoperability API).\n * - `gcs` — Native Google Cloud Storage / Firebase Storage via `@google-cloud/storage`.\n *\n * **Custom providers:**\n * For other cloud storage (Azure Blob, etc.), implement the `StorageController`\n * interface and pass the instance directly to the `storage` config.\n */\nexport type BackendStorageConfig = LocalStorageConfig | S3StorageConfig | GCSStorageConfig;\n\n/** What a storage request is trying to do to an object. */\nexport type {\n StorageOperation,\n StorageAuthorizeUser,\n StorageAuthorizeContext,\n StorageAuthorizeData,\n StorageAuthorize\n} from \"@rebasepro/types\";\n\n/**\n * Storage controller interface for backend implementations\n */\nexport interface StorageController {\n /**\n * Upload an object\n */\n putObject(props: UploadFileProps): Promise<UploadFileResult>;\n\n /**\n * Get a download URL (signed URL equivalent) for an object\n */\n getSignedUrl(key: string, bucket?: string): Promise<DownloadConfig>;\n\n /**\n * Describe an object — size, type, custom metadata — without producing a\n * URL for it, or `null` when there is no such object.\n *\n * `GET /metadata/*` wants only this: the client builds its own `/file/*`\n * URL and spends the download token the route mints. Asking\n * `getSignedUrl` for it made every metadata read sign a URL nobody used,\n * and on GCS signing is not free — without a key file the client asks the\n * IAM Credentials API to sign, which needs `iam.serviceAccounts.signBlob`\n * on the runtime account itself. Cloud Run does not grant that, so every\n * private object answered 500 there.\n *\n * A controller that does not implement this keeps the old behaviour: the\n * route falls back to `getSignedUrl` and reads its `metadata`.\n */\n getMetadata?(key: string, bucket?: string): Promise<DownloadMetadata | null>;\n\n /**\n * Get object as a File\n */\n getObject(key: string, bucket?: string): Promise<File | null>;\n\n /**\n * Delete an object\n */\n deleteObject(key: string, bucket?: string): Promise<void>;\n\n /**\n * List objects in a prefix\n */\n listObjects(prefix: string, options?: {\n bucket?: string;\n maxResults?: number;\n pageToken?: string;\n }): Promise<StorageListResult>;\n\n /**\n * Get the storage provider identifier.\n *\n * Built-in values are `'local'` and `'s3'`. Custom implementations\n * should return their own identifier (e.g. `'gcs'`, `'azure'`).\n */\n getType(): string;\n\n /**\n * The buckets this controller serves, if it knows.\n *\n * The `bucket` a request may name is not a free-text field: on S3 and GCS it\n * goes straight to the provider, so an unvalidated one addresses *any*\n * bucket the deployment's credentials can reach, and locally it names a\n * directory the framework would create on demand. Either way, a caller who\n * mistypes it gets \"file not found\" — the same answer as a missing object —\n * with nothing to say the bucket was the problem.\n *\n * So a controller that knows its buckets says so, and the routes refuse\n * anything else with `UNKNOWN_STORAGE_SOURCE` naming what is served. A\n * controller that does not implement this keeps the old behaviour and is\n * handed whatever the caller wrote — a custom implementation may address\n * buckets this framework has no way to enumerate.\n *\n * `\"default\"` is always among them: it is the logical name every route uses\n * when the caller names none, and each provider maps it to its own.\n */\n knownBuckets?(): string[];\n}\n\n/**\n * Default maximum file size (50MB)\n */\nexport const DEFAULT_MAX_FILE_SIZE = 50 * 1024 * 1024;\n\n/**\n * Common image MIME types\n */\nexport const IMAGE_MIME_TYPES = [\n \"image/jpeg\",\n \"image/png\",\n \"image/gif\",\n \"image/webp\",\n \"image/svg+xml\",\n \"image/bmp\",\n \"image/tiff\"\n];\n\n/**\n * Common document MIME types\n */\nexport const DOCUMENT_MIME_TYPES = [\n \"application/pdf\",\n \"application/msword\",\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n \"application/vnd.ms-excel\",\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n \"application/vnd.ms-powerpoint\",\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\",\n \"text/plain\",\n \"text/csv\"\n];\n"],"mappings":";;;;;;;;AAsLA,IAAa,wBAAwB,KAAK,OAAO;;;;AAKjD,IAAa,mBAAmB;CAC5B;CACA;CACA;CACA;CACA;CACA;CACA;AACJ;;;;AAKA,IAAa,sBAAsB;CAC/B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACJ"}
1
+ {"version":3,"file":"types-BfKcm9do.js","names":[],"sources":["../src/storage/types.ts"],"sourcesContent":["/**\n * Storage configuration and types for Rebase backend\n */\n\nimport { StorageSource, UploadFileProps, UploadFileResult, DownloadConfig, DownloadMetadata, StorageListResult, StorageReference } from \"@rebasepro/types\";\n\n/**\n * Local filesystem storage configuration\n */\nexport interface LocalStorageConfig {\n type: \"local\";\n /** Base directory for file storage (e.g., './uploads') */\n basePath: string;\n /** Maximum file size in bytes (default: 50MB) */\n maxFileSize?: number;\n /** Allowed MIME types (if not set, all types allowed) */\n allowedMimeTypes?: string[];\n /** Base URL for generating download URLs (default: auto-detected from request) */\n baseUrl?: string;\n /**\n * Set when this local backend stands in for an object store the source\n * declared and nothing bound — `bucket(\"media\", { engine: \"s3\" })` with no\n * `S3_BUCKET__MEDIA` in a development process. Names the engine it stands\n * in for, so boot can say so and `rebase status` can show it. Never set in\n * production, where an unbound object store stays unbound.\n */\n standsInFor?: string;\n /**\n * Set when nothing chose this backend: no `STORAGE_TYPE` and no declared\n * engine, so local disk is only the default a source falls back to.\n * Production drops a local backend either way. This only decides how that\n * is reported: an implicit default being dropped means the project uses no\n * storage, which is not a misconfiguration.\n */\n implicit?: boolean;\n}\n\n/**\n * S3-compatible storage configuration (works with AWS S3 and MinIO)\n */\nexport interface S3StorageConfig {\n type: \"s3\";\n /** S3 bucket name */\n bucket: string;\n /** AWS region (e.g., 'us-east-1') */\n region?: string;\n /** Custom endpoint URL (required for MinIO, Cloudflare R2, Hetzner Object Storage) */\n endpoint?: string;\n /** AWS access key ID */\n accessKeyId: string;\n /** AWS secret access key */\n secretAccessKey: string;\n /** Use path-style URLs (required for MinIO) */\n forcePathStyle?: boolean;\n /** Maximum file size in bytes (default: 50MB) */\n maxFileSize?: number;\n /** Allowed MIME types (if not set, all types allowed) */\n allowedMimeTypes?: string[];\n /** URL expiration time in seconds for signed URLs (default: 3600) */\n signedUrlExpiration?: number;\n}\n\n/**\n * Google Cloud Storage configuration (works with GCS and Firebase Storage)\n */\nexport interface GCSStorageConfig {\n type: \"gcs\";\n /** GCS bucket name (e.g. \"my-project.appspot.com\" for Firebase Storage) */\n bucket: string;\n /** GCP project ID (optional, auto-detected from credentials) */\n projectId?: string;\n /** Path to service account JSON key file */\n keyFilename?: string;\n /** Service account credentials object (alternative to keyFilename) */\n credentials?: { client_email: string; private_key: string; project_id?: string };\n /** Maximum file size in bytes (default: 50MB) */\n maxFileSize?: number;\n /** Allowed MIME types */\n allowedMimeTypes?: string[];\n /** Signed URL expiration in seconds (default: 3600) */\n signedUrlExpiration?: number;\n}\n\n/**\n * Storage configuration — local filesystem, S3-compatible, or Google Cloud Storage.\n *\n * **Built-in providers:**\n * - `local` — Zero-config filesystem storage. Great for dev and single-server deployments (Hetzner, bare metal).\n * - `s3` — Any S3-compatible provider. AWS S3, Cloudflare R2, MinIO, Hetzner Object Storage,\n * Backblaze B2, DigitalOcean Spaces, and even GCS (via its S3-compatible interoperability API).\n * - `gcs` — Native Google Cloud Storage / Firebase Storage via `@google-cloud/storage`.\n *\n * **Custom providers:**\n * For other cloud storage (Azure Blob, etc.), implement the `StorageController`\n * interface and pass the instance directly to the `storage` config.\n */\nexport type BackendStorageConfig = LocalStorageConfig | S3StorageConfig | GCSStorageConfig;\n\n/** What a storage request is trying to do to an object. */\nexport type {\n StorageOperation,\n StorageAuthorizeUser,\n StorageAuthorizeContext,\n StorageAuthorizeData,\n StorageAuthorize\n} from \"@rebasepro/types\";\n\n/**\n * Storage controller interface for backend implementations\n */\nexport interface StorageController {\n /**\n * Upload an object\n */\n putObject(props: UploadFileProps): Promise<UploadFileResult>;\n\n /**\n * Get a download URL (signed URL equivalent) for an object\n */\n getSignedUrl(key: string, bucket?: string): Promise<DownloadConfig>;\n\n /**\n * Describe an object — size, type, custom metadata — without producing a\n * URL for it, or `null` when there is no such object.\n *\n * `GET /metadata/*` wants only this: the client builds its own `/file/*`\n * URL and spends the download token the route mints. Asking\n * `getSignedUrl` for it made every metadata read sign a URL nobody used,\n * and on GCS signing is not free — without a key file the client asks the\n * IAM Credentials API to sign, which needs `iam.serviceAccounts.signBlob`\n * on the runtime account itself. Cloud Run does not grant that, so every\n * private object answered 500 there.\n *\n * A controller that does not implement this keeps the old behaviour: the\n * route falls back to `getSignedUrl` and reads its `metadata`.\n */\n getMetadata?(key: string, bucket?: string): Promise<DownloadMetadata | null>;\n\n /**\n * Get object as a File\n */\n getObject(key: string, bucket?: string): Promise<File | null>;\n\n /**\n * Delete an object\n */\n deleteObject(key: string, bucket?: string): Promise<void>;\n\n /**\n * List objects in a prefix\n */\n listObjects(prefix: string, options?: {\n bucket?: string;\n maxResults?: number;\n pageToken?: string;\n }): Promise<StorageListResult>;\n\n /**\n * Get the storage provider identifier.\n *\n * Built-in values are `'local'` and `'s3'`. Custom implementations\n * should return their own identifier (e.g. `'gcs'`, `'azure'`).\n */\n getType(): string;\n\n /**\n * The buckets this controller serves, if it knows.\n *\n * The `bucket` a request may name is not a free-text field: on S3 and GCS it\n * goes straight to the provider, so an unvalidated one addresses *any*\n * bucket the deployment's credentials can reach, and locally it names a\n * directory the framework would create on demand. Either way, a caller who\n * mistypes it gets \"file not found\" — the same answer as a missing object —\n * with nothing to say the bucket was the problem.\n *\n * So a controller that knows its buckets says so, and the routes refuse\n * anything else with `UNKNOWN_STORAGE_SOURCE` naming what is served. A\n * controller that does not implement this keeps the old behaviour and is\n * handed whatever the caller wrote — a custom implementation may address\n * buckets this framework has no way to enumerate.\n *\n * `\"default\"` is always among them: it is the logical name every route uses\n * when the caller names none, and each provider maps it to its own.\n */\n knownBuckets?(): string[];\n\n /**\n * The largest file this controller accepts, in bytes.\n *\n * Every door that takes a file reads it, so the number a client is told is\n * the number it is held to: the upload route checks it before anything is\n * stored, the resumable route checks it at creation — before the first\n * chunk rather than after the last — and advertises it as `Tus-Max-Size`,\n * and the server's upload body limit is the largest of them. A controller\n * that does not implement this is held to the deployment-wide limit alone.\n */\n maxFileSize?(): number;\n}\n\n/**\n * Default maximum file size (50MB)\n */\nexport const DEFAULT_MAX_FILE_SIZE = 50 * 1024 * 1024;\n\n/**\n * Common image MIME types\n */\nexport const IMAGE_MIME_TYPES = [\n \"image/jpeg\",\n \"image/png\",\n \"image/gif\",\n \"image/webp\",\n \"image/svg+xml\",\n \"image/bmp\",\n \"image/tiff\"\n];\n\n/**\n * Common document MIME types\n */\nexport const DOCUMENT_MIME_TYPES = [\n \"application/pdf\",\n \"application/msword\",\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n \"application/vnd.ms-excel\",\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n \"application/vnd.ms-powerpoint\",\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\",\n \"text/plain\",\n \"text/csv\"\n];\n"],"mappings":";;;;;;;;AA0MA,IAAa,wBAAwB,KAAK,OAAO;;;;AAKjD,IAAa,mBAAmB;CAC5B;CACA;CACA;CACA;CACA;CACA;CACA;AACJ;;;;AAKA,IAAa,sBAAsB;CAC/B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACJ"}
@@ -50,6 +50,18 @@ export declare function rawQueryLoggingEnabled(): boolean;
50
50
  * before persisting and then logs the result.
51
51
  */
52
52
  export declare function redactSensitiveText(text: string): string;
53
+ /**
54
+ * `JSON.stringify`, with the redaction a structured field gets.
55
+ *
56
+ * For a value that has to go *into the message*. Once an object is a string,
57
+ * `emit` can only strip `Failed query:` spans out of it: a sensitive key would
58
+ * pass, and so would the `query` and `params` own-properties `DrizzleQueryError`
59
+ * carries beside its message, which `JSON.stringify` copies and the message
60
+ * redaction never sees. Unlike the structured walk this honours `toJSON`, since
61
+ * the result is read as text. Never throws: a value that cannot be stringified
62
+ * is a marker, not a failed log line.
63
+ */
64
+ export declare function stringifyForLog(value: unknown): string | undefined;
53
65
  /**
54
66
  * Something that wants a copy of every line this logger writes.
55
67
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rebasepro/server",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Database-Agnostic Backend Core for Rebase",
5
5
  "keywords": [
6
6
  "rebase",
@@ -54,10 +54,10 @@
54
54
  "jsonwebtoken": "^9.0.3",
55
55
  "ws": "^8.21.1",
56
56
  "zod": "^4.4.3",
57
- "@rebasepro/client": "0.23.0",
58
- "@rebasepro/common": "0.23.0",
59
- "@rebasepro/types": "0.23.0",
60
- "@rebasepro/utils": "0.23.0"
57
+ "@rebasepro/client": "0.24.0",
58
+ "@rebasepro/common": "0.24.0",
59
+ "@rebasepro/utils": "0.24.0",
60
+ "@rebasepro/types": "0.24.0"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@jest/globals": "^30.4.1",
@@ -1 +0,0 @@
1
- {"version":3,"file":"GCSStorageController-CjrA4PMo.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 {\n StorageController,\n GCSStorageConfig,\n DEFAULT_MAX_FILE_SIZE\n} from \"./types\";\nimport { folderKey, listingPrefix } from \"./keys\";\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): Promise<UploadFileResult> {\n 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 // Convert File to Buffer\n const arrayBuffer = await file.arrayBuffer();\n const buffer = Buffer.from(arrayBuffer);\n\n await gcsFile.save(buffer, {\n contentType: file.type,\n metadata: metadata ? { metadata: this.flattenMetadata(metadata) } : undefined\n });\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 error;\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 const expiresIn = this.config.signedUrlExpiration ?? 3600;\n const [url] = await client.bucket(metadata.bucket).file(metadata.fullPath).getSignedUrl({\n action: \"read\",\n expires: Date.now() + expiresIn * 1000\n });\n\n return {\n url,\n metadata\n };\n }\n\n async getObject(key: string, bucket?: string): Promise<File | 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 gcsFile = client.bucket(resolvedBucket).file(resolvedPath);\n\n const [contents] = await gcsFile.download();\n\n // Get content type from metadata\n const [fileMetadata] = await gcsFile.getMetadata();\n const contentType = fileMetadata.contentType || \"application/octet-stream\";\n const fileName = resolvedPath.split(\"/\").pop() || resolvedPath;\n\n const blob = new Blob([new Uint8Array(contents)], { type: contentType });\n return new File([blob], fileName, { type: contentType });\n } catch (error: unknown) {\n if (isNotFoundError(error)) {\n return null;\n }\n throw error;\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 await gcsFile.delete({ ignoreNotFound: true });\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 [files, , filesApiResponse] = await gcsBucket.getFiles({\n prefix: listingPrefix(prefix),\n delimiter: \"/\",\n maxResults: options?.maxResults ?? 1000,\n pageToken: options?.pageToken,\n autoPaginate: false\n });\n\n const items: StorageReference[] = files.map(file => ({\n bucket: resolvedBucket,\n fullPath: file.name,\n name: file.name.split(\"/\").pop() || \"\",\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 /**\n * Validate file before upload\n */\n private validateFile(file: File): void {\n const maxSize = this.config.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;\n if (file.size > maxSize) {\n throw new Error(`File size ${file.size} exceeds maximum allowed size ${maxSize}`);\n }\n\n if (this.config.allowedMimeTypes && this.config.allowedMimeTypes.length > 0) {\n if (!this.config.allowedMimeTypes.includes(file.type)) {\n throw new Error(`File type ${file.type} is not allowed. Allowed types: ${this.config.allowedMimeTypes.join(\", \")}`);\n }\n }\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/**\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 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"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"S3StorageController-B6pKDNVj.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 { DEFAULT_MAX_FILE_SIZE, S3StorageConfig, StorageController } from \"./types\";\nimport { folderKey, listingPrefix } from \"./keys\";\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/**\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 /**\n * Validate file before upload\n */\n private validateFile(file: File): void {\n const maxSize = this.config.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;\n if (file.size > maxSize) {\n throw new Error(`File size ${file.size} exceeds maximum allowed size ${maxSize}`);\n }\n\n if (this.config.allowedMimeTypes && this.config.allowedMimeTypes.length > 0) {\n if (!this.config.allowedMimeTypes.includes(file.type)) {\n throw new Error(`File type ${file.type} is not allowed. Allowed types: ${this.config.allowedMimeTypes.join(\", \")}`);\n }\n }\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): Promise<UploadFileResult> {\n this.validateFile(file);\n\n const usedBucket = this.getBucket(bucket);\n\n // Convert File to Buffer\n const arrayBuffer = await file.arrayBuffer();\n const buffer = Buffer.from(arrayBuffer);\n\n const s3 = await loadS3();\n const client = await this.getClient();\n\n const command = new s3.PutObjectCommand({\n Bucket: usedBucket,\n Key: key,\n Body: buffer,\n ContentType: file.type,\n Metadata: metadata ? this.flattenMetadata(metadata) : undefined\n });\n\n await client.send(command);\n\n return {\n key,\n bucket: usedBucket,\n storageUrl: `s3://${usedBucket}/${key}`\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 const expiresIn = this.config.signedUrlExpiration ?? 3600;\n const url = await presigner.getSignedUrl(client, getCommand, { expiresIn });\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 const s3Error = error as { name?: string; $metadata?: { httpStatusCode?: number } };\n if (s3Error.name === \"NotFound\" || s3Error.$metadata?.httpStatusCode === 404) {\n return {\n url: null,\n fileNotFound: true\n };\n }\n throw error;\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 return new File([blob], fileName, { type: contentType });\n } catch (error: unknown) {\n const s3Error = error as { name?: string; $metadata?: { httpStatusCode?: number } };\n if (s3Error.name === \"NoSuchKey\" || s3Error.$metadata?.httpStatusCode === 404) {\n return null;\n }\n throw error;\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 const s3Error = error as { name?: string; $metadata?: { httpStatusCode?: number } };\n if (s3Error.name === \"NoSuchKey\" || s3Error.$metadata?.httpStatusCode === 404) {\n return;\n }\n throw error;\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 command = new s3.ListObjectsV2Command({\n Bucket: resolvedBucket,\n Prefix: listingPrefix(prefix),\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\n const items: StorageReference[] = (response.Contents || []).map(obj => ({\n bucket: resolvedBucket,\n fullPath: obj.Key || \"\",\n name: (obj.Key || \"\").split(\"/\").pop() || \"\",\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"],"mappings":"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"}
@@ -1,36 +0,0 @@
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
- * Which role names carry administrative privilege.
8
- *
9
- * One definition, because there used to be several and they disagreed.
10
- * `requireAdmin` accepted `admin` **or** `schema-admin`; the guard that refuses
11
- * a dangerous `defaultRole` compared against `admin` alone. So
12
- * `AUTH_DEFAULT_ROLE=schema-admin` passed the guard and made every public
13
- * registrant an administrator — and, since a `schema-admin` may edit users, one
14
- * of them could then grant themselves real `admin` and keep it.
15
- *
16
- * The guard and the check have to read the same list or the gap comes back, so
17
- * neither of them owns it.
18
- *
19
- * `schema-admin` is here deliberately: it can reach the schema editor and the
20
- * SQL surfaces, which is administrative by any definition that matters. If a
21
- * genuinely lesser role is added later, it does not belong in this list — it
22
- * belongs in a capability check of its own.
23
- */
24
- var ADMINISTRATIVE_ROLES = ["admin", "schema-admin"];
25
- /** Does this single role name carry administrative privilege? */
26
- function isAdministrativeRole(role) {
27
- return ADMINISTRATIVE_ROLES.includes(role);
28
- }
29
- /** Does any role in this list carry administrative privilege? */
30
- function hasAdministrativeRole(roles) {
31
- return !!roles?.some(isAdministrativeRole);
32
- }
33
- //#endregion
34
- export { hasAdministrativeRole as n, isAdministrativeRole as r, ADMINISTRATIVE_ROLES as t };
35
-
36
- //# sourceMappingURL=admin-roles-vYdp_Pil.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"admin-roles-vYdp_Pil.js","names":[],"sources":["../src/auth/admin-roles.ts"],"sourcesContent":["/**\n * Which role names carry administrative privilege.\n *\n * One definition, because there used to be several and they disagreed.\n * `requireAdmin` accepted `admin` **or** `schema-admin`; the guard that refuses\n * a dangerous `defaultRole` compared against `admin` alone. So\n * `AUTH_DEFAULT_ROLE=schema-admin` passed the guard and made every public\n * registrant an administrator — and, since a `schema-admin` may edit users, one\n * of them could then grant themselves real `admin` and keep it.\n *\n * The guard and the check have to read the same list or the gap comes back, so\n * neither of them owns it.\n *\n * `schema-admin` is here deliberately: it can reach the schema editor and the\n * SQL surfaces, which is administrative by any definition that matters. If a\n * genuinely lesser role is added later, it does not belong in this list — it\n * belongs in a capability check of its own.\n */\nexport const ADMINISTRATIVE_ROLES = [\"admin\", \"schema-admin\"] as const;\n\nexport type AdministrativeRole = (typeof ADMINISTRATIVE_ROLES)[number];\n\n/** Does this single role name carry administrative privilege? */\nexport function isAdministrativeRole(role: string): role is AdministrativeRole {\n return (ADMINISTRATIVE_ROLES as readonly string[]).includes(role);\n}\n\n/** Does any role in this list carry administrative privilege? */\nexport function hasAdministrativeRole(roles: readonly string[] | null | undefined): boolean {\n return !!roles?.some(isAdministrativeRole);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAkBA,IAAa,uBAAuB,CAAC,SAAS,cAAc;;AAK5D,SAAgB,qBAAqB,MAA0C;CAC3E,OAAQ,qBAA2C,SAAS,IAAI;AACpE;;AAGA,SAAgB,sBAAsB,OAAsD;CACxF,OAAO,CAAC,CAAC,OAAO,KAAK,oBAAoB;AAC7C"}