@rebasepro/server 0.17.3 → 0.18.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 (147) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +5 -1
  3. package/bin/rebase-server.js +16 -1
  4. package/dist/{GCSStorageController-Bl7nmhdv.js → GCSStorageController-ZXPoNqW3.js} +21 -10
  5. package/dist/GCSStorageController-ZXPoNqW3.js.map +1 -0
  6. package/dist/{S3StorageController-CWvvrRpF.js → S3StorageController-5pAXyv31.js} +24 -10
  7. package/dist/S3StorageController-5pAXyv31.js.map +1 -0
  8. package/dist/admin-roles-vYdp_Pil.js +36 -0
  9. package/dist/admin-roles-vYdp_Pil.js.map +1 -0
  10. package/dist/{admin_block-BX6YULvJ.js → admin_block-0Xu0r6eZ.js} +4 -1
  11. package/dist/admin_block-0Xu0r6eZ.js.map +1 -0
  12. package/dist/api/errors.d.ts +37 -0
  13. package/dist/api/live-schema-routes.d.ts +14 -0
  14. package/dist/api/logs-routes.d.ts +8 -0
  15. package/dist/api/rest/api-generator.d.ts +33 -0
  16. package/dist/api/root-error-handler.d.ts +62 -0
  17. package/dist/api/schema-drift.d.ts +39 -0
  18. package/dist/api/types.d.ts +55 -0
  19. package/dist/{ast-schema-editor-CvkYOvri.js → ast-schema-editor-CNgFJ3NF.js} +23 -7
  20. package/dist/ast-schema-editor-CNgFJ3NF.js.map +1 -0
  21. package/dist/auth/api-keys/api-key-routes.d.ts +12 -0
  22. package/dist/auth/auth-hooks.d.ts +13 -2
  23. package/dist/auth/exclude-auth-secrets.d.ts +14 -0
  24. package/dist/auth/jwt.d.ts +22 -1
  25. package/dist/auth/mfa-routes.d.ts +10 -1
  26. package/dist/auth/middleware.d.ts +22 -0
  27. package/dist/auth/rate-limiter.d.ts +50 -0
  28. package/dist/auth/registration-policy.d.ts +27 -0
  29. package/dist/auth/rls-scope.d.ts +9 -1
  30. package/dist/auth/routes.d.ts +8 -1
  31. package/dist/auth/seed-admin.d.ts +73 -0
  32. package/dist/auth/session-routes.d.ts +10 -0
  33. package/dist/{auth-DkbzMZUS.js → auth-BRiOuyq8.js} +283 -198
  34. package/dist/auth-BRiOuyq8.js.map +1 -0
  35. package/dist/{backup-CRdZkA6c.js → backup-C8P6Cl3G.js} +2 -2
  36. package/dist/{backup-CRdZkA6c.js.map → backup-C8P6Cl3G.js.map} +1 -1
  37. package/dist/boot/boot.d.ts +18 -0
  38. package/dist/boot/bundle.d.ts +29 -6
  39. package/dist/boot/ddl-bootstrap.d.ts +2 -0
  40. package/dist/boot/env.d.ts +30 -1
  41. package/dist/boot/fetch-bundle.d.ts +24 -1
  42. package/dist/boot/options.d.ts +0 -8
  43. package/dist/boot/provision.d.ts +58 -18
  44. package/dist/boot/resource-adapters.d.ts +11 -1
  45. package/dist/boot/resource-resolvers.d.ts +96 -0
  46. package/dist/boot/sources.d.ts +27 -20
  47. package/dist/collections/validate-config.d.ts +29 -0
  48. package/dist/{contract-routes-CHPVKTZW.js → contract-routes-CusnEB5h.js} +24 -9
  49. package/dist/{contract-routes-CHPVKTZW.js.map → contract-routes-CusnEB5h.js.map} +1 -1
  50. package/dist/cron/cron-loader.d.ts +7 -1
  51. package/dist/cron/cron-scheduler.d.ts +48 -2
  52. package/dist/{cron-loader-d9WMFENB.js → cron-loader-DnmIePn_.js} +23 -4
  53. package/dist/cron-loader-DnmIePn_.js.map +1 -0
  54. package/dist/{cron-routes-DSC-kZXI.js → cron-routes-D3x2ydMa.js} +9 -6
  55. package/dist/cron-routes-D3x2ydMa.js.map +1 -0
  56. package/dist/{cron-scheduler-BB82dWuU.js → cron-scheduler-BpPKpl4i.js} +148 -28
  57. package/dist/cron-scheduler-BpPKpl4i.js.map +1 -0
  58. package/dist/{cron-store-KeN8rN63.js → cron-store-CCQXwgVL.js} +5 -5
  59. package/dist/{cron-store-KeN8rN63.js.map → cron-store-CCQXwgVL.js.map} +1 -1
  60. package/dist/{ddl-bootstrap-5YZCZ8qk.js → ddl-bootstrap-CfNvxMuK.js} +26 -2
  61. package/dist/{ddl-bootstrap-5YZCZ8qk.js.map → ddl-bootstrap-CfNvxMuK.js.map} +1 -1
  62. package/dist/email/index.d.ts +1 -0
  63. package/dist/email/unconfigured.d.ts +20 -0
  64. package/dist/env.d.ts +25 -0
  65. package/dist/errors-HjfaPlvY.js +385 -0
  66. package/dist/errors-HjfaPlvY.js.map +1 -0
  67. package/dist/{function-loader-LLdmBFoL.js → function-loader-xnbDAPfa.js} +9 -3
  68. package/dist/{function-loader-LLdmBFoL.js.map → function-loader-xnbDAPfa.js.map} +1 -1
  69. package/dist/function-routes-gQ0EShVG.js +267 -0
  70. package/dist/function-routes-gQ0EShVG.js.map +1 -0
  71. package/dist/functions/define-function.d.ts +6 -3
  72. package/dist/functions/function-routes.d.ts +8 -4
  73. package/dist/functions/index.js +128 -23
  74. package/dist/functions/index.js.map +1 -1
  75. package/dist/{history-recorder-B2pL3X8b.js → history-recorder-B1FwXx9J.js} +3 -3
  76. package/dist/{history-recorder-B2pL3X8b.js.map → history-recorder-B1FwXx9J.js.map} +1 -1
  77. package/dist/{history-store-BUJ-37Sm.js → history-store-LHXaQywp.js} +2 -2
  78. package/dist/{history-store-BUJ-37Sm.js.map → history-store-LHXaQywp.js.map} +1 -1
  79. package/dist/index.d.ts +16 -4
  80. package/dist/index.es.js +16229 -7676
  81. package/dist/index.es.js.map +1 -1
  82. package/dist/init.d.ts +33 -13
  83. package/dist/{jobs-XXXcZrmj.js → jobs-DkkD9mPV.js} +5 -5
  84. package/dist/{jobs-XXXcZrmj.js.map → jobs-DkkD9mPV.js.map} +1 -1
  85. package/dist/{jwt-DNeOn39r.js → jwt-DkhXwMzR.js} +9 -314
  86. package/dist/jwt-DkhXwMzR.js.map +1 -0
  87. package/dist/keys-g8lbVC_o.js +230 -0
  88. package/dist/keys-g8lbVC_o.js.map +1 -0
  89. package/dist/{logger-DS03e908.js → logger-DO2PZc4i.js} +197 -18
  90. package/dist/logger-DO2PZc4i.js.map +1 -0
  91. package/dist/{logs-routes-DB72iQSr.js → logs-routes-CbsTpozn.js} +74 -6
  92. package/dist/logs-routes-CbsTpozn.js.map +1 -0
  93. package/dist/{openapi-generator-CpZp2Fai.js → openapi-generator-BIBbO1Tq.js} +221 -38
  94. package/dist/openapi-generator-BIBbO1Tq.js.map +1 -0
  95. package/dist/{proxy-CMymhnwG.js → proxy-Czngl3p9.js} +2 -2
  96. package/dist/{proxy-CMymhnwG.js.map → proxy-Czngl3p9.js.map} +1 -1
  97. package/dist/{query-parser-DqXNaBIH.js → query-parser-C68Q9EX4.js} +35 -6
  98. package/dist/{query-parser-DqXNaBIH.js.map → query-parser-C68Q9EX4.js.map} +1 -1
  99. package/dist/queues/runtime.d.ts +56 -0
  100. package/dist/{request-timeout-C8gkc-j7.js → request-timeout-DESvlfrS.js} +4 -6
  101. package/dist/request-timeout-DESvlfrS.js.map +1 -0
  102. package/dist/schema-edit/project-root.d.ts +4 -4
  103. package/dist/{schema-editor-routes-DNV_nBhW.js → schema-editor-routes-CcZKh50q.js} +4 -4
  104. package/dist/{schema-editor-routes-DNV_nBhW.js.map → schema-editor-routes-CcZKh50q.js.map} +1 -1
  105. package/dist/{schemas-DBxgjM9A.js → schemas-C3234HWE.js} +2746 -72
  106. package/dist/schemas-C3234HWE.js.map +1 -0
  107. package/dist/singleton.d.ts +6 -4
  108. package/dist/src-DHK4fHkw.js +1002 -0
  109. package/dist/src-DHK4fHkw.js.map +1 -0
  110. package/dist/{src-C9gKj23J.js → src-Dq-I3Ybx.js} +507 -54
  111. package/dist/src-Dq-I3Ybx.js.map +1 -0
  112. package/dist/storage/GCSStorageController.d.ts +5 -0
  113. package/dist/storage/LocalStorageController.d.ts +14 -0
  114. package/dist/storage/S3StorageController.d.ts +8 -0
  115. package/dist/storage/image-transform.d.ts +35 -0
  116. package/dist/storage/keys.d.ts +23 -0
  117. package/dist/storage/policies.d.ts +22 -0
  118. package/dist/storage/routes.d.ts +9 -0
  119. package/dist/storage/tus-handler.d.ts +43 -1
  120. package/dist/storage/types.d.ts +28 -0
  121. package/dist/types-BfKcm9do.js.map +1 -1
  122. package/dist/utils/dev-port.d.ts +18 -0
  123. package/dist/utils/logger.d.ts +60 -0
  124. package/dist/utils/request-logger.d.ts +2 -1
  125. package/package.json +30 -24
  126. package/dist/GCSStorageController-Bl7nmhdv.js.map +0 -1
  127. package/dist/S3StorageController-CWvvrRpF.js.map +0 -1
  128. package/dist/admin_block-BX6YULvJ.js.map +0 -1
  129. package/dist/ast-schema-editor-CvkYOvri.js.map +0 -1
  130. package/dist/auth-DkbzMZUS.js.map +0 -1
  131. package/dist/cron-loader-d9WMFENB.js.map +0 -1
  132. package/dist/cron-routes-DSC-kZXI.js.map +0 -1
  133. package/dist/cron-scheduler-BB82dWuU.js.map +0 -1
  134. package/dist/errors-DBwpj9N8.js +0 -231
  135. package/dist/errors-DBwpj9N8.js.map +0 -1
  136. package/dist/function-routes-ClT6UQpD.js +0 -41
  137. package/dist/function-routes-ClT6UQpD.js.map +0 -1
  138. package/dist/jwt-DNeOn39r.js.map +0 -1
  139. package/dist/logger-DS03e908.js.map +0 -1
  140. package/dist/logs-routes-DB72iQSr.js.map +0 -1
  141. package/dist/openapi-generator-CpZp2Fai.js.map +0 -1
  142. package/dist/request-timeout-C8gkc-j7.js.map +0 -1
  143. package/dist/schemas-DBxgjM9A.js.map +0 -1
  144. package/dist/src-C9gKj23J.js.map +0 -1
  145. package/dist/src-CcOl05Oq.js +0 -552
  146. package/dist/src-CcOl05Oq.js.map +0 -1
  147. package/dist/utils/logging.d.ts +0 -5
package/LICENSE CHANGED
@@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
22
-
package/README.md CHANGED
@@ -8,6 +8,10 @@ Database-agnostic backend core for Rebase.
8
8
  pnpm add @rebasepro/server
9
9
  ```
10
10
 
11
+ ESM-only: `"type": "module"` with no CommonJS build, so it is loaded with
12
+ `import`. `require()` of it resolves only on Node 22.12+, which supports
13
+ `require(esm)`.
14
+
11
15
  ## What This Package Does
12
16
 
13
17
  This is the central orchestrator for any Rebase backend. It provides the framework-level plumbing — HTTP routing (Hono), authentication middleware, storage, email, cron jobs, custom functions, and the REST API generator — without being coupled to any specific database. Database implementations are plugged in via driver packages like `@rebasepro/server-postgres` or `@rebasepro/server-mongo`.
@@ -24,7 +28,7 @@ This is the central orchestrator for any Rebase backend. It provides the framewo
24
28
  | `RebaseAuthConfig` | Auth config type (JWT, OAuth providers, hooks, service key). |
25
29
  | `RebaseBackendInstance` | Return type — includes `driver`, `healthCheck()`, `shutdown()`, `cronScheduler`, `storageController`, etc. |
26
30
  | `RebaseEnv` | Zod-inferred type of validated environment variables. |
27
- | `z` | Re-exported Zod instance for extending `loadEnv`. |
31
+ | `z` | The runtime's own Zod instance. Build `loadEnv({ extend })` schemas with this one — a schema from a second copy of zod is silently ignored. |
28
32
  | `_setRebaseMock` / `_resetRebaseMock` | Test helpers to mock the `rebase` singleton (NODE_ENV=test only). |
29
33
 
30
34
  Also re-exports all abstract interfaces (`DatabaseAdapter`, `AuthAdapter`, `DataDriver`, etc.), API types (`HonoEnv`, `ApiConfig`), auth module, email module, storage module, history module, cron module, custom functions, logging utilities, and driver registry.
@@ -27,12 +27,27 @@ Arguments:
27
27
  bundle-dir Path to the bundle. Defaults to $REBASE_BUNDLE, then ./dist-bundle
28
28
 
29
29
  Key environment variables:
30
+ NODE_ENV "production" for a deployment. It closes the first-admin
31
+ window, requires CORS_ORIGINS, refuses local file
32
+ storage and turns the OpenAPI docs off
30
33
  DATABASE_URL Connection string for the default database (required)
31
34
  JWT_SECRET Signing secret, >=32 chars (required in production)
35
+ REBASE_SERVICE_KEY Server-to-server credential that bypasses row-level
36
+ security. Treat it like a database superuser password
32
37
  PORT Port to bind (default 3001)
33
38
  CORS_ORIGINS Comma-separated allowed origins (required in production)
39
+ REBASE_ADMIN_EMAIL The first admin account, created once while the user
40
+ REBASE_ADMIN_PASSWORD table is empty (>=12 chars). In production the first
41
+ account to register is NOT promoted, so without these
42
+ a fresh deployment has no way in
43
+ DISABLE_SELF_REGISTRATION "true" to refuse sign-ups outright
34
44
  REBASE_METRICS "true" to expose Prometheus metrics at /metrics
35
- REBASE_MIGRATE_ON_BOOT none | ensure (collection tables: run 'rebase db push')
45
+ REBASE_MIGRATE_ON_BOOT none | ensure (default). "ensure" creates missing
46
+ tables and columns at boot, INCLUDING your collections',
47
+ additively, and applies their row-level security. It
48
+ never alters, drops or narrows: those go through
49
+ 'rebase db push' from a checkout, along with
50
+ junction-table RLS for many-to-many relations
36
51
 
37
52
  Additional databases and buckets are configured by suffixing the variable with
38
53
  the source key, e.g. DATABASE_URL__ANALYTICS or S3_BUCKET__MEDIA.
@@ -3,6 +3,7 @@ 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 { o as folderKey, s as listingPrefix } from "./keys-g8lbVC_o.js";
6
7
  import "./types-BfKcm9do.js";
7
8
  //#region src/storage/GCSStorageController.ts
8
9
  /**
@@ -48,6 +49,13 @@ var GCSStorageController = class {
48
49
  getType() {
49
50
  return "gcs";
50
51
  }
52
+ /**
53
+ * The configured bucket, and the logical `default` that maps to it.
54
+ * @see S3StorageController.knownBuckets
55
+ */
56
+ knownBuckets() {
57
+ return ["default", this.config.bucket];
58
+ }
51
59
  async putObject({ file, key, metadata, bucket }) {
52
60
  this.validateFile(file);
53
61
  const usedBucket = this.getBucket(bucket);
@@ -148,7 +156,7 @@ var GCSStorageController = class {
148
156
  async listObjects(prefix, options) {
149
157
  const resolvedBucket = this.getBucket(options?.bucket);
150
158
  const [files, , filesApiResponse] = await (await this.getClient()).bucket(resolvedBucket).getFiles({
151
- prefix: prefix || void 0,
159
+ prefix: listingPrefix(prefix),
152
160
  delimiter: "/",
153
161
  maxResults: options?.maxResults ?? 1e3,
154
162
  pageToken: options?.pageToken,
@@ -163,14 +171,17 @@ var GCSStorageController = class {
163
171
  root: null,
164
172
  toString: () => `gs://${resolvedBucket}/${file.name}`
165
173
  })),
166
- prefixes: (filesApiResponse?.prefixes ?? []).map((p) => ({
167
- bucket: resolvedBucket,
168
- fullPath: p,
169
- name: p.replace(/\/$/, "").split("/").pop() || "",
170
- parent: null,
171
- root: null,
172
- toString: () => `gs://${resolvedBucket}/${p}`
173
- })),
174
+ prefixes: (filesApiResponse?.prefixes ?? []).map((p) => {
175
+ const folder = folderKey(p);
176
+ return {
177
+ bucket: resolvedBucket,
178
+ fullPath: folder,
179
+ name: folder.split("/").pop() || "",
180
+ parent: null,
181
+ root: null,
182
+ toString: () => `gs://${resolvedBucket}/${folder}`
183
+ };
184
+ }),
174
185
  nextPageToken: filesApiResponse?.nextPageToken
175
186
  };
176
187
  }
@@ -214,4 +225,4 @@ function isNotFoundError(error) {
214
225
  //#endregion
215
226
  export { GCSStorageController_exports as n, GCSStorageController as t };
216
227
 
217
- //# sourceMappingURL=GCSStorageController-Bl7nmhdv.js.map
228
+ //# sourceMappingURL=GCSStorageController-ZXPoNqW3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GCSStorageController-ZXPoNqW3.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 async getSignedUrl(key: string, bucket?: string): Promise<DownloadConfig> {\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 // Get metadata first to check existence and populate response\n const [fileMetadata] = await gcsFile.getMetadata();\n\n // Generate a signed URL\n const expiresIn = this.config.signedUrlExpiration ?? 3600;\n const [url] = await gcsFile.getSignedUrl({\n action: \"read\",\n expires: Date.now() + expiresIn * 1000\n });\n\n const metadata: DownloadMetadata = {\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\n return {\n url,\n metadata\n };\n } catch (error: unknown) {\n if (isNotFoundError(error)) {\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 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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"}
@@ -3,6 +3,7 @@ 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 { o as folderKey, s as listingPrefix } from "./keys-g8lbVC_o.js";
6
7
  import "./types-BfKcm9do.js";
7
8
  //#region src/storage/S3StorageController.ts
8
9
  var S3StorageController_exports = /* @__PURE__ */ __exportAll({ S3StorageController: () => S3StorageController });
@@ -56,6 +57,16 @@ var S3StorageController = class {
56
57
  return "s3";
57
58
  }
58
59
  /**
60
+ * The configured bucket, and the logical `default` that maps to it.
61
+ *
62
+ * Any other name went straight to S3 as a bucket name, so the request
63
+ * parameter was a way to address *any* bucket the deployment's credentials
64
+ * can reach — and a mistyped one came back as "file not found".
65
+ */
66
+ knownBuckets() {
67
+ return ["default", this.config.bucket];
68
+ }
69
+ /**
59
70
  * Validate file before upload
60
71
  */
61
72
  validateFile(file) {
@@ -217,7 +228,7 @@ var S3StorageController = class {
217
228
  const client = await this.getClient();
218
229
  const command = new s3.ListObjectsV2Command({
219
230
  Bucket: resolvedBucket,
220
- Prefix: prefix || void 0,
231
+ Prefix: listingPrefix(prefix),
221
232
  MaxKeys: options?.maxResults ?? 1e3,
222
233
  ContinuationToken: options?.pageToken,
223
234
  Delimiter: "/"
@@ -232,14 +243,17 @@ var S3StorageController = class {
232
243
  root: null,
233
244
  toString: () => `s3://${resolvedBucket}/${obj.Key}`
234
245
  })),
235
- prefixes: (response.CommonPrefixes || []).map((prefix) => ({
236
- bucket: resolvedBucket,
237
- fullPath: prefix.Prefix || "",
238
- name: (prefix.Prefix || "").replace(/\/$/, "").split("/").pop() || "",
239
- parent: null,
240
- root: null,
241
- toString: () => `s3://${resolvedBucket}/${prefix.Prefix}`
242
- })),
246
+ prefixes: (response.CommonPrefixes || []).map((common) => {
247
+ const folder = folderKey(common.Prefix || "");
248
+ return {
249
+ bucket: resolvedBucket,
250
+ fullPath: folder,
251
+ name: folder.split("/").pop() || "",
252
+ parent: null,
253
+ root: null,
254
+ toString: () => `s3://${resolvedBucket}/${folder}`
255
+ };
256
+ }),
243
257
  nextPageToken: response.NextContinuationToken
244
258
  };
245
259
  }
@@ -247,4 +261,4 @@ var S3StorageController = class {
247
261
  //#endregion
248
262
  export { S3StorageController_exports as n, S3StorageController as t };
249
263
 
250
- //# sourceMappingURL=S3StorageController-CWvvrRpF.js.map
264
+ //# sourceMappingURL=S3StorageController-5pAXyv31.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"S3StorageController-5pAXyv31.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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"}
@@ -0,0 +1,36 @@
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
@@ -0,0 +1 @@
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"}
@@ -38,6 +38,7 @@ var ADMIN_COLLECTION_KEYS = [
38
38
  "Actions",
39
39
  "additionalFields",
40
40
  "alwaysApplyDefaultValues",
41
+ "browserCallbacks",
41
42
  "components",
42
43
  "customViews",
43
44
  "defaultEntityAction",
@@ -105,6 +106,7 @@ var ADMIN_PROPERTY_KEYS = [
105
106
  "Filter",
106
107
  "filterOperators",
107
108
  "fixedFilter",
109
+ "format",
108
110
  "hideFromCollection",
109
111
  "includeEntityLink",
110
112
  "includeId",
@@ -115,6 +117,7 @@ var ADMIN_PROPERTY_KEYS = [
115
117
  "previewAsTag",
116
118
  "previewProperties",
117
119
  "readOnly",
120
+ "renderInForm",
118
121
  "sortable",
119
122
  "span",
120
123
  "spreadChildren",
@@ -182,4 +185,4 @@ function nestAdminPropertyKeys(property) {
182
185
  //#endregion
183
186
  export { nestAdminPropertyKeys as i, ADMIN_PROPERTY_KEYS as n, nestAdminCollectionKeys as r, ADMIN_COLLECTION_KEYS as t };
184
187
 
185
- //# sourceMappingURL=admin_block-BX6YULvJ.js.map
188
+ //# sourceMappingURL=admin_block-0Xu0r6eZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin_block-0Xu0r6eZ.js","names":[],"sources":["../../types/src/types/admin_block.ts"],"sourcesContent":["/**\n * The keys of a collection's admin block, as data.\n *\n * There is no *type* for the block in this package any more, and that is the point:\n * `admin` is not declared on `BaseCollectionConfig` or on any property here, so a\n * BaaS install cannot even write one. `@rebasepro/cms-types` adds the field back by\n * declaration merging, which is why installing it is what makes the admin surface\n * appear.\n *\n * The *list* still has to live here, because three runtime consumers need it and two\n * of them are core — see below.\n */\n\n/**\n * Every key that belongs inside a collection's `admin` block, as data.\n *\n * The type that describes these fields is `AdminCollectionOptions` in\n * `@rebasepro/cms-types`, and it is erased at build time — but three runtime\n * consumers need the list, and two of them are core:\n *\n * - `serializeCollections`, to drop the block from the contract\n * - the ts-morph schema editor in `@rebasepro/server`, which rewrites collection\n * files on disk from the admin panel and has to know where each key goes. A key\n * missing from this list gets written to the *top level* of the file, where the\n * backend ignores it and the panel never finds it again.\n * - the `collections-admin-block` codemod\n *\n * `@rebasepro/cms-types` re-exports this and asserts it names only real option\n * keys; the count is pinned by a test there.\n *\n * @group Models\n */\nexport const ADMIN_COLLECTION_KEYS = [\n \"Actions\",\n \"additionalFields\",\n \"alwaysApplyDefaultValues\",\n \"browserCallbacks\",\n \"components\",\n \"customViews\",\n \"defaultEntityAction\",\n \"defaultFilter\",\n \"defaultSelectedView\",\n \"defaultSize\",\n \"defaultViewMode\",\n \"disableDefaultActions\",\n \"display\",\n \"enabledViews\",\n \"entityActions\",\n \"entityViews\",\n \"exportable\",\n \"filterPresets\",\n \"fixedFilter\",\n \"form\",\n \"formAutoSave\",\n \"formView\",\n \"group\",\n \"hideFromEntityViews\",\n \"hideFromNavigation\",\n \"hideIdFromCollection\",\n \"hideIdFromForm\",\n \"icon\",\n \"includeJsonView\",\n \"inlineEditing\",\n \"kanban\",\n \"listProperties\",\n \"localChangesBackup\",\n \"openEntityMode\",\n \"orderProperty\",\n \"pagination\",\n \"previewProperties\",\n \"propertiesOrder\",\n \"selectionController\",\n \"selectionEnabled\",\n \"sideDialogWidth\",\n \"sort\"\n] as const;\n\n/** A key of a collection's `admin` block. @group Models */\nexport type AdminCollectionKey = typeof ADMIN_COLLECTION_KEYS[number];\n\n/**\n * Every key that belongs inside a *property's* `admin` block, as data.\n *\n * The union of `AdminPropertyOptions` and its per-type extensions\n * (`AdminStringOptions`, `AdminArrayOptions`, …) in `@rebasepro/cms-types`.\n * It lives here for the same reason {@link ADMIN_COLLECTION_KEYS} does: the\n * runtime consumers are core packages that the BaaS guard forbids from\n * importing `@rebasepro/cms-types`. Here it is the boot-time collection\n * validator in `@rebasepro/server`, which has to tell \"you left `readOnly` at\n * the top of the property, where nothing reads it\" apart from \"you invented a\n * key we have never heard of\".\n *\n * `@rebasepro/cms-types` re-exports this and asserts it names only real\n * option keys.\n *\n * @group Models\n */\nexport const ADMIN_PROPERTY_KEYS = [\n \"canAddElements\",\n \"clearable\",\n \"columnWidth\",\n \"customProps\",\n \"disabled\",\n \"expanded\",\n \"Field\",\n \"Filter\",\n \"filterOperators\",\n \"fixedFilter\",\n \"format\",\n \"hideFromCollection\",\n \"includeEntityLink\",\n \"includeId\",\n \"markdown\",\n \"minimalistView\",\n \"multiline\",\n \"Preview\",\n \"previewAsTag\",\n \"previewProperties\",\n \"readOnly\",\n \"renderInForm\",\n \"sortable\",\n \"span\",\n \"spreadChildren\",\n \"urlPreview\",\n \"widget\",\n] as const;\n\n/** A key of a property's `admin` block. @group Models */\nexport type AdminPropertyKey = typeof ADMIN_PROPERTY_KEYS[number];\n\n/**\n * Move flattened admin keys back down into the `admin` block.\n *\n * The admin panel works with a *flat* view model — the block merged onto the\n * collection — so what comes back from a form has `icon` and `defaultViewMode`\n * at the top level while `admin` still holds whatever the file was loaded with.\n * This is the way back.\n *\n * **The top-level value wins.** It is the one the form just wrote; the block is\n * the copy the collection was loaded with, and preferring it resolves every edit\n * in favour of the value the user changed away from.\n *\n * This lives here, next to the key lists, because it had two implementations —\n * `toAdminCollectionConfig` in `@rebasepro/cms-types` and `nestAdminKeys` in\n * `@rebasepro/server`'s schema editor — that agreed on everything except that\n * precedence, which is the only part that decides whether a save is visible.\n *\n * @group Models\n */\nexport function nestAdminKeysOf(\n source: Record<string, unknown>,\n adminKeys: readonly string[]\n): Record<string, unknown> {\n const keys = new Set<string>(adminKeys);\n const top: Record<string, unknown> = {};\n const block: Record<string, unknown> = { ...((source.admin as Record<string, unknown> | undefined) ?? {}) };\n\n for (const [key, value] of Object.entries(source)) {\n if (key === \"admin\") continue;\n if (keys.has(key)) block[key] = value;\n else top[key] = value;\n }\n\n if (Object.keys(block).length > 0) top.admin = block;\n return top;\n}\n\n/**\n * {@link nestAdminKeysOf} for a collection.\n *\n * @group Models\n */\nexport function nestAdminCollectionKeys(collection: Record<string, unknown>): Record<string, unknown> {\n return nestAdminKeysOf(collection, ADMIN_COLLECTION_KEYS);\n}\n\n/**\n * {@link nestAdminKeysOf} for a property, applied to its children too.\n *\n * A map property carries `properties`, an array property carries `of`, and both\n * hold properties with `admin` blocks of their own. A flat `readOnly` left on a\n * child is as dead — and as fatal at the next boot — as one left on the parent,\n * so the walk goes all the way down.\n *\n * @group Models\n */\nexport function nestAdminPropertyKeys(property: Record<string, unknown>): Record<string, unknown> {\n const nested = nestAdminKeysOf(property, ADMIN_PROPERTY_KEYS);\n\n const children = nested.properties;\n if (children && typeof children === \"object\" && !Array.isArray(children)) {\n nested.properties = Object.fromEntries(\n Object.entries(children as Record<string, unknown>).map(([key, child]) => [\n key,\n child && typeof child === \"object\" && !Array.isArray(child)\n ? nestAdminPropertyKeys(child as Record<string, unknown>)\n : child\n ])\n );\n }\n\n const of = nested.of;\n if (Array.isArray(of)) {\n nested.of = of.map(entry => entry && typeof entry === \"object\" && !Array.isArray(entry)\n ? nestAdminPropertyKeys(entry as Record<string, unknown>)\n : entry);\n } else if (of && typeof of === \"object\") {\n nested.of = nestAdminPropertyKeys(of as Record<string, unknown>);\n }\n\n return nested;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,IAAa,wBAAwB;CACjC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACJ;;;;;;;;;;;;;;;;;;AAsBA,IAAa,sBAAsB;CAC/B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACJ;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,gBACZ,QACA,WACuB;CACvB,MAAM,OAAO,IAAI,IAAY,SAAS;CACtC,MAAM,MAA+B,CAAC;CACtC,MAAM,QAAiC,EAAE,GAAK,OAAO,SAAiD,CAAC,EAAG;CAE1G,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAAG;EAC/C,IAAI,QAAQ,SAAS;EACrB,IAAI,KAAK,IAAI,GAAG,GAAG,MAAM,OAAO;OAC3B,IAAI,OAAO;CACpB;CAEA,IAAI,OAAO,KAAK,KAAK,CAAC,CAAC,SAAS,GAAG,IAAI,QAAQ;CAC/C,OAAO;AACX;;;;;;AAOA,SAAgB,wBAAwB,YAA8D;CAClG,OAAO,gBAAgB,YAAY,qBAAqB;AAC5D;;;;;;;;;;;AAYA,SAAgB,sBAAsB,UAA4D;CAC9F,MAAM,SAAS,gBAAgB,UAAU,mBAAmB;CAE5D,MAAM,WAAW,OAAO;CACxB,IAAI,YAAY,OAAO,aAAa,YAAY,CAAC,MAAM,QAAQ,QAAQ,GACnE,OAAO,aAAa,OAAO,YACvB,OAAO,QAAQ,QAAmC,CAAC,CAAC,KAAK,CAAC,KAAK,WAAW,CACtE,KACA,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,IACpD,sBAAsB,KAAgC,IACtD,KACV,CAAC,CACL;CAGJ,MAAM,KAAK,OAAO;CAClB,IAAI,MAAM,QAAQ,EAAE,GAChB,OAAO,KAAK,GAAG,KAAI,UAAS,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,IAChF,sBAAsB,KAAgC,IACtD,KAAK;MACR,IAAI,MAAM,OAAO,OAAO,UAC3B,OAAO,KAAK,sBAAsB,EAA6B;CAGnE,OAAO;AACX"}
@@ -1,5 +1,28 @@
1
1
  import type { ErrorHandler } from "hono";
2
2
  import type { HonoEnv } from "./types.js";
3
+ /**
4
+ * The schema-drift remedy, in the words that work on *this* database.
5
+ *
6
+ * The three copies of this message hard-coded `Run \`pnpm db:push\``, and on a
7
+ * stock scaffold — where the managed PGlite database is the default — that
8
+ * command answers `✗ rebase db push does not work on the managed development
9
+ * database.` and exits 1. So the one instruction the server gave when a
10
+ * developer's schema had drifted was a command their project refuses.
11
+ *
12
+ * Atlas plans a push by diffing against a second, empty database, and PGlite
13
+ * serves exactly one — which is why it cannot run there, and why the remedy
14
+ * has to know which database is under this run. There, boot applies additive
15
+ * changes, so restarting `rebase dev` *is* the fix.
16
+ *
17
+ * `REBASE_DEV_DATABASE_KIND` is set by the CLI from the database it resolved,
18
+ * the same variable `rebase schema generate`'s closing line already branches
19
+ * on. Absent — a deployed backend, a container, anything not started by
20
+ * `rebase dev` — the answer is the general one.
21
+ */
22
+ export declare function schemaDriftRemedy(): {
23
+ short: string;
24
+ lines: string[];
25
+ };
3
26
  /**
4
27
  * Standardized API error class.
5
28
  * Throw this from any route handler — the errorHandler middleware
@@ -56,6 +79,20 @@ export interface ErrorResponse {
56
79
  details?: unknown;
57
80
  /** Request correlation ID for tracing (echoes X-Request-ID). */
58
81
  requestId?: string;
82
+ /**
83
+ * Why this request was going to fail whatever it asked for.
84
+ *
85
+ * Only `SCHEMA_DRIFT` today: the caller's `x-rebase-schema` stamp is
86
+ * older than this backend's, so a 400 naming an unknown field is very
87
+ * likely a rename the client has not regenerated for. The error itself
88
+ * is unchanged — this explains it, it does not cause it.
89
+ */
90
+ cause?: {
91
+ code: string;
92
+ message: string;
93
+ clientSchema: string;
94
+ serverSchema: string;
95
+ };
59
96
  };
60
97
  }
61
98
  /**
@@ -60,6 +60,20 @@ export interface LiveSchemaRoutesConfig {
60
60
  path: string;
61
61
  contents: string;
62
62
  }[]>;
63
+ /**
64
+ * Why {@link writeSource} is absent, when the caller knows.
65
+ *
66
+ * `writeSource` being undefined has several causes — the editor turned off,
67
+ * `baas` mode, no `collectionsDir`, `NODE_ENV=production`, `ts-morph` not
68
+ * installed — and this route could see only that the field was unset, so it
69
+ * reported the last of them for all of them. The admin panel calls this
70
+ * *and* `/api/schema-editor/status` on every page load and got two
71
+ * different answers, one of which named a dependency that was installed.
72
+ */
73
+ sourceEditingOff?: {
74
+ code: string;
75
+ message: string;
76
+ };
63
77
  /**
64
78
  * Which paths {@link writeSource} will touch, without touching them.
65
79
  *
@@ -58,6 +58,14 @@ export interface LogMiddlewareOptions {
58
58
  }
59
59
  /** Hono middleware to log API requests */
60
60
  export declare function logMiddleware(options?: LogMiddlewareOptions): MiddlewareHandler<HonoEnv>;
61
+ /**
62
+ * The `source` for a teed line, from whatever prefix it carries.
63
+ *
64
+ * Exported for its test: the mapping is a string match on wording somebody else
65
+ * writes, which is the shape of check that stops working without failing.
66
+ */
67
+ export declare function sourceForMessage(message: string): LogEntry["source"];
68
+ export declare function teeLoggerIntoLogBuffer(): () => void;
61
69
  /**
62
70
  * The stream's timings, injectable only so they can be tested.
63
71
  *
@@ -32,6 +32,39 @@ export declare class RestApiGenerator {
32
32
  * Generate REST routes using existing DataDriver
33
33
  */
34
34
  generateRoutes(): Hono<HonoEnv>;
35
+ /**
36
+ * A 404 that says which row was not found, and names the other reason.
37
+ *
38
+ * "Entity not found" was the entire message on five routes. It does not say
39
+ * which collection, which id, or — the part that costs the most time — that
40
+ * a row can be perfectly present and invisible: authenticated requests run
41
+ * as a restricted role, so a `SELECT` policy that excludes this caller
42
+ * produces exactly this 404. Somebody checking whether the row exists finds
43
+ * it in psql, concludes the API is broken, and goes looking in the wrong
44
+ * place.
45
+ *
46
+ * The collection and the id are both in the URL the caller just sent, so
47
+ * naming them back reveals nothing. `details` is withheld in production
48
+ * anyway, on the same principle as the database diagnostics one layer up:
49
+ * structured fields are for the people building against this, and a
50
+ * deployed API answers strangers.
51
+ */
52
+ private entityNotFound;
53
+ /**
54
+ * Record which collection a request is about, before anything can fail.
55
+ *
56
+ * One middleware rather than a `c.set` in each of the fifteen handlers,
57
+ * because the value has to be there for the ones that throw *before*
58
+ * reaching a handler body — an API-key permission check, a query parser
59
+ * refusing an operator — which is exactly the set of requests whose log
60
+ * line is worth reading.
61
+ *
62
+ * The slug is validated against the collections this backend serves, so the
63
+ * field is always a real collection and never whatever a caller typed. An
64
+ * unknown slug leaves it unset; the path is still logged, and the
65
+ * unmatched-route handler already says the collection does not exist.
66
+ */
67
+ private nameTheCollection;
35
68
  /**
36
69
  * Answer anything left under `/api/data` in the canonical error envelope.
37
70
  *
@@ -0,0 +1,62 @@
1
+ import { Hono } from "hono";
2
+ import type { HonoEnv } from "./types.js";
3
+ /**
4
+ * Give the root app the JSON error envelope, unless it already has one.
5
+ *
6
+ * `errorHandler` was installed on the data router and the functions router and
7
+ * nowhere else, so anything thrown outside those two — an app-level middleware,
8
+ * an auth route, a storage route, a custom route a project mounted itself — fell
9
+ * through to Hono's default: `500` with the body `Internal Server Error`, as
10
+ * `text/plain`. A client that parses `error.code` got a JSON parse failure
11
+ * instead of a code, which is the one shape no caller handles.
12
+ *
13
+ * The check is deliberate rather than an unconditional `onError`. An
14
+ * application that passes its own `config.app` may have installed a handler of
15
+ * its own, and overwriting it would silently replace the project's error
16
+ * contract with ours — a much worse failure than the one this fixes, because it
17
+ * would look like the framework working.
18
+ *
19
+ * Identity against a fresh `Hono` is what distinguishes the two: Hono's default
20
+ * handler is one module-level function shared by every instance, so an app that
21
+ * has never been given a handler compares equal to a brand-new one, and an app
22
+ * that has compares unequal. It reads a private field, which is why it is here,
23
+ * behind a name, with a test that fails if Hono ever stops working this way.
24
+ */
25
+ export declare function installRootErrorHandler(app: Hono<HonoEnv>): boolean;
26
+ /**
27
+ * And the same envelope for a path under `basePath` that matched nothing.
28
+ *
29
+ * `onError` was only ever half of it. A request that matches no route is not a
30
+ * thrown error — Hono answers it from its not-found handler, which `onError`
31
+ * never sees — so every unmatched path under `/api` came back as `404 Not
32
+ * Found`, `text/plain`. `backend/errors.md` opens by promising that *every*
33
+ * failure uses one envelope and carries a stable `code`, and
34
+ * `troubleshooting.md` says it again; a client parsing `error.code` got a JSON
35
+ * parse failure instead, which is the one shape no caller handles. Through the
36
+ * SDK it arrived as `RebaseApiError { code: undefined }`, so `e.code ===
37
+ * "NOT_FOUND"` never matched.
38
+ *
39
+ * Written as middleware that inspects the answer, rather than as `app.notFound`
40
+ * or as a catch-all route, because both of those are wrong here:
41
+ *
42
+ * - `app.notFound` would *replace* the handler of an application that passes its
43
+ * own `config.app`. Hono keeps that handler in a `#private` field, so — unlike
44
+ * the error handler, which is readable and therefore respected — there is no
45
+ * way to tell an app that has one from an app that has the default.
46
+ * - A catch-all route depends on being registered after every real route, and
47
+ * `boot.ts` registers `/api/health` *after* `initializeRebaseBackend` returns.
48
+ * A catch-all would have swallowed the health probe.
49
+ *
50
+ * Post-processing has neither problem: it converts a 404 that came back as
51
+ * anything but JSON, and leaves the ones that are already an envelope — a
52
+ * storage miss, an unknown collection, an unknown function — exactly as their
53
+ * route wrote them. Scoped to `basePath`, so an app serving its own pages at `/`
54
+ * keeps answering its own way for the pages.
55
+ *
56
+ * `expected`, so it logs at debug. A 404 from a frontend holding a stale link is
57
+ * routine, and a `⚠️` per request would trade a missing envelope for noise
58
+ * nobody can act on.
59
+ */
60
+ export declare function installUnmatchedApiEnvelope(app: Hono<HonoEnv>, basePath: string): void;
61
+ /** Whether `onError` has been called on this app. */
62
+ export declare function hasOwnErrorHandler(app: Hono<never> | Hono<HonoEnv>): boolean;
@@ -0,0 +1,39 @@
1
+ import type { MiddlewareHandler } from "hono";
2
+ import type { HonoEnv } from "./types.js";
3
+ /**
4
+ * What a request said about the schema it was built against, and what this
5
+ * server's schema actually is.
6
+ *
7
+ * Both stamps, always — "your SDK is stale" is not actionable without the two
8
+ * numbers to compare and quote.
9
+ */
10
+ export interface SchemaDrift {
11
+ /** The stamp the caller's generated SDK carries. */
12
+ client: string;
13
+ /** The stamp this backend's collections hash to. */
14
+ server: string;
15
+ }
16
+ /**
17
+ * Notice that a caller was generated against an older schema than this one.
18
+ *
19
+ * The header has been documented for a while — "echoed by a generated SDK in
20
+ * the `x-rebase-schema` header … so the platform can say 'this app was built
21
+ * against an older schema' instead of failing mysteriously at the first
22
+ * request" — and the sender became real when `generate-sdk` started emitting
23
+ * `schema.meta.ts`. Nothing read it. A renamed column therefore answered a
24
+ * generated client with a bare 400 naming a field the client's own types say
25
+ * exists, which is the exact confusion the header was introduced to prevent.
26
+ *
27
+ * This does not refuse anything. Drift is *not* an error: an SDK a schema older
28
+ * than the server's is usually still perfectly compatible — nothing it knows
29
+ * about was touched — and a middleware that rejected on a version mismatch
30
+ * would break every deploy where the backend ships before the frontend. So it
31
+ * only records the fact, and the error handler attaches it as the cause of a
32
+ * 400 or 404 that would have been returned anyway. A request that succeeds
33
+ * never learns it was stale.
34
+ *
35
+ * `resolveServerVersion` is a thunk, called at most once: computing a stamp
36
+ * walks and canonicalizes every collection, and a project whose bundle recorded
37
+ * one at build time (all of them but `baas`) answers without computing at all.
38
+ */
39
+ export declare function createSchemaDriftDetector(resolveServerVersion: () => string | undefined): MiddlewareHandler<HonoEnv>;