@rebasepro/server 0.16.0 → 0.16.1-canary.g2d1aec8

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 (160) hide show
  1. package/dist/api/contract-routes.d.ts +1 -1
  2. package/dist/api/errors.d.ts +1 -1
  3. package/dist/api/index.d.ts +3 -3
  4. package/dist/api/live-schema-routes.d.ts +111 -0
  5. package/dist/api/logs-routes.d.ts +1 -1
  6. package/dist/api/mount.d.ts +41 -0
  7. package/dist/api/rest/api-generator.d.ts +2 -2
  8. package/dist/api/rest/index.d.ts +1 -1
  9. package/dist/api/rest/query-parser.d.ts +1 -1
  10. package/dist/api/schema-editor-routes.d.ts +1 -1
  11. package/dist/api/types.d.ts +3 -4
  12. package/dist/{schema-editor-routes-CV9k0w3G.js → ast-schema-editor-BpFYpT38.js} +2 -39
  13. package/dist/ast-schema-editor-BpFYpT38.js.map +1 -0
  14. package/dist/auth/adapter-middleware.d.ts +2 -2
  15. package/dist/auth/admin-roles-route.d.ts +2 -2
  16. package/dist/auth/admin-user-ops.d.ts +3 -3
  17. package/dist/auth/admin-users-route.d.ts +4 -4
  18. package/dist/auth/api-keys/api-key-middleware.d.ts +2 -2
  19. package/dist/auth/api-keys/api-key-permission-guard.d.ts +1 -1
  20. package/dist/auth/api-keys/api-key-routes.d.ts +2 -2
  21. package/dist/auth/api-keys/api-key-store.d.ts +1 -1
  22. package/dist/auth/api-keys/index.d.ts +9 -9
  23. package/dist/auth/apple-oauth.d.ts +2 -2
  24. package/dist/auth/auth-hooks.d.ts +3 -3
  25. package/dist/auth/bitbucket-oauth.d.ts +2 -2
  26. package/dist/auth/builtin-auth-adapter.d.ts +7 -4
  27. package/dist/auth/captcha.d.ts +86 -0
  28. package/dist/auth/cookie-utils.d.ts +2 -2
  29. package/dist/auth/discord-oauth.d.ts +2 -2
  30. package/dist/auth/facebook-oauth.d.ts +2 -2
  31. package/dist/auth/github-oauth.d.ts +2 -2
  32. package/dist/auth/gitlab-oauth.d.ts +2 -2
  33. package/dist/auth/google-oauth.d.ts +1 -1
  34. package/dist/auth/index.d.ts +56 -54
  35. package/dist/auth/jwks-routes.d.ts +1 -1
  36. package/dist/auth/jwt.d.ts +1 -1
  37. package/dist/auth/linkedin-oauth.d.ts +2 -2
  38. package/dist/auth/magic-link-routes.d.ts +10 -3
  39. package/dist/auth/mfa-gate.d.ts +1 -1
  40. package/dist/auth/mfa-routes.d.ts +3 -3
  41. package/dist/auth/microsoft-oauth.d.ts +2 -2
  42. package/dist/auth/middleware.d.ts +5 -5
  43. package/dist/auth/rate-limiter.d.ts +2 -2
  44. package/dist/auth/require-auth.d.ts +1 -1
  45. package/dist/auth/reset-password-admin.d.ts +4 -4
  46. package/dist/auth/routes.d.ts +13 -4
  47. package/dist/auth/session-routes.d.ts +3 -3
  48. package/dist/auth/slack-oauth.d.ts +2 -2
  49. package/dist/auth/spotify-oauth.d.ts +2 -2
  50. package/dist/auth/token-revocation.d.ts +2 -2
  51. package/dist/auth/twitter-oauth.d.ts +2 -2
  52. package/dist/{auth-5Et5mnUA.js → auth-C74dqR2B.js} +250 -15
  53. package/dist/auth-C74dqR2B.js.map +1 -0
  54. package/dist/backup/backup-common.d.ts +1 -1
  55. package/dist/backup/backup-routes.d.ts +3 -3
  56. package/dist/backup/index.d.ts +3 -3
  57. package/dist/{backup-C6ljYVTp.js → backup-BJ86ah4T.js} +2 -2
  58. package/dist/{backup-C6ljYVTp.js.map → backup-BJ86ah4T.js.map} +1 -1
  59. package/dist/boot/boot.d.ts +5 -5
  60. package/dist/boot/bundle.d.ts +1 -1
  61. package/dist/boot/driver.d.ts +1 -1
  62. package/dist/boot/env.d.ts +13 -1
  63. package/dist/boot/options.d.ts +30 -6
  64. package/dist/boot/role.d.ts +3 -2
  65. package/dist/boot/sources.d.ts +1 -1
  66. package/dist/collections/BackendCollectionRegistry.d.ts +1 -1
  67. package/dist/collections/loader.d.ts +1 -1
  68. package/dist/{contract-routes-DZ-LBpSL.js → contract-routes-BEq7euZg.js} +2 -2
  69. package/dist/contract-routes-BEq7euZg.js.map +1 -0
  70. package/dist/cron/cron-routes.d.ts +2 -2
  71. package/dist/cron/cron-scheduler.d.ts +2 -2
  72. package/dist/cron/index.d.ts +8 -8
  73. package/dist/{cron-loader-YhhQeVBM.js → cron-loader-BMvtW6-J.js} +2 -2
  74. package/dist/{cron-loader-YhhQeVBM.js.map → cron-loader-BMvtW6-J.js.map} +1 -1
  75. package/dist/{cron-routes-maM_RlUu.js → cron-routes-BvYk-Kmi.js} +2 -2
  76. package/dist/{cron-routes-maM_RlUu.js.map → cron-routes-BvYk-Kmi.js.map} +1 -1
  77. package/dist/{cron-scheduler-DIpYBmZP.js → cron-scheduler-Buf-uVam.js} +2 -2
  78. package/dist/{cron-scheduler-DIpYBmZP.js.map → cron-scheduler-Buf-uVam.js.map} +1 -1
  79. package/dist/{cron-store-DfH_4Cd9.js → cron-store-CyF-oInV.js} +3 -3
  80. package/dist/{cron-store-DfH_4Cd9.js.map → cron-store-CyF-oInV.js.map} +1 -1
  81. package/dist/{ddl-bootstrap-Cywoj8Ta.js → ddl-bootstrap-DgSnERYN.js} +17 -3
  82. package/dist/ddl-bootstrap-DgSnERYN.js.map +1 -0
  83. package/dist/dev-secrets.d.ts +52 -0
  84. package/dist/email/dev-sink.d.ts +67 -0
  85. package/dist/email/index.d.ts +9 -7
  86. package/dist/email/link-base.d.ts +1 -1
  87. package/dist/email/smtp-email-service.d.ts +1 -1
  88. package/dist/env.d.ts +1 -1
  89. package/dist/{errors-EBYiaJ2E.js → errors-D_LwNKRM.js} +5 -5
  90. package/dist/errors-D_LwNKRM.js.map +1 -0
  91. package/dist/{function-loader-DDS1v7YX.js → function-loader-BrLmC_-y.js} +3 -3
  92. package/dist/{function-loader-DDS1v7YX.js.map → function-loader-BrLmC_-y.js.map} +1 -1
  93. package/dist/functions/context.d.ts +141 -0
  94. package/dist/functions/define-function.d.ts +1 -1
  95. package/dist/functions/function-routes.d.ts +2 -2
  96. package/dist/functions/guards.d.ts +76 -0
  97. package/dist/functions/index.d.ts +96 -5
  98. package/dist/functions/index.js +919 -0
  99. package/dist/functions/index.js.map +1 -0
  100. package/dist/functions/internal.d.ts +26 -0
  101. package/dist/functions/proxy.d.ts +1 -1
  102. package/dist/functions/request-timeout.d.ts +9 -1
  103. package/dist/functions/runtime-env.d.ts +92 -0
  104. package/dist/functions/wait-until.d.ts +74 -0
  105. package/dist/history/history-routes.d.ts +2 -2
  106. package/dist/history/index.d.ts +1 -1
  107. package/dist/index.d.ts +63 -54
  108. package/dist/index.es.js +2183 -164
  109. package/dist/index.es.js.map +1 -1
  110. package/dist/init/docs.d.ts +1 -1
  111. package/dist/init/middlewares.d.ts +1 -1
  112. package/dist/init/shutdown.d.ts +4 -0
  113. package/dist/init/storage.d.ts +1 -1
  114. package/dist/init/surfaces.d.ts +10 -0
  115. package/dist/init.d.ts +104 -18
  116. package/dist/jobs/index.d.ts +5 -5
  117. package/dist/{jobs-CyOKXXlu.js → jobs-Bn6nhY8J.js} +3 -3
  118. package/dist/{jobs-CyOKXXlu.js.map → jobs-Bn6nhY8J.js.map} +1 -1
  119. package/dist/{jwt-DxH9fLPt.js → jwt-BbJi0TR0.js} +2 -2
  120. package/dist/{jwt-DxH9fLPt.js.map → jwt-BbJi0TR0.js.map} +1 -1
  121. package/dist/{logger-DfvF_8r-.js → logger-TdvXIGqR.js} +101 -8
  122. package/dist/logger-TdvXIGqR.js.map +1 -0
  123. package/dist/metrics/index.d.ts +1 -1
  124. package/dist/{proxy-Bj5DVllb.js → proxy-QJKSS-CV.js} +5 -3
  125. package/dist/{proxy-Bj5DVllb.js.map → proxy-QJKSS-CV.js.map} +1 -1
  126. package/dist/{request-timeout-BuFoEKwT.js → request-timeout-OofPCHQT.js} +17 -3
  127. package/dist/request-timeout-OofPCHQT.js.map +1 -0
  128. package/dist/rls-audit/index.d.ts +111 -0
  129. package/dist/schema-edit/apply-schema-change.d.ts +141 -0
  130. package/dist/schema-edit/github-repository.d.ts +67 -0
  131. package/dist/schema-edit/local-git-repository.d.ts +28 -0
  132. package/dist/schema-edit/project-root.d.ts +29 -0
  133. package/dist/schema-edit/remote-source.d.ts +25 -0
  134. package/dist/schema-edit/schema-edit-permissions.d.ts +129 -0
  135. package/dist/schema-editor-routes-yh2cQcdJ.js +44 -0
  136. package/dist/schema-editor-routes-yh2cQcdJ.js.map +1 -0
  137. package/dist/services/webhook-service.d.ts +1 -1
  138. package/dist/singleton.d.ts +7 -0
  139. package/dist/storage/GCSStorageController.d.ts +1 -1
  140. package/dist/storage/LocalStorageController.d.ts +1 -1
  141. package/dist/storage/S3StorageController.d.ts +1 -1
  142. package/dist/storage/cache-headers.d.ts +87 -0
  143. package/dist/storage/index.d.ts +11 -11
  144. package/dist/storage/policies.d.ts +88 -0
  145. package/dist/storage/range.d.ts +63 -0
  146. package/dist/storage/routes.d.ts +3 -3
  147. package/dist/storage/storage-registry.d.ts +1 -1
  148. package/dist/storage/tus-handler.d.ts +2 -2
  149. package/dist/utils/host.d.ts +58 -0
  150. package/dist/utils/logger.d.ts +0 -15
  151. package/dist/utils/request-id.d.ts +1 -1
  152. package/functions/package.json +24 -0
  153. package/package.json +13 -7
  154. package/dist/auth-5Et5mnUA.js.map +0 -1
  155. package/dist/contract-routes-DZ-LBpSL.js.map +0 -1
  156. package/dist/ddl-bootstrap-Cywoj8Ta.js.map +0 -1
  157. package/dist/errors-EBYiaJ2E.js.map +0 -1
  158. package/dist/logger-DfvF_8r-.js.map +0 -1
  159. package/dist/request-timeout-BuFoEKwT.js.map +0 -1
  160. package/dist/schema-editor-routes-CV9k0w3G.js.map +0 -1
package/dist/index.es.js CHANGED
@@ -4,43 +4,50 @@ __createRequire(import.meta.url);
4
4
  import { A as GeoPoint, M as RebaseApiError, N as RebaseClientError, O as EntityReference, S as toSnakeCase, _ as fieldKeyForColumn, b as buildCompositeId, c as collectAllPages, d as normalizeOrderBy, f as serializeOrderBy, g as getEffectiveSecurityRules, h as resolveDataSource, j as Vector, k as EntityRelation, l as paginateFind, m as createDataSourceRegistry, n as buildSdkData, o as serializeFilter, p as CollectionRegistry, s as serializeLogicalCondition, t as buildRoutedRebaseData, u as resolveFindWindow, v as findRelation, x as resolvePrimaryKeys, y as resolveCollectionRelations } from "./src-BPfYOeN4.js";
5
5
  import { c as DEFAULT_DATA_SOURCE_KEY, g as toCanonicalOp, i as getCollectionDataPath, m as parseRelationAggregateSort, n as RLS_ROLES_SQL, o as isPostgresCollectionConfig, r as RLS_UID_SQL, t as RLS_JWT_SQL } from "./src-CrCxd8km.js";
6
6
  import { n as ADMIN_PROPERTY_KEYS, t as ADMIN_COLLECTION_KEYS } from "./admin_block-DLILvzle.js";
7
- import { a as isDuplicateObjectRace, i as isConcurrentDdlRace, n as createDdlBootstrapper, o as revokeInternalTableSql, s as isSQLAdmin, t as CONCURRENT_DDL_SQLSTATES } from "./ddl-bootstrap-Cywoj8Ta.js";
8
- import { C as canonicalStorageKey, D as findStorageSuffixCollision, E as DEFAULT_STORAGE_SOURCE_KEY, O as normalizeStorageSources, S as canonicalStorageId, b as InvalidStorageKeyError, c as getJwks, d as hasAsymmetricSigningKey, i as generateDownloadToken, k as storageEnvSuffix, n as configureJwt, p as isJwtConfigured, v as normalizePemFromEnv, x as canonicalStorageBucket, y as InvalidStorageBucketError } from "./jwt-DxH9fLPt.js";
9
- import { n as createContractRoutes, r as computeSchemaVersion } from "./contract-routes-DZ-LBpSL.js";
10
- import { $ as createAdapterAuthMiddleware, A as _coercedNumber, B as validatePasswordStrength, C as providerVerifiedEmail, Ct as parseQueryOptions, D as _enum, E as ZodNumber, Et as isPublicStoragePath, F as createEmailService, G as getPasswordResetTemplate, H as generateSecurePassword, I as assertEmailLinkBases, J as resolveEmailBranding, K as getUserInvitationTemplate, L as resolveEmailLinkBase, M as createDataRateLimiter, N as MemoryRateLimitStore, O as object, P as SMTPEmailService, Q as raw, R as resolveAuthHooks, S as pkceTokenParams, St as parseGroupBy, T as createBuiltinAuthAdapter, Tt as PUBLIC_STORAGE_PREFIX, U as getEmailVerificationTemplate, V as verifyPassword, W as getMagicLinkTemplate, X as escapeHtml, Y as RawHtml, Z as html, _ as verifyOidcIdToken, _t as scopeDataDriver, a as resolveRateLimitStoreKind, at as publicObjectAuth, b as createGoogleProvider, bt as orderByEntriesToTuples, c as createSlackProvider, ct as requireAuth, d as createDiscordProvider, dt as createStorageApiKeyGuard, et as createAuthMiddleware, f as createTwitterProvider, ft as isApiKeyToken, g as tryVerifyOidcIdToken, gt as SERVICE_IDENTITY, h as createMicrosoftProvider, ht as safeCompare, i as createApiKeyStore, it as optionalAuth, j as DEFAULT_FUNCTIONS_ANONYMOUS_LIMIT, k as string, l as createBitbucketProvider, lt as createApiKeyPreAuth, m as createAppleProvider, mt as extractBearerToken, n as createCustomAuthAdapter, nt as extractUserFromToken, o as createSqlRateLimitStore, ot as queryTokenAuth, p as createFacebookProvider, pt as validateApiKey, q as getWelcomeEmailTemplate, r as createApiKeyRoutes, rt as fileTokenAuth, s as createSpotifyProvider, st as requireAdmin, tt as createRequireAuth, u as createGitLabProvider, ut as createFunctionApiKeyGuard, v as createGitHubProvider, vt as httpMethodToOperation, w as createJwksRoutes, wt as resolveListLimitParam, x as oauthCodeFlowSchema, xt as parseAggregateSelect, y as createLinkedinProvider, yt as isOperationAllowed, z as hashPassword } from "./auth-5Et5mnUA.js";
11
- import { t as logger } from "./logger-DfvF_8r-.js";
12
- import { n as errorHandler, t as ApiError } from "./errors-EBYiaJ2E.js";
7
+ import { a as isDuplicateObjectRace, c as isSchemaEditingAdmin, i as isConcurrentDdlRace, n as createDdlBootstrapper, o as revokeInternalTableSql, s as isSQLAdmin, t as CONCURRENT_DDL_SQLSTATES } from "./ddl-bootstrap-DgSnERYN.js";
8
+ import { C as canonicalStorageKey, D as findStorageSuffixCollision, E as DEFAULT_STORAGE_SOURCE_KEY, O as normalizeStorageSources, S as canonicalStorageId, b as InvalidStorageKeyError, c as getJwks, d as hasAsymmetricSigningKey, i as generateDownloadToken, k as storageEnvSuffix, n as configureJwt, p as isJwtConfigured, v as normalizePemFromEnv, x as canonicalStorageBucket, y as InvalidStorageBucketError } from "./jwt-BbJi0TR0.js";
9
+ import { n as createContractRoutes, r as computeSchemaVersion } from "./contract-routes-BEq7euZg.js";
10
+ import { $ as html, A as _coercedNumber, B as resolveAuthHooks, C as providerVerifiedEmail, Ct as orderByEntriesToTuples, D as _enum, Dt as resolveListLimitParam, E as ZodNumber, Et as parseQueryOptions, F as extractLinks, G as getEmailVerificationTemplate, H as validatePasswordStrength, I as SMTPEmailService, J as getUserInvitationTemplate, K as getMagicLinkTemplate, L as createEmailService, M as createDataRateLimiter, N as MemoryRateLimitStore, O as object, Ot as PUBLIC_STORAGE_PREFIX, P as createDevEmailSink, Q as escapeHtml, R as assertEmailLinkBases, S as pkceTokenParams, St as isOperationAllowed, T as createBuiltinAuthAdapter, Tt as parseGroupBy, U as verifyPassword, V as hashPassword, W as generateSecurePassword, X as resolveEmailBranding, Y as getWelcomeEmailTemplate, Z as RawHtml, _ as verifyOidcIdToken, _t as safeCompare, a as resolveRateLimitStoreKind, at as fileTokenAuth, b as createGoogleProvider, bt as hasAdministrativeRole, c as createSlackProvider, ct as queryTokenAuth, d as createDiscordProvider, dt as createApiKeyPreAuth, et as raw, f as createTwitterProvider, ft as createFunctionApiKeyGuard, g as tryVerifyOidcIdToken, gt as extractBearerToken, h as createMicrosoftProvider, ht as validateApiKey, i as createApiKeyStore, it as extractUserFromToken, j as DEFAULT_FUNCTIONS_ANONYMOUS_LIMIT, k as string, kt as isPublicStoragePath, l as createBitbucketProvider, lt as requireAdmin, m as createAppleProvider, mt as isApiKeyToken, n as createCustomAuthAdapter, nt as createAuthMiddleware, o as createSqlRateLimitStore, ot as optionalAuth, p as createFacebookProvider, pt as createStorageApiKeyGuard, q as getPasswordResetTemplate, r as createApiKeyRoutes, rt as createRequireAuth, s as createSpotifyProvider, st as publicObjectAuth, tt as createAdapterAuthMiddleware, u as createGitLabProvider, ut as requireAuth, v as createGitHubProvider, vt as SERVICE_IDENTITY, w as createJwksRoutes, wt as parseAggregateSelect, x as oauthCodeFlowSchema, xt as httpMethodToOperation, y as createLinkedinProvider, yt as scopeDataDriver, z as resolveEmailLinkBase } from "./auth-C74dqR2B.js";
11
+ import { r as hostEnv, t as logger } from "./logger-TdvXIGqR.js";
12
+ import { n as errorHandler, t as ApiError } from "./errors-D_LwNKRM.js";
13
13
  import { t as logMiddleware } from "./logs-routes-CWBLQj2l.js";
14
14
  import { n as DOCUMENT_MIME_TYPES, r as IMAGE_MIME_TYPES, t as DEFAULT_MAX_FILE_SIZE } from "./types-DSnOC4mF.js";
15
15
  import { t as S3StorageController } from "./S3StorageController-B2EDXXMH.js";
16
16
  import { t as GCSStorageController } from "./GCSStorageController-BEmDYFKc.js";
17
- import { n as loadFunctionsFromDirectory } from "./function-loader-DDS1v7YX.js";
17
+ import { n as loadFunctionsFromDirectory, r as loadFunctionsWithDiagnostics } from "./function-loader-BrLmC_-y.js";
18
18
  import { t as createFunctionRoutes } from "./function-routes-Btcez1T-.js";
19
- import { n as loadCronJobsFromDirectory, r as loadCronJobsWithDiagnostics } from "./cron-loader-YhhQeVBM.js";
20
- import { r as validateCronExpression, t as CronScheduler } from "./cron-scheduler-DIpYBmZP.js";
21
- import { t as createCronRoutes } from "./cron-routes-maM_RlUu.js";
22
- import { t as createCronStore } from "./cron-store-DfH_4Cd9.js";
23
- import { a as parseBackupTimestamp, i as parseBackupDestination, n as createBackupRoutes, o as readBackupBytes, r as listBackupObjects } from "./backup-C6ljYVTp.js";
24
- import { i as createJobStore, n as createJobQueue, r as defaultBackoff } from "./jobs-CyOKXXlu.js";
19
+ import "./proxy-QJKSS-CV.js";
20
+ import "./request-timeout-OofPCHQT.js";
25
21
  import { t as FunctionSelectionError } from "./selection-_z6TM1DB.js";
26
- import { createHash, randomBytes, randomUUID } from "node:crypto";
27
- import * as fs$2 from "fs";
22
+ import { n as loadCronJobsFromDirectory, r as loadCronJobsWithDiagnostics } from "./cron-loader-BMvtW6-J.js";
23
+ import { r as validateCronExpression, t as CronScheduler } from "./cron-scheduler-Buf-uVam.js";
24
+ import { t as createCronRoutes } from "./cron-routes-BvYk-Kmi.js";
25
+ import { t as createCronStore } from "./cron-store-CyF-oInV.js";
26
+ import { a as parseBackupTimestamp, i as parseBackupDestination, n as createBackupRoutes, o as readBackupBytes, r as listBackupObjects } from "./backup-BJ86ah4T.js";
27
+ import { i as createJobStore, n as createJobQueue, r as defaultBackoff } from "./jobs-Bn6nhY8J.js";
28
+ import { createHash, createSign, randomBytes, randomUUID } from "node:crypto";
29
+ import * as fs$3 from "fs";
28
30
  import fs, { existsSync } from "fs";
29
- import * as path$2 from "path";
31
+ import * as path$3 from "path";
30
32
  import path, { join } from "path";
31
33
  import { pathToFileURL } from "url";
32
34
  import { Hono } from "hono";
33
35
  import { promisify } from "util";
34
36
  import * as crypto$1 from "crypto";
35
37
  import { createHmac, randomUUID as randomUUID$1 } from "crypto";
38
+ import * as path$2 from "node:path";
36
39
  import path$1 from "node:path";
37
40
  import { bodyLimit } from "hono/body-limit";
38
41
  import { csrf } from "hono/csrf";
39
42
  import { compress } from "hono/compress";
40
43
  import fs$1 from "node:fs";
44
+ import * as fs$2 from "node:fs/promises";
41
45
  import fsp from "node:fs/promises";
42
46
  import os from "node:os";
43
47
  import { mkdir, open, unlink, writeFile } from "fs/promises";
48
+ import { execFile } from "node:child_process";
49
+ import { promisify as promisify$1 } from "node:util";
50
+ import { env, getRuntimeKey } from "hono/adapter";
44
51
  import { sql } from "drizzle-orm";
45
52
  import { lookup } from "node:dns/promises";
46
53
  import { isIP } from "node:net";
@@ -49,8 +56,14 @@ import { createServer } from "http";
49
56
  import { cors } from "hono/cors";
50
57
  import { secureHeaders } from "hono/secure-headers";
51
58
  import { getRequestListener } from "@hono/node-server";
52
- import { execFile } from "node:child_process";
53
- import { promisify as promisify$1 } from "node:util";
59
+ //#region ../types/src/types/schema_editing.ts
60
+ var DEFAULT_COMMIT_PATHS = {
61
+ schemaFile: "backend/src/schema.generated.ts",
62
+ ddlFile: "drizzle/schema.sql",
63
+ policiesFile: "drizzle/policies.sql",
64
+ searchFile: "drizzle/search.sql"
65
+ };
66
+ //#endregion
54
67
  //#region src/collections/BackendCollectionRegistry.ts
55
68
  /**
56
69
  * Backend-agnostic collection registry.
@@ -161,7 +174,7 @@ var PROPERTY_KEYS_BY_TYPE = {
161
174
  ],
162
175
  geopoint: [],
163
176
  binary: [],
164
- vector: ["dimensions"],
177
+ vector: ["dimensions", "index"],
165
178
  reference: [
166
179
  "isId",
167
180
  "path",
@@ -464,8 +477,8 @@ async function importModule(filePath) {
464
477
  /** Read `defaultSecurityRules` from a collections directory's index module. */
465
478
  async function readDefaults(directory) {
466
479
  for (const name of ["index.ts", "index.js"]) {
467
- const indexPath = path$2.join(directory, name);
468
- if (!fs$2.existsSync(indexPath)) continue;
480
+ const indexPath = path$3.join(directory, name);
481
+ if (!fs$3.existsSync(indexPath)) continue;
469
482
  try {
470
483
  return { defaultSecurityRules: (await importModule(indexPath)).defaultSecurityRules };
471
484
  } catch (err) {
@@ -502,23 +515,23 @@ function applyCollectionDefaults(collections, defaults) {
502
515
  * rejects.
503
516
  */
504
517
  async function loadCollectionsFromDirectory(source, options = {}) {
505
- const resolved = path$2.resolve(source);
518
+ const resolved = path$3.resolve(source);
506
519
  const validate = (collections) => {
507
520
  if (options.validate !== false) assertCollectionConfigs(collections, options.validate ?? {});
508
521
  return collections;
509
522
  };
510
- if (!fs$2.existsSync(resolved)) {
523
+ if (!fs$3.existsSync(resolved)) {
511
524
  logger.warn(`[collections] Not found: ${resolved}`);
512
525
  return [];
513
526
  }
514
- if (!fs$2.statSync(resolved).isDirectory()) {
527
+ if (!fs$3.statSync(resolved).isDirectory()) {
515
528
  const mod = await importModule(resolved);
516
529
  return validate(applyCollectionDefaults([...mod.backendCollections || mod.collections || []], { defaultSecurityRules: mod.defaultSecurityRules }));
517
530
  }
518
531
  const collections = [];
519
532
  const failures = [];
520
- for (const file of fs$2.readdirSync(resolved).filter(isCollectionFile)) try {
521
- const mod = await importModule(path$2.join(resolved, file));
533
+ for (const file of fs$3.readdirSync(resolved).filter(isCollectionFile)) try {
534
+ const mod = await importModule(path$3.join(resolved, file));
522
535
  if (mod?.default) collections.push(mod.default);
523
536
  else failures.push(`${file}: no default export`);
524
537
  } catch (err) {
@@ -2632,13 +2645,13 @@ function configureMiddlewares(app, basePath, isProduction, config) {
2632
2645
  /**
2633
2646
  * Local filesystem storage controller
2634
2647
  */
2635
- var mkdir$1 = promisify(fs$2.mkdir);
2636
- var writeFile$1 = promisify(fs$2.writeFile);
2637
- var readFile = promisify(fs$2.readFile);
2638
- var unlink$1 = promisify(fs$2.unlink);
2639
- var readdir = promisify(fs$2.readdir);
2640
- var stat = promisify(fs$2.stat);
2641
- var access = promisify(fs$2.access);
2648
+ var mkdir$1 = promisify(fs$3.mkdir);
2649
+ var writeFile$1 = promisify(fs$3.writeFile);
2650
+ var readFile = promisify(fs$3.readFile);
2651
+ var unlink$1 = promisify(fs$3.unlink);
2652
+ var readdir = promisify(fs$3.readdir);
2653
+ var stat = promisify(fs$3.stat);
2654
+ var access = promisify(fs$3.access);
2642
2655
  /**
2643
2656
  * Remove initial and trailing slashes from a path.
2644
2657
  * Handles paths like "/images/", "images/", "/images" → "images"
@@ -2658,7 +2671,7 @@ var LocalStorageController = class {
2658
2671
  basePath;
2659
2672
  constructor(config) {
2660
2673
  this.config = config;
2661
- this.basePath = path$2.resolve(config.basePath);
2674
+ this.basePath = path$3.resolve(config.basePath);
2662
2675
  }
2663
2676
  getType() {
2664
2677
  return "local";
@@ -2699,10 +2712,10 @@ var LocalStorageController = class {
2699
2712
  * depend on it having run.
2700
2713
  */
2701
2714
  getFullPath(storagePath, bucket) {
2702
- const bucketPath = path$2.resolve(this.basePath, bucket || "default");
2703
- if (!bucketPath.startsWith(this.basePath + path$2.sep)) throw new Error("Path traversal detected: bucket resolves outside the storage root.");
2704
- const resolved = path$2.resolve(bucketPath, storagePath);
2705
- if (!resolved.startsWith(bucketPath + path$2.sep) && resolved !== bucketPath) throw new Error("Path traversal detected: resolved storage path is outside the bucket directory.");
2715
+ const bucketPath = path$3.resolve(this.basePath, bucket || "default");
2716
+ if (!bucketPath.startsWith(this.basePath + path$3.sep)) throw new Error("Path traversal detected: bucket resolves outside the storage root.");
2717
+ const resolved = path$3.resolve(bucketPath, storagePath);
2718
+ if (!resolved.startsWith(bucketPath + path$3.sep) && resolved !== bucketPath) throw new Error("Path traversal detected: resolved storage path is outside the bucket directory.");
2706
2719
  return resolved;
2707
2720
  }
2708
2721
  /**
@@ -2720,7 +2733,7 @@ var LocalStorageController = class {
2720
2733
  const usedBucket = bucket || "default";
2721
2734
  const fullStoragePath = key;
2722
2735
  const fullPath = this.getFullPath(fullStoragePath, usedBucket);
2723
- await this.ensureDir(path$2.dirname(fullPath));
2736
+ await this.ensureDir(path$3.dirname(fullPath));
2724
2737
  const arrayBuffer = await file.arrayBuffer();
2725
2738
  await writeFile$1(fullPath, Buffer.from(arrayBuffer));
2726
2739
  await writeFile$1(`${fullPath}.metadata.json`, JSON.stringify({
@@ -2749,7 +2762,7 @@ var LocalStorageController = class {
2749
2762
  resolvedPath = normalizeStoragePath(resolvedPath);
2750
2763
  const fullPath = this.getFullPath(resolvedPath, resolvedBucket);
2751
2764
  try {
2752
- await access(fullPath, fs$2.constants.R_OK);
2765
+ await access(fullPath, fs$3.constants.R_OK);
2753
2766
  } catch {
2754
2767
  return {
2755
2768
  url: null,
@@ -2765,7 +2778,7 @@ var LocalStorageController = class {
2765
2778
  metadata = {
2766
2779
  bucket: resolvedBucket ?? "default",
2767
2780
  fullPath: resolvedPath,
2768
- name: path$2.basename(resolvedPath),
2781
+ name: path$3.basename(resolvedPath),
2769
2782
  size: fileStat.size,
2770
2783
  contentType: savedMetadata.contentType || "application/octet-stream",
2771
2784
  customMetadata: savedMetadata
@@ -2776,7 +2789,7 @@ var LocalStorageController = class {
2776
2789
  metadata = {
2777
2790
  bucket: resolvedBucket ?? "default",
2778
2791
  fullPath: resolvedPath,
2779
- name: path$2.basename(resolvedPath),
2792
+ name: path$3.basename(resolvedPath),
2780
2793
  size: fileStat.size,
2781
2794
  contentType: "application/octet-stream",
2782
2795
  customMetadata: {}
@@ -2802,7 +2815,7 @@ var LocalStorageController = class {
2802
2815
  resolvedPath = normalizeStoragePath(resolvedPath);
2803
2816
  const fullPath = this.getFullPath(resolvedPath, resolvedBucket);
2804
2817
  try {
2805
- await access(fullPath, fs$2.constants.R_OK);
2818
+ await access(fullPath, fs$3.constants.R_OK);
2806
2819
  const buffer = await readFile(fullPath);
2807
2820
  let contentType = "application/octet-stream";
2808
2821
  try {
@@ -2810,7 +2823,7 @@ var LocalStorageController = class {
2810
2823
  contentType = JSON.parse(metadataContent).contentType || contentType;
2811
2824
  } catch {}
2812
2825
  const blob = new Blob([buffer], { type: contentType });
2813
- return new File([blob], path$2.basename(resolvedPath), { type: contentType });
2826
+ return new File([blob], path$3.basename(resolvedPath), { type: contentType });
2814
2827
  } catch {
2815
2828
  return null;
2816
2829
  }
@@ -2830,12 +2843,12 @@ var LocalStorageController = class {
2830
2843
  if (!resolvedPath) return;
2831
2844
  const fullPath = this.getFullPath(resolvedPath, resolvedBucket);
2832
2845
  try {
2833
- await access(fullPath, fs$2.constants.F_OK);
2846
+ await access(fullPath, fs$3.constants.F_OK);
2834
2847
  } catch {
2835
2848
  return;
2836
2849
  }
2837
2850
  try {
2838
- if ((await stat(fullPath)).isDirectory()) await fs$2.promises.rmdir(fullPath);
2851
+ if ((await stat(fullPath)).isDirectory()) await fs$3.promises.rmdir(fullPath);
2839
2852
  else {
2840
2853
  await unlink$1(fullPath);
2841
2854
  try {
@@ -2856,7 +2869,7 @@ var LocalStorageController = class {
2856
2869
  const items = [];
2857
2870
  const prefixes = [];
2858
2871
  try {
2859
- await access(fullPath, fs$2.constants.R_OK);
2872
+ await access(fullPath, fs$3.constants.R_OK);
2860
2873
  const entries = await readdir(fullPath, { withFileTypes: true });
2861
2874
  let count = 0;
2862
2875
  const maxResults = options?.maxResults ?? 1e3;
@@ -3032,6 +3045,138 @@ function isStorageController(obj) {
3032
3045
  return typeof controller.putObject === "function" && typeof controller.getSignedUrl === "function" && typeof controller.deleteObject === "function" && typeof controller.listObjects === "function" && typeof controller.getType === "function";
3033
3046
  }
3034
3047
  //#endregion
3048
+ //#region src/storage/cache-headers.ts
3049
+ /**
3050
+ * A weak validator built from size and modification time.
3051
+ *
3052
+ * Weak (`W/`) because it is not a digest of the body: two different bodies of
3053
+ * the same length written in the same millisecond would collide. That is the
3054
+ * same trade nginx makes for static files, and the alternative — hashing every
3055
+ * object on every request — costs more than it saves for content this server is
3056
+ * already streaming from somewhere else.
3057
+ */
3058
+ function buildEntityTag(size, lastModifiedMs) {
3059
+ return `W/"${size.toString(16)}-${Math.floor(lastModifiedMs).toString(16)}"`;
3060
+ }
3061
+ function objectValidators(size, lastModifiedMs) {
3062
+ const seconds = Math.floor(lastModifiedMs / 1e3) * 1e3;
3063
+ return {
3064
+ etag: buildEntityTag(size, lastModifiedMs),
3065
+ lastModified: new Date(seconds).toUTCString(),
3066
+ lastModifiedMs: seconds
3067
+ };
3068
+ }
3069
+ function cacheControl(options) {
3070
+ const parts = [options.isPublic ? "public" : "private", `max-age=${options.maxAgeSeconds}`];
3071
+ if (options.isPublic && options.staleWhileRevalidateSeconds) parts.push(`stale-while-revalidate=${options.staleWhileRevalidateSeconds}`);
3072
+ parts.push("must-revalidate");
3073
+ return parts.join(", ");
3074
+ }
3075
+ /** Every `ETag` in an `If-None-Match` header, unquoted comparison-ready. */
3076
+ function parseIfNoneMatch(header) {
3077
+ return header.split(",").map((part) => part.trim()).filter(Boolean);
3078
+ }
3079
+ /**
3080
+ * Weak comparison, which is the only comparison `If-None-Match` on a GET is
3081
+ * allowed to use (RFC 9110 §8.8.3.2). `W/"x"` and `"x"` are the same entity for
3082
+ * this purpose, so the prefix is stripped from both sides before comparing.
3083
+ */
3084
+ function weakMatch(a, b) {
3085
+ const strip = (v) => v.startsWith("W/") ? v.slice(2) : v;
3086
+ return strip(a) === strip(b);
3087
+ }
3088
+ /**
3089
+ * Whether the request already holds this exact representation.
3090
+ *
3091
+ * `If-None-Match` wins outright when present: it is an exact validator, whereas
3092
+ * `If-Modified-Since` has second resolution and is only consulted in its
3093
+ * absence — again the order RFC 9110 requires.
3094
+ */
3095
+ function isNotModified(c, validators) {
3096
+ const ifNoneMatch = c.req.header("if-none-match");
3097
+ if (ifNoneMatch) {
3098
+ const tags = parseIfNoneMatch(ifNoneMatch);
3099
+ return tags.includes("*") || tags.some((tag) => weakMatch(tag, validators.etag));
3100
+ }
3101
+ const ifModifiedSince = c.req.header("if-modified-since");
3102
+ if (ifModifiedSince) {
3103
+ const since = Date.parse(ifModifiedSince);
3104
+ if (!Number.isNaN(since)) return validators.lastModifiedMs <= since;
3105
+ }
3106
+ return false;
3107
+ }
3108
+ /**
3109
+ * Put the validators and the cache policy on a response.
3110
+ *
3111
+ * Applied to the 304 as well as the 200: a 304 that omits them tells the client
3112
+ * nothing about how long the answer holds, so the next load revalidates again
3113
+ * immediately and the round trip repeats forever.
3114
+ */
3115
+ function applyCacheHeaders(c, validators, options) {
3116
+ c.header("ETag", validators.etag);
3117
+ c.header("Last-Modified", validators.lastModified);
3118
+ c.header("Cache-Control", cacheControl(options));
3119
+ c.header("Vary", "Authorization");
3120
+ }
3121
+ //#endregion
3122
+ //#region src/storage/range.ts
3123
+ /**
3124
+ * Parse a `Range` header against an object of `size` bytes.
3125
+ *
3126
+ * Deliberately conservative: anything malformed, multi-range, or in a unit other
3127
+ * than bytes resolves to `none`, which serves the whole object. A range request
3128
+ * answered with `200` is always legal; a range answered wrongly is a corrupted
3129
+ * download.
3130
+ */
3131
+ function parseRange(header, size) {
3132
+ if (!header) return { kind: "none" };
3133
+ const match = /^bytes=(.*)$/i.exec(header.trim());
3134
+ if (!match) return { kind: "none" };
3135
+ const spec = match[1].trim();
3136
+ if (spec.includes(",")) return { kind: "none" };
3137
+ const parts = /^(\d*)-(\d*)$/.exec(spec);
3138
+ if (!parts) return { kind: "none" };
3139
+ const [, rawStart, rawEnd] = parts;
3140
+ if (size === 0) return { kind: "unsatisfiable" };
3141
+ let start;
3142
+ let end;
3143
+ if (rawStart === "") {
3144
+ if (rawEnd === "") return { kind: "none" };
3145
+ const suffix = Number(rawEnd);
3146
+ if (!Number.isSafeInteger(suffix)) return { kind: "none" };
3147
+ if (suffix === 0) return { kind: "unsatisfiable" };
3148
+ start = Math.max(0, size - suffix);
3149
+ end = size - 1;
3150
+ } else {
3151
+ start = Number(rawStart);
3152
+ if (!Number.isSafeInteger(start)) return { kind: "none" };
3153
+ if (start >= size) return { kind: "unsatisfiable" };
3154
+ if (rawEnd === "") end = size - 1;
3155
+ else {
3156
+ end = Number(rawEnd);
3157
+ if (!Number.isSafeInteger(end)) return { kind: "none" };
3158
+ end = Math.min(end, size - 1);
3159
+ }
3160
+ if (end < start) return { kind: "unsatisfiable" };
3161
+ }
3162
+ return {
3163
+ kind: "range",
3164
+ range: {
3165
+ start,
3166
+ end,
3167
+ length: end - start + 1
3168
+ }
3169
+ };
3170
+ }
3171
+ /** The `Content-Range` for a served range. */
3172
+ function contentRange(range, size) {
3173
+ return `bytes ${range.start}-${range.end}/${size}`;
3174
+ }
3175
+ /** The `Content-Range` for a `416`, which names the size and no range. */
3176
+ function unsatisfiableContentRange(size) {
3177
+ return `bytes */${size}`;
3178
+ }
3179
+ //#endregion
3035
3180
  //#region src/storage/image-transform.ts
3036
3181
  /**
3037
3182
  * Image Transformation Service
@@ -3893,11 +4038,20 @@ function createStorageRoutes(config) {
3893
4038
  await checkAuthorized(c, "read", resolvedPath, bucket, storageId);
3894
4039
  c.header("X-Content-Type-Options", "nosniff");
3895
4040
  const transformKeyPrefix = `${storageId || "(default)"}/${bucket}/${resolvedPath}`;
4041
+ const sharedCacheable = publicRead === true || isPublicStoragePath(resolvedPath);
4042
+ const cachePolicy = (maxAgeSeconds) => ({
4043
+ isPublic: sharedCacheable,
4044
+ maxAgeSeconds,
4045
+ staleWhileRevalidateSeconds: 86400
4046
+ });
4047
+ const OBJECT_MAX_AGE = 60;
4048
+ const TRANSFORM_MAX_AGE = 3600;
3896
4049
  const transformOpts = transformOptionsOrBadRequest(c.req.query());
3897
4050
  if (resolved.getType() === "local") {
3898
4051
  const absolutePath = resolved.getAbsolutePath(resolvedPath, bucket ?? "default");
4052
+ let localStat;
3899
4053
  try {
3900
- await fsp.access(absolutePath);
4054
+ localStat = await fsp.stat(absolutePath);
3901
4055
  } catch {
3902
4056
  throw ApiError.notFound("File not found");
3903
4057
  }
@@ -3908,31 +4062,75 @@ function createStorageRoutes(config) {
3908
4062
  contentType = JSON.parse(metadataRaw).contentType || contentType;
3909
4063
  } catch {}
3910
4064
  if (transformOpts && isTransformableImage(contentType)) {
3911
- const transformed = await transformOnce(transformCache.buildKey(transformKeyPrefix, transformOpts), transformOpts, async () => Buffer.from(await fsp.readFile(absolutePath)));
4065
+ const sourceVersion = buildEntityTag(localStat.size, localStat.mtimeMs);
4066
+ const transformed = await transformOnce(transformCache.buildKey(`${transformKeyPrefix}@${sourceVersion}`, transformOpts), transformOpts, async () => Buffer.from(await fsp.readFile(absolutePath)));
4067
+ const validators = objectValidators(transformed.data.byteLength, localStat.mtimeMs);
4068
+ applyCacheHeaders(c, validators, cachePolicy(TRANSFORM_MAX_AGE));
4069
+ if (isNotModified(c, validators)) return c.body(null, 304);
3912
4070
  c.header("Content-Type", transformed.contentType);
3913
- c.header("Cache-Control", "public, max-age=31536000, immutable");
3914
4071
  return c.body(new Uint8Array(transformed.data));
3915
4072
  }
3916
- const fileContent = await fsp.readFile(absolutePath);
4073
+ const validators = objectValidators(localStat.size, localStat.mtimeMs);
4074
+ applyCacheHeaders(c, validators, cachePolicy(OBJECT_MAX_AGE));
3917
4075
  const served = resolveServedContentType(contentType);
3918
4076
  c.header("Content-Type", served.contentType);
3919
4077
  if (served.attachment) c.header("Content-Disposition", "attachment");
4078
+ c.header("Accept-Ranges", "bytes");
4079
+ if (isNotModified(c, validators)) return c.body(null, 304);
4080
+ const localRange = parseRange(c.req.header("range"), localStat.size);
4081
+ if (localRange.kind === "unsatisfiable") {
4082
+ c.header("Content-Range", unsatisfiableContentRange(localStat.size));
4083
+ return c.body(null, 416);
4084
+ }
4085
+ if (localRange.kind === "range") {
4086
+ const { start, length } = localRange.range;
4087
+ const handle = await fsp.open(absolutePath, "r");
4088
+ try {
4089
+ const buffer = Buffer.alloc(length);
4090
+ await handle.read(buffer, 0, length, start);
4091
+ c.header("Content-Range", contentRange(localRange.range, localStat.size));
4092
+ c.header("Content-Length", String(length));
4093
+ return c.body(new Uint8Array(buffer), 206);
4094
+ } finally {
4095
+ await handle.close();
4096
+ }
4097
+ }
4098
+ c.header("Content-Length", String(localStat.size));
4099
+ const fileContent = await fsp.readFile(absolutePath);
3920
4100
  return c.body(new Uint8Array(fileContent));
3921
4101
  }
3922
4102
  const fileObject = await resolved.getObject(resolvedPath, bucket);
3923
4103
  if (!fileObject) throw ApiError.notFound("File not found");
3924
4104
  const remoteContentType = fileObject.type || "application/octet-stream";
3925
4105
  if (transformOpts && isTransformableImage(remoteContentType)) {
3926
- const transformed = await transformOnce(transformCache.buildKey(transformKeyPrefix, transformOpts), transformOpts, async () => Buffer.from(await fileObject.arrayBuffer()));
4106
+ const sourceVersion = buildEntityTag(fileObject.size, fileObject.lastModified);
4107
+ const transformed = await transformOnce(transformCache.buildKey(`${transformKeyPrefix}@${sourceVersion}`, transformOpts), transformOpts, async () => Buffer.from(await fileObject.arrayBuffer()));
4108
+ const validators = objectValidators(transformed.data.byteLength, fileObject.lastModified);
4109
+ applyCacheHeaders(c, validators, cachePolicy(TRANSFORM_MAX_AGE));
4110
+ if (isNotModified(c, validators)) return c.body(null, 304);
3927
4111
  c.header("Content-Type", transformed.contentType);
3928
- c.header("Cache-Control", "public, max-age=31536000, immutable");
3929
4112
  return c.body(new Uint8Array(transformed.data));
3930
4113
  }
3931
4114
  const servedRemote = resolveServedContentType(remoteContentType);
3932
4115
  c.header("Content-Type", servedRemote.contentType);
3933
4116
  if (servedRemote.attachment) c.header("Content-Disposition", "attachment");
3934
- c.header("Cache-Control", "public, max-age=3600, immutable");
4117
+ const remoteValidators = objectValidators(fileObject.size, fileObject.lastModified);
4118
+ applyCacheHeaders(c, remoteValidators, cachePolicy(OBJECT_MAX_AGE));
4119
+ c.header("Accept-Ranges", "bytes");
4120
+ if (isNotModified(c, remoteValidators)) return c.body(null, 304);
4121
+ const remoteRange = parseRange(c.req.header("range"), fileObject.size);
4122
+ if (remoteRange.kind === "unsatisfiable") {
4123
+ c.header("Content-Range", unsatisfiableContentRange(fileObject.size));
4124
+ return c.body(null, 416);
4125
+ }
3935
4126
  const buf = await fileObject.arrayBuffer();
4127
+ if (remoteRange.kind === "range") {
4128
+ const { start, end, length } = remoteRange.range;
4129
+ c.header("Content-Range", contentRange(remoteRange.range, fileObject.size));
4130
+ c.header("Content-Length", String(length));
4131
+ return c.body(new Uint8Array(buf.slice(start, end + 1)), 206);
4132
+ }
4133
+ c.header("Content-Length", String(fileObject.size));
3936
4134
  return c.body(new Uint8Array(buf));
3937
4135
  });
3938
4136
  /**
@@ -4161,6 +4359,114 @@ function assertStorageAccessControlConfigured(state, isProduction) {
4161
4359
  logger.warn(STORAGE_NO_ACCESS_CONTROL_MESSAGE);
4162
4360
  }
4163
4361
  //#endregion
4362
+ //#region src/storage/policies.ts
4363
+ var StoragePolicyError = class extends Error {
4364
+ constructor(message) {
4365
+ super(message);
4366
+ this.name = "StoragePolicyError";
4367
+ }
4368
+ };
4369
+ var ALL_OPERATIONS = [
4370
+ "read",
4371
+ "write",
4372
+ "delete",
4373
+ "list"
4374
+ ];
4375
+ /**
4376
+ * Split a key into segments, dropping empty ones.
4377
+ *
4378
+ * Empty segments come from a leading slash or a `//`, and treating them as real
4379
+ * would let `users//x` match a pattern expecting a captured segment with the
4380
+ * empty string. Keys reaching here are already sanitized against traversal; this
4381
+ * is about matching, not safety.
4382
+ */
4383
+ function segmentsOf(value) {
4384
+ return value.split("/").filter((segment) => segment.length > 0);
4385
+ }
4386
+ function compile(policy, index) {
4387
+ const label = `storagePolicies[${index}]`;
4388
+ if (typeof policy.path !== "string" || policy.path.trim() === "") throw new StoragePolicyError(`${label}: \`path\` must be a non-empty string.`);
4389
+ const segments = segmentsOf(policy.path);
4390
+ if (segments.length === 0) throw new StoragePolicyError(`${label}: \`path\` "${policy.path}" names no segments. Use "**" to match every key.`);
4391
+ const starIndex = segments.indexOf("**");
4392
+ if (starIndex !== -1 && starIndex !== segments.length - 1) throw new StoragePolicyError(`${label}: "**" is only allowed as the last segment of \`path\`, and "${policy.path}" puts it in the middle. Use "*" for a single segment.`);
4393
+ const captures = segments.filter((s) => s.startsWith(":")).map((s) => s.slice(1));
4394
+ for (const name of captures) {
4395
+ if (name === "") throw new StoragePolicyError(`${label}: a ":" placeholder in "${policy.path}" has no name.`);
4396
+ if (captures.filter((c) => c === name).length > 1) throw new StoragePolicyError(`${label}: "${policy.path}" captures ":${name}" twice, so one would silently win.`);
4397
+ }
4398
+ if (policy.operations !== void 0) {
4399
+ if (!Array.isArray(policy.operations) || policy.operations.length === 0) throw new StoragePolicyError(`${label}: \`operations\` is empty, so this policy grants nothing. Remove it to grant all, or name the operations.`);
4400
+ for (const operation of policy.operations) if (!ALL_OPERATIONS.includes(operation)) throw new StoragePolicyError(`${label}: \`operations\` names "${operation}", which is not a storage operation. Use ${ALL_OPERATIONS.map((o) => `"${o}"`).join(", ")}.`);
4401
+ }
4402
+ if (policy.allow !== "public" && policy.allow !== "authenticated" && typeof policy.allow !== "function") throw new StoragePolicyError(`${label}: \`allow\` must be "public", "authenticated", or a function. A policy with no \`allow\` grants nothing, which is never what was meant.`);
4403
+ return {
4404
+ segments,
4405
+ trailing: starIndex !== -1,
4406
+ operations: new Set(policy.operations ?? ALL_OPERATIONS),
4407
+ allow: policy.allow,
4408
+ source: policy.path
4409
+ };
4410
+ }
4411
+ /** Match a key's segments against a compiled pattern, capturing as it goes. */
4412
+ function match(compiled, key) {
4413
+ const keySegments = segmentsOf(key);
4414
+ const pattern = compiled.trailing ? compiled.segments.slice(0, -1) : compiled.segments;
4415
+ if (compiled.trailing ? keySegments.length < pattern.length : keySegments.length !== pattern.length) return;
4416
+ const params = {};
4417
+ for (let i = 0; i < pattern.length; i++) {
4418
+ const expected = pattern[i];
4419
+ const actual = keySegments[i];
4420
+ if (expected === "*") continue;
4421
+ if (expected.startsWith(":")) {
4422
+ params[expected.slice(1)] = actual;
4423
+ continue;
4424
+ }
4425
+ if (expected !== actual) return void 0;
4426
+ }
4427
+ return params;
4428
+ }
4429
+ /**
4430
+ * Compile a policy list into a {@link StorageAuthorize}.
4431
+ *
4432
+ * Compiled once, at boot, so a malformed pattern fails the start rather than the
4433
+ * first upload. `fallback` is an explicit `storageAuthorize`, consulted only
4434
+ * when no policy matched.
4435
+ */
4436
+ function compileStoragePolicies(policies, fallback) {
4437
+ if (!Array.isArray(policies)) throw new StoragePolicyError("`storagePolicies` must be an array.");
4438
+ const compiled = policies.map(compile);
4439
+ return async (ctx) => {
4440
+ for (const policy of compiled) {
4441
+ if (!policy.operations.has(ctx.operation)) continue;
4442
+ const params = match(policy, ctx.key);
4443
+ if (!params) continue;
4444
+ if (policy.allow === "public") return true;
4445
+ if (policy.allow === "authenticated") {
4446
+ if (ctx.user?.uid) return true;
4447
+ continue;
4448
+ }
4449
+ if (await policy.allow({
4450
+ ...ctx,
4451
+ params
4452
+ })) return true;
4453
+ }
4454
+ if (fallback) return await fallback(ctx);
4455
+ return false;
4456
+ };
4457
+ }
4458
+ /**
4459
+ * The access-control function for a configuration, or `undefined` when it
4460
+ * declares none.
4461
+ *
4462
+ * Kept separate from {@link compileStoragePolicies} so `init` has one call to
4463
+ * make and one thing to reason about: policies, a hook, both, or neither.
4464
+ */
4465
+ function resolveStorageAccessControl(config) {
4466
+ if (config.storagePolicies?.length) return compileStoragePolicies(config.storagePolicies, config.storageAuthorize);
4467
+ return config.storageAuthorize;
4468
+ }
4469
+ //#endregion
4164
4470
  //#region src/init/docs.ts
4165
4471
  /**
4166
4472
  * Serve the OpenAPI document, and Swagger UI outside production.
@@ -4256,6 +4562,101 @@ function createHealthCheck(defaultDriver, authSchemaCheck) {
4256
4562
  };
4257
4563
  }
4258
4564
  //#endregion
4565
+ //#region src/functions/wait-until.ts
4566
+ /**
4567
+ * Pending background work, on a process-global slot for the reason given in
4568
+ * `../singleton.ts`: two copies of this module in one process must not each
4569
+ * hold half the work, or shutdown drains one half and drops the other.
4570
+ */
4571
+ var PENDING_SLOT = Symbol.for("@rebasepro/server:pending-background-work");
4572
+ function pending() {
4573
+ const global = globalThis;
4574
+ if (!global[PENDING_SLOT]) global[PENDING_SLOT] = /* @__PURE__ */ new Set();
4575
+ return global[PENDING_SLOT];
4576
+ }
4577
+ /**
4578
+ * Keep `work` alive past the response, on any runtime.
4579
+ *
4580
+ * Hand it a promise or a function returning one. Nothing is awaited: the
4581
+ * response goes out immediately, exactly as an un-awaited call would.
4582
+ *
4583
+ * What it buys, per host:
4584
+ *
4585
+ * - **Isolate hosts** (Workers, Deno Deploy, Vercel Edge): the host is told to
4586
+ * keep the isolate alive until the promise settles. Without this the work is
4587
+ * dropped when the response resolves.
4588
+ * - **Node**: the promise is tracked so {@link drainBackgroundWork} can wait for
4589
+ * it during graceful shutdown instead of the process exiting out from under
4590
+ * it.
4591
+ *
4592
+ * On every host a rejection is logged rather than left to the unhandled-rejection
4593
+ * handler, so the failure names the function it came from.
4594
+ *
4595
+ * @example
4596
+ * ```ts
4597
+ * app.post("/orders", requireAuth, async (c) => {
4598
+ * const order = await createOrder(c);
4599
+ * waitUntil(c, notifyWarehouse(order)); // caller does not wait for this
4600
+ * return c.json({ id: order.id });
4601
+ * });
4602
+ * ```
4603
+ */
4604
+ function waitUntil(c, work) {
4605
+ let promise;
4606
+ try {
4607
+ promise = typeof work === "function" ? Promise.resolve(work()) : work;
4608
+ } catch (error) {
4609
+ logBackgroundFailure(c, error);
4610
+ return;
4611
+ }
4612
+ const tracked = promise.catch((error) => {
4613
+ logBackgroundFailure(c, error);
4614
+ });
4615
+ const set = pending();
4616
+ set.add(tracked);
4617
+ tracked.finally(() => set.delete(tracked));
4618
+ try {
4619
+ c.executionCtx?.waitUntil?.(tracked);
4620
+ } catch {}
4621
+ }
4622
+ function logBackgroundFailure(c, error) {
4623
+ logger.error("[functions] Background work failed after the response was sent", {
4624
+ method: c.req?.method,
4625
+ path: c.req?.path,
4626
+ error: error instanceof Error ? error : new Error(String(error))
4627
+ });
4628
+ }
4629
+ /** How many background promises are still in flight. Diagnostics and tests. */
4630
+ function pendingBackgroundWork() {
4631
+ return pending().size;
4632
+ }
4633
+ /**
4634
+ * Wait for tracked background work to finish, up to `timeoutMs`.
4635
+ *
4636
+ * Called by the shutdown path after the server stops accepting connections. It
4637
+ * resolves to the number of promises still outstanding when it returned — `0`
4638
+ * for a clean drain, more than that for a timeout, which is worth a log line
4639
+ * because it means work was dropped.
4640
+ *
4641
+ * Rejections do not propagate: every tracked promise has already had a `catch`
4642
+ * attached by {@link waitUntil}.
4643
+ */
4644
+ async function drainBackgroundWork(timeoutMs = 5e3) {
4645
+ const set = pending();
4646
+ if (set.size === 0) return 0;
4647
+ let timer;
4648
+ const expired = new Promise((resolve) => {
4649
+ timer = setTimeout(() => resolve("timeout"), timeoutMs);
4650
+ timer.unref?.();
4651
+ });
4652
+ try {
4653
+ await Promise.race([Promise.allSettled([...set]), expired]);
4654
+ } finally {
4655
+ if (timer) clearTimeout(timer);
4656
+ }
4657
+ return set.size;
4658
+ }
4659
+ //#endregion
4259
4660
  //#region src/init/shutdown.ts
4260
4661
  /**
4261
4662
  * Install graceful-shutdown signal handlers for a Rebase backend.
@@ -4280,77 +4681,1072 @@ function createHealthCheck(defaultDriver, authSchemaCheck) {
4280
4681
  * installShutdownHandlers(backend, { onCleanup: () => pool.end() });
4281
4682
  * ```
4282
4683
  */
4283
- function installShutdownHandlers(backend, options = {}) {
4284
- const { onCleanup, timeoutMs = 15e3, signals = ["SIGTERM", "SIGINT"], exit = process.exit } = options;
4285
- let shuttingDown = false;
4286
- const shutdownSequence = async (signal) => {
4287
- if (shuttingDown) return;
4288
- shuttingDown = true;
4289
- logger.info(`Received ${signal}, shutting down gracefully...`);
4290
- const forceTimer = setTimeout(() => {
4291
- logger.error(`Shutdown timed out after ${Math.round(timeoutMs / 1e3)}s. Forcefully exiting.`);
4292
- exit(1);
4293
- }, timeoutMs);
4294
- forceTimer.unref();
4684
+ function installShutdownHandlers(backend, options = {}) {
4685
+ const { onCleanup, timeoutMs = 15e3, signals = ["SIGTERM", "SIGINT"], exit = process.exit } = options;
4686
+ let shuttingDown = false;
4687
+ const shutdownSequence = async (signal) => {
4688
+ if (shuttingDown) return;
4689
+ shuttingDown = true;
4690
+ logger.info(`Received ${signal}, shutting down gracefully...`);
4691
+ const forceTimer = setTimeout(() => {
4692
+ logger.error(`Shutdown timed out after ${Math.round(timeoutMs / 1e3)}s. Forcefully exiting.`);
4693
+ exit(1);
4694
+ }, timeoutMs);
4695
+ forceTimer.unref();
4696
+ try {
4697
+ await backend.shutdown(timeoutMs);
4698
+ if (onCleanup) await onCleanup();
4699
+ clearTimeout(forceTimer);
4700
+ logger.info("Graceful shutdown complete.");
4701
+ exit(0);
4702
+ } catch (err) {
4703
+ logger.error("Error during shutdown cleanup:", { error: err instanceof Error ? err : new Error(String(err)) });
4704
+ exit(1);
4705
+ }
4706
+ };
4707
+ const listeners = signals.map((signal) => {
4708
+ const listener = () => {
4709
+ shutdownSequence(signal);
4710
+ };
4711
+ process.on(signal, listener);
4712
+ return {
4713
+ signal,
4714
+ listener
4715
+ };
4716
+ });
4717
+ return () => {
4718
+ for (const { signal, listener } of listeners) process.removeListener(signal, listener);
4719
+ };
4720
+ }
4721
+ function createShutdown(config) {
4722
+ return (timeoutMs = 15e3) => {
4723
+ return new Promise((resolve) => {
4724
+ (async () => {
4725
+ logger.info("Shutting down Rebase Backend...");
4726
+ if (config.cronScheduler) {
4727
+ config.cronScheduler.stop();
4728
+ logger.info("Cron scheduler stopped");
4729
+ }
4730
+ if (config.rlsAudit) config.rlsAudit.stop();
4731
+ if (config.jobQueue) {
4732
+ await config.jobQueue.stop();
4733
+ logger.info("Job queue stopped");
4734
+ }
4735
+ const drainBudget = timeoutMs > 0 ? Math.min(5e3, timeoutMs) : 5e3;
4736
+ const stillPending = await drainBackgroundWork(drainBudget);
4737
+ if (stillPending > 0) logger.warn(`${stillPending} background task(s) handed to waitUntil() did not finish within ${drainBudget}ms and are being dropped. Give them an AbortSignal, or move work this long into a cron job or the job queue, where it is restartable.`);
4738
+ for (const [key, rt] of Object.entries(config.realtimeServices)) try {
4739
+ if (typeof rt.destroy === "function") {
4740
+ await rt.destroy();
4741
+ logger.info(`Realtime service "${key}" destroyed`);
4742
+ } else if (typeof rt.stopListening === "function") {
4743
+ await rt.stopListening();
4744
+ logger.info(`Realtime service "${key}" LISTEN client stopped`);
4745
+ }
4746
+ } catch (err) {
4747
+ logger.warn(`Error destroying realtime service "${key}":`, { error: err });
4748
+ }
4749
+ config.server.close(() => {
4750
+ logger.info("HTTP server closed");
4751
+ resolve();
4752
+ });
4753
+ if (timeoutMs > 0) setTimeout(() => {
4754
+ logger.warn(`Forced shutdown after ${timeoutMs / 1e3}s timeout`);
4755
+ resolve();
4756
+ }, timeoutMs).unref();
4757
+ })();
4758
+ });
4759
+ };
4760
+ }
4761
+ //#endregion
4762
+ //#region src/schema-edit/apply-schema-change.ts
4763
+ /**
4764
+ * The plan says the change is not applicable.
4765
+ *
4766
+ * Re-checked here rather than trusted from the planner: this module is the one
4767
+ * that writes and runs things, so the guarantee belongs where the consequence
4768
+ * is. A caller that built a plan by hand cannot route around it.
4769
+ */
4770
+ var UnapplicableChangeError = class extends Error {
4771
+ classified;
4772
+ constructor(message, classified) {
4773
+ super(message);
4774
+ this.classified = classified;
4775
+ this.name = "UnapplicableChangeError";
4776
+ }
4777
+ };
4778
+ var DirtyWorkingTreeError = class extends Error {
4779
+ paths;
4780
+ constructor(paths) {
4781
+ super("The working tree has uncommitted changes to files this would commit:\n" + paths.map((p) => ` • ${p}`).join("\n") + "\n Commit or stash them first. A schema change must not sweep up work in progress.");
4782
+ this.paths = paths;
4783
+ this.name = "DirtyWorkingTreeError";
4784
+ }
4785
+ };
4786
+ /**
4787
+ * Generate the commit, land it, then run the DDL.
4788
+ *
4789
+ * Throws only for things that mean *nothing happened*: a change the ensure path
4790
+ * cannot express, a dirty tree, or a failed commit. Once the commit lands,
4791
+ * every outcome is a result.
4792
+ */
4793
+ async function applySchemaChange(input) {
4794
+ const commit = input.plan;
4795
+ if (!commit.classified.applicable) throw new UnapplicableChangeError("This change cannot be applied to a running database:\n" + commit.classified.changes.filter((change) => change.verdict !== "safe").map((change) => ` • ${change.detail}${change.remedy ? `\n ${change.remedy}` : ""}`).join("\n"), commit.classified);
4796
+ const plannedPaths = [...commit.files.map((file) => file.path), ...input.sourcePaths ?? []];
4797
+ const dirty = (await input.repository.dirtyPaths()).filter((path) => plannedPaths.includes(path));
4798
+ if (dirty.length > 0) throw new DirtyWorkingTreeError(dirty);
4799
+ const branch = await input.repository.currentBranch();
4800
+ const files = [...input.writeSource ? await input.writeSource() : [], ...commit.files];
4801
+ const paths = files.map((file) => file.path);
4802
+ await input.repository.writeFiles(files);
4803
+ const sha = await input.repository.commit(paths, commit.message);
4804
+ const committed = {
4805
+ sha,
4806
+ branch,
4807
+ files: paths
4808
+ };
4809
+ if (commit.statements.length === 0) return {
4810
+ committed,
4811
+ applied: true,
4812
+ statements: [],
4813
+ classified: commit.classified,
4814
+ summary: `Committed ${sha.slice(0, 9)} on ${branch}. No DDL was needed.`
4815
+ };
4816
+ try {
4817
+ await input.apply(commit.statements);
4818
+ return {
4819
+ committed,
4820
+ applied: true,
4821
+ statements: commit.statements,
4822
+ classified: commit.classified,
4823
+ summary: `Committed ${sha.slice(0, 9)} on ${branch} and applied ${commit.statements.length} statement(s).`
4824
+ };
4825
+ } catch (err) {
4826
+ const detail = err instanceof Error ? err.message : String(err);
4827
+ return {
4828
+ committed,
4829
+ applied: false,
4830
+ applyError: detail,
4831
+ statements: commit.statements,
4832
+ classified: commit.classified,
4833
+ summary: `Committed ${sha.slice(0, 9)} on ${branch}, but the database was not changed: ${detail}. The change will be applied on the next boot.`
4834
+ };
4835
+ }
4836
+ }
4837
+ //#endregion
4838
+ //#region src/schema-edit/schema-edit-permissions.ts
4839
+ /**
4840
+ * A caller whose identity is a credential rather than a person.
4841
+ *
4842
+ * Two shapes, both set by the auth middleware:
4843
+ *
4844
+ * - the service key resolves to `uid: "service"` with `roles: ["admin"]`;
4845
+ * - an API key resolves to `uid: "api-key:<id>"`, with `service` among its
4846
+ * roles and `admin` too when the key is an admin key.
4847
+ *
4848
+ * Detected by uid rather than by role, because `service` is also a role a
4849
+ * person could be granted, and a person with an unfortunate role name should
4850
+ * not lose the ability to commit under their own name.
4851
+ */
4852
+ function machineKindOf(uid) {
4853
+ if (uid === "service") return "service-key";
4854
+ if (uid.startsWith("api-key:")) return "api-key";
4855
+ }
4856
+ /** Read the caller off the request context's `user`. */
4857
+ function classifyPrincipal(user) {
4858
+ if (!user || typeof user !== "object") return {
4859
+ kind: "anonymous",
4860
+ uid: "",
4861
+ roles: []
4862
+ };
4863
+ const { uid, roles } = user;
4864
+ if (typeof uid !== "string" || uid.length === 0) return {
4865
+ kind: "anonymous",
4866
+ uid: "",
4867
+ roles: []
4868
+ };
4869
+ const roleList = Array.isArray(roles) ? roles.filter((r) => typeof r === "string") : [];
4870
+ const machineKind = machineKindOf(uid);
4871
+ return {
4872
+ kind: machineKind ? "machine" : "person",
4873
+ uid,
4874
+ roles: roleList,
4875
+ machineKind
4876
+ };
4877
+ }
4878
+ var MACHINE_REFUSAL = {
4879
+ "service-key": "This request is authenticated with the server's service key, which identifies the server rather than a person. Applying a schema change writes a commit to your repository, and that commit needs an author.",
4880
+ "api-key": "This request is authenticated with an API key, which identifies a credential rather than a person. Applying a schema change writes a commit to your repository, and that commit needs an author."
4881
+ };
4882
+ /**
4883
+ * What this caller may do.
4884
+ *
4885
+ * Assumes the admin gate has already run — this refines that decision rather
4886
+ * than replacing it, and an anonymous caller should never have reached here.
4887
+ * Handled anyway, because a capability function that trusts its caller to have
4888
+ * checked is one bad refactor away from granting everything.
4889
+ */
4890
+ function schemaEditCapabilities(principal, policy = {}) {
4891
+ if (principal.kind === "anonymous") return {
4892
+ plan: false,
4893
+ apply: false,
4894
+ code: "UNAUTHORIZED",
4895
+ reason: "This request carries no identity."
4896
+ };
4897
+ if (principal.kind === "machine" && !policy.allowMachineApply) return {
4898
+ plan: true,
4899
+ apply: false,
4900
+ code: "SCHEMA_EDIT_REQUIRES_A_PERSON",
4901
+ reason: `${MACHINE_REFUSAL[principal.machineKind ?? "api-key"]} Sign in as a user to apply it, or set \`liveSchema.allowMachineApply\` if an automated schema change is what you want.`
4902
+ };
4903
+ return {
4904
+ plan: true,
4905
+ apply: true
4906
+ };
4907
+ }
4908
+ /**
4909
+ * The git identity for a machine that has been allowed to apply.
4910
+ *
4911
+ * Named for what it is. An operator reading `git log` a month from now should
4912
+ * be able to tell a change somebody made from one a pipeline made, and a
4913
+ * credential wearing a person-shaped name is how that distinction is lost.
4914
+ */
4915
+ function machineCommitAuthor(principal) {
4916
+ return {
4917
+ name: principal.machineKind === "service-key" ? "Rebase service key" : `Rebase API key (${principal.uid.replace(/^api-key:/, "")})`,
4918
+ email: `${principal.uid.replace(/[^A-Za-z0-9._-]/g, "-")}@machines.noreply.rebase.pro`
4919
+ };
4920
+ }
4921
+ //#endregion
4922
+ //#region src/api/live-schema-routes.ts
4923
+ /**
4924
+ * Creating and altering collections against a running backend.
4925
+ *
4926
+ * The existing schema editor rewrites collection source and is off in
4927
+ * production, for an honest reason: a deployed server's files are rebuilt from
4928
+ * the repository on every deploy, so an edit there would be discarded. These
4929
+ * routes are the answer to that — they commit the change to the repository the
4930
+ * server is running in, and *then* apply the DDL, so the edit survives the next
4931
+ * deploy because the deploy is built from it.
4932
+ *
4933
+ * Two routes, and the first one is the important one.
4934
+ *
4935
+ * ## Why `plan` exists separately from `apply`
4936
+ *
4937
+ * `POST /plan` has no side effects and answers what would happen. That is not a
4938
+ * convenience: a schema change has three possible verdicts and two of them are
4939
+ * refusals, so a UI that could only try-and-see would be asking people to
4940
+ * discover `needs-migration` by pressing a button on a live database.
4941
+ *
4942
+ * It also surfaces the `diverges` cases, which are the ones nothing else in the
4943
+ * system reports — a required property that will arrive nullable, an enum value
4944
+ * that will never land. Those apply "successfully" today and leave a database
4945
+ * that does not match its configuration.
4946
+ *
4947
+ * ## What these routes will not do
4948
+ *
4949
+ * Anything the boot-time ensure path cannot express. No drops, no type changes,
4950
+ * no primary keys. The planner refuses them and this refuses to route around it
4951
+ * — `applySchemaChange` re-checks rather than trusting the plan it is handed.
4952
+ */
4953
+ /**
4954
+ * The git identity for the caller.
4955
+ *
4956
+ * `undefined` when the request carries no user — a service key, say — which
4957
+ * lets the repository fall back to its own git config rather than inventing a
4958
+ * name for somebody.
4959
+ */
4960
+ function commitAuthor(user) {
4961
+ if (!user || typeof user !== "object") return void 0;
4962
+ const { uid, email, displayName } = user;
4963
+ if (!uid) return void 0;
4964
+ return {
4965
+ name: displayName || email || uid,
4966
+ email: email || `${uid}@users.noreply.rebase.pro`
4967
+ };
4968
+ }
4969
+ /**
4970
+ * What a collection id may contain.
4971
+ *
4972
+ * The same alphabet the AST editor sanitises to, stated at the boundary
4973
+ * instead. The id becomes a *filename* — `<collectionsDir>/<id>.ts` — in two
4974
+ * places, and only one of them checked it: the editor refuses a traversal when
4975
+ * it writes, but the caller derives the same path first, to hand the dirty
4976
+ * check the paths it is about to touch. A refusal that arrives one layer in is
4977
+ * a refusal that the layer above already acted on.
4978
+ */
4979
+ var SAFE_COLLECTION_ID = /^[A-Za-z0-9_-]+$/;
4980
+ var parseProposed = (body) => {
4981
+ const candidate = body;
4982
+ if (!candidate?.collectionId || typeof candidate.collectionId !== "string") throw ApiError.badRequest("`collectionId` is required.", "INVALID_CHANGE");
4983
+ if (!SAFE_COLLECTION_ID.test(candidate.collectionId)) throw ApiError.badRequest(`"${candidate.collectionId}" is not a usable collection id. It becomes a filename, so it may contain only letters, numbers, underscores and hyphens.`, "INVALID_CHANGE");
4984
+ if (!candidate.collection || typeof candidate.collection !== "object") throw ApiError.badRequest("`collection` is required.", "INVALID_CHANGE");
4985
+ return {
4986
+ collectionId: candidate.collectionId,
4987
+ collection: candidate.collection
4988
+ };
4989
+ };
4990
+ /**
4991
+ * The proposed collection set: everything as it is, with one collection
4992
+ * replaced or added.
4993
+ */
4994
+ function proposedCollections(current, change) {
4995
+ const next = {
4996
+ ...change.collection,
4997
+ slug: change.collectionId
4998
+ };
4999
+ const replaced = current.map((collection) => collection.slug === change.collectionId ? next : collection);
5000
+ return replaced.some((collection) => collection.slug === change.collectionId) ? replaced : [...replaced, next];
5001
+ }
5002
+ function createLiveSchemaRoutes(config) {
5003
+ const router = new Hono();
5004
+ router.onError(errorHandler);
5005
+ /**
5006
+ * Whether this backend can edit its schema, and what is missing when it
5007
+ * cannot.
5008
+ *
5009
+ * Every optional surface in this API answers `GET …/status` — see
5010
+ * `docs/api-conventions.md`. The panel asks before it offers the control,
5011
+ * because the alternative is a button that looks available and fails on
5012
+ * press with a refusal the person could have been told about up front.
5013
+ *
5014
+ * Ordered by how fundamental the obstacle is: a driver that cannot plan is
5015
+ * a different conversation from a missing dependency, which is a different
5016
+ * conversation from a deployment with no source. Reporting the first one
5017
+ * that applies is what keeps the message actionable.
5018
+ */
5019
+ router.get("/status", (c) => {
5020
+ const admin = config.getAdmin();
5021
+ const repository = config.getRepository();
5022
+ if (!isSchemaEditingAdmin(admin)) return c.json({
5023
+ enabled: false,
5024
+ canPlan: false,
5025
+ code: "SCHEMA_EDITING_UNSUPPORTED",
5026
+ reason: "This backend's driver cannot plan schema changes. Live editing is available on Postgres."
5027
+ });
5028
+ if (!config.writeSource) return c.json({
5029
+ enabled: false,
5030
+ canPlan: true,
5031
+ code: "SCHEMA_EDITOR_MISSING_DEPENDENCY",
5032
+ reason: "Rewriting collection source needs `ts-morph`, which is not installed on this server. Run `pnpm add -D ts-morph@28.0.0` to enable it."
5033
+ });
5034
+ if (!repository) return c.json({
5035
+ enabled: false,
5036
+ canPlan: true,
5037
+ code: "SCHEMA_EDITING_NO_REPOSITORY",
5038
+ reason: "There is no repository to commit to. Live schema editing writes the change to your project's source before applying it, so it needs the project mounted — a deployment running from a built bundle has compiled output and no source to edit."
5039
+ });
5040
+ const capabilities = schemaEditCapabilities(classifyPrincipal(c.get("user")), config.policy);
5041
+ return c.json({
5042
+ enabled: true,
5043
+ canPlan: capabilities.plan,
5044
+ canApply: capabilities.apply,
5045
+ applyRefusedBecause: capabilities.reason,
5046
+ applyRefusedCode: capabilities.code,
5047
+ repository: repository.root
5048
+ });
5049
+ });
5050
+ /** The admin, or a refusal naming what is missing rather than a 500. */
5051
+ const requirePlanner = () => {
5052
+ const admin = config.getAdmin();
5053
+ if (!isSchemaEditingAdmin(admin)) throw ApiError.serviceUnavailable("This backend's driver cannot plan schema changes. Live editing is available on Postgres.", "SCHEMA_EDITING_UNSUPPORTED");
5054
+ return admin;
5055
+ };
5056
+ const requireRepository = (author) => {
5057
+ const repository = config.getRepository(author);
5058
+ if (!repository) throw ApiError.serviceUnavailable("There is no repository to commit to. Live schema editing writes the change to your project's source before applying it, so it needs the project mounted — a deployment running from a built bundle has compiled output and no source to edit.", "SCHEMA_EDITING_NO_REPOSITORY");
5059
+ return repository;
5060
+ };
5061
+ /**
5062
+ * What would happen. No side effects, and deliberately reachable even when
5063
+ * there is no repository — knowing a change is blocked is useful before
5064
+ * knowing where it would be committed.
5065
+ */
5066
+ router.post("/plan", async (c) => {
5067
+ const change = parseProposed(await c.req.json());
5068
+ const admin = requirePlanner();
5069
+ const capabilities = schemaEditCapabilities(classifyPrincipal(c.get("user")), config.policy);
5070
+ if (!capabilities.plan) throw ApiError.forbidden(capabilities.reason ?? "This caller may not plan schema changes.", capabilities.code ?? "FORBIDDEN");
5071
+ const before = config.getCollections();
5072
+ const after = proposedCollections(before, change);
5073
+ const plan = await admin.planSchemaChange(before, after, { paths: config.commitPaths });
5074
+ return c.json({
5075
+ applicable: plan.classified.applicable,
5076
+ verdict: plan.classified.verdict,
5077
+ changes: plan.classified.changes,
5078
+ statements: plan.statements,
5079
+ files: plan.files.map((file) => file.path),
5080
+ message: plan.message,
5081
+ withheldConstraints: plan.withheldConstraints ?? []
5082
+ });
5083
+ });
5084
+ router.post("/apply", async (c) => {
5085
+ const change = parseProposed(await c.req.json());
5086
+ const admin = requirePlanner();
5087
+ const principal = classifyPrincipal(c.get("user"));
5088
+ const capabilities = schemaEditCapabilities(principal, config.policy);
5089
+ if (!capabilities.apply) throw ApiError.forbidden(capabilities.reason ?? "This caller may not apply schema changes.", capabilities.code ?? "FORBIDDEN");
5090
+ const author = principal.kind === "machine" ? machineCommitAuthor(principal) : commitAuthor(c.get("user"));
5091
+ const repository = requireRepository(author);
5092
+ const before = config.getCollections();
5093
+ const after = proposedCollections(before, change);
5094
+ const plan = await admin.planSchemaChange(before, after, { paths: config.commitPaths });
5095
+ if (!plan.classified.applicable) {
5096
+ const blocking = plan.classified.changes.filter((c) => c.verdict !== "safe");
5097
+ throw ApiError.badRequest("This change cannot be applied to a running database:\n" + blocking.map((c) => ` • ${c.detail}${c.remedy ? `\n ${c.remedy}` : ""}`).join("\n"), "SCHEMA_CHANGE_UNAPPLICABLE", { changes: plan.classified.changes });
5098
+ }
5099
+ try {
5100
+ const result = await applySchemaChange({
5101
+ plan,
5102
+ repository,
5103
+ sourcePaths: config.sourcePathsFor?.(change) ?? [],
5104
+ writeSource: config.writeSource ? () => config.writeSource(change) : void 0,
5105
+ apply: async (statements) => {
5106
+ const sql = config.getAdmin();
5107
+ if (!isSQLAdmin(sql)) throw new Error("The driver cannot run SQL, so the change cannot be applied here.");
5108
+ for (const statement of statements) await sql.executeSql(statement);
5109
+ }
5110
+ });
5111
+ logger.info(`[schema-edit] ${result.summary}`);
5112
+ return c.json({
5113
+ ...result,
5114
+ withheldConstraints: plan.withheldConstraints ?? []
5115
+ });
5116
+ } catch (err) {
5117
+ if (err instanceof UnapplicableChangeError) throw ApiError.badRequest(err.message, "SCHEMA_CHANGE_UNAPPLICABLE", { changes: err.classified.changes });
5118
+ if (err instanceof DirtyWorkingTreeError) throw ApiError.conflict(err.message, "SCHEMA_EDIT_DIRTY_TREE");
5119
+ throw ApiError.badRequest(err instanceof Error ? err.message : String(err), "SCHEMA_CHANGE_FAILED");
5120
+ }
5121
+ });
5122
+ return router;
5123
+ }
5124
+ //#endregion
5125
+ //#region src/api/mount.ts
5126
+ /**
5127
+ * Mounting a surface at its canonical path, and at the path it used to have.
5128
+ *
5129
+ * Several admin-only surfaces grew up at the top level — `/api/cron`,
5130
+ * `/api/logs`, `/api/schema-editor` — while the ones added later went under
5131
+ * `/api/admin`. Nothing was wrong with either choice on its own; what was wrong
5132
+ * was having both, because the shape of the path stopped predicting whether a
5133
+ * caller needed to be an admin. `docs/api-conventions.md` states the rule the
5134
+ * repo now follows and this module is how existing callers survive it.
5135
+ *
5136
+ * ## Why an alias rather than a redirect
5137
+ *
5138
+ * A 308 would be correct and would break things anyway: `fetch` follows
5139
+ * redirects but drops an `Authorization` header on a cross-origin hop, and a
5140
+ * good deal of client code posts to these paths with one attached. An alias
5141
+ * serves the same handler at both paths, so nothing observable changes for a
5142
+ * caller that has not moved yet.
5143
+ *
5144
+ * The alias is not silent. Every response through it carries `Deprecation` and
5145
+ * a `Link` naming the successor, which is what makes the old path removable
5146
+ * later: an operator can find the callers in their own logs rather than
5147
+ * discovering them when the path is gone.
5148
+ */
5149
+ /**
5150
+ * Mount `router` at its canonical path, and at its legacy path when it has one.
5151
+ *
5152
+ * The same router instance serves both, so the two paths cannot drift: there is
5153
+ * one set of handlers and one gate, and an alias that forgot to apply the admin
5154
+ * gate is not a state this can reach.
5155
+ */
5156
+ function mountWithLegacyAlias(app, router, options) {
5157
+ app.route(options.canonical, router);
5158
+ if (!options.legacy) {
5159
+ logger.debug(`${options.surface} routes mounted`, { path: options.canonical });
5160
+ return;
5161
+ }
5162
+ const alias = new Hono();
5163
+ alias.use("/*", async (c, next) => {
5164
+ await next();
5165
+ c.header("Deprecation", "true");
5166
+ c.header("Link", `<${options.canonical}>; rel="successor-version"`);
5167
+ });
5168
+ alias.route("/", router);
5169
+ app.route(options.legacy, alias);
5170
+ logger.debug(`${options.surface} routes mounted`, {
5171
+ path: options.canonical,
5172
+ deprecatedAlias: options.legacy
5173
+ });
5174
+ }
5175
+ function findProjectRoot(startDir, levels = 5) {
5176
+ let dir = path$1.resolve(startDir);
5177
+ for (let i = 0; i <= levels; i++) {
5178
+ if (fs$1.existsSync(path$1.join(dir, "rebase.json"))) return dir;
5179
+ const parent = path$1.dirname(dir);
5180
+ if (parent === dir) return void 0;
5181
+ dir = parent;
5182
+ }
5183
+ }
5184
+ /**
5185
+ * The generated-artifact paths for a project, expressed relative to the
5186
+ * repository that will hold the commit.
5187
+ *
5188
+ * Returns `undefined` when there is nothing to correct — no project marker, or
5189
+ * a project that *is* the repository — so the caller passes no override and the
5190
+ * defaults stand. Saying "no change needed" is different from saying
5191
+ * "the change is a no-op prefix", and only the first leaves the existing
5192
+ * behaviour untouched for every project that already worked.
5193
+ */
5194
+ function commitPathsFor(collectionsDir, repositoryRoot) {
5195
+ const projectRoot = findProjectRoot(collectionsDir);
5196
+ if (!projectRoot) return void 0;
5197
+ const prefix = path$1.relative(path$1.resolve(repositoryRoot), projectRoot);
5198
+ if (prefix === "" || prefix.startsWith("..")) return void 0;
5199
+ const relocate = (p) => path$1.posix.join(...prefix.split(path$1.sep), p);
5200
+ return {
5201
+ schemaFile: relocate(DEFAULT_COMMIT_PATHS.schemaFile),
5202
+ ddlFile: relocate(DEFAULT_COMMIT_PATHS.ddlFile),
5203
+ policiesFile: relocate(DEFAULT_COMMIT_PATHS.policiesFile),
5204
+ searchFile: relocate(DEFAULT_COMMIT_PATHS.searchFile)
5205
+ };
5206
+ }
5207
+ //#endregion
5208
+ //#region src/schema-edit/github-repository.ts
5209
+ /**
5210
+ * A {@link SchemaEditRepository} backed by a repository on GitHub.
5211
+ *
5212
+ * This is the implementation for a deployment whose source is not on the
5213
+ * machine — a Cloud tenant runs a built bundle, and its repository lives
5214
+ * somewhere else entirely. The local one commits with `git`; this one has no
5215
+ * working tree at all and uses the Git Data API, which creates a blob, a tree,
5216
+ * a commit and moves a ref in four calls. Nothing is cloned.
5217
+ *
5218
+ * ## Authentication
5219
+ *
5220
+ * A GitHub App, not a token per project. The app holds one private key
5221
+ * globally; each project stores an `installationId`, which is not a secret. A
5222
+ * per-project personal access token would mean a credential to hold, rotate and
5223
+ * leak for every customer.
5224
+ *
5225
+ * Three steps, and the middle one is the one people miss:
5226
+ *
5227
+ * 1. sign a JWT with the private key — RS256, `iss` the app's id, ten minutes
5228
+ * at most, which authenticates *the app* and can touch no repository;
5229
+ * 2. exchange it for an **installation access token**, which expires in an hour
5230
+ * and is what can actually write;
5231
+ * 3. use that token as the bearer for the Git Data calls.
5232
+ *
5233
+ * The token is cached until shortly before it expires, because minting one is a
5234
+ * round trip and a schema edit makes several calls.
5235
+ *
5236
+ * ## What `dirtyPaths` means without a working tree
5237
+ *
5238
+ * Nothing, directly — there is no tree to be dirty. The equivalent hazard is the
5239
+ * branch moving under us between reading its head and writing a commit onto it,
5240
+ * and that is handled where it belongs: the commit is created against the head
5241
+ * read at the start, and updating the ref is **not** forced, so GitHub rejects
5242
+ * it if anything else landed meanwhile. A lost race is a refusal, never a
5243
+ * silent overwrite of somebody's commit.
5244
+ */
5245
+ var GITHUB_API = "https://api.github.com";
5246
+ /** GitHub caps app JWTs at ten minutes; nine leaves room for clock skew. */
5247
+ var APP_JWT_TTL_SECONDS = 540;
5248
+ /** Renew an installation token this long before it lapses. */
5249
+ var TOKEN_REFRESH_MARGIN_MS = 6e4;
5250
+ var GitHubApiError = class extends Error {
5251
+ status;
5252
+ path;
5253
+ constructor(status, path, detail) {
5254
+ super(`GitHub ${path} failed with ${status}: ${detail}`);
5255
+ this.status = status;
5256
+ this.path = path;
5257
+ this.name = "GitHubApiError";
5258
+ }
5259
+ };
5260
+ /** base64url, which JWT requires and `Buffer` does not produce by default. */
5261
+ var b64url = (input) => Buffer.from(input).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
5262
+ /**
5263
+ * A JWT authenticating the app itself.
5264
+ *
5265
+ * `iat` is backdated by a minute: GitHub rejects a token whose `iat` is in its
5266
+ * future, and a server clock a few seconds fast is enough to trigger it.
5267
+ */
5268
+ function createAppJwt(appId, privateKey, nowMs) {
5269
+ const issued = Math.floor(nowMs / 1e3) - 60;
5270
+ const header = b64url(JSON.stringify({
5271
+ alg: "RS256",
5272
+ typ: "JWT"
5273
+ }));
5274
+ const payload = b64url(JSON.stringify({
5275
+ iat: issued,
5276
+ exp: issued + APP_JWT_TTL_SECONDS,
5277
+ iss: appId
5278
+ }));
5279
+ const signer = createSign("RSA-SHA256");
5280
+ signer.update(`${header}.${payload}`);
5281
+ signer.end();
5282
+ return `${header}.${payload}.${b64url(signer.sign(privateKey))}`;
5283
+ }
5284
+ function createGitHubRepository(options) {
5285
+ const doFetch = options.fetchImpl ?? fetch;
5286
+ const now = options.now ?? (() => Date.now());
5287
+ const { owner, repo, branch } = options;
5288
+ /** Files staged by `writeFiles`, held until `commit`. */
5289
+ let staged = [];
5290
+ let token;
5291
+ const call = async (path, init) => {
5292
+ const { auth, ...rest } = init;
5293
+ const response = await doFetch(`${GITHUB_API}${path}`, {
5294
+ ...rest,
5295
+ headers: {
5296
+ accept: "application/vnd.github+json",
5297
+ "x-github-api-version": "2022-11-28",
5298
+ authorization: `Bearer ${auth}`,
5299
+ "content-type": "application/json",
5300
+ ...rest.headers ?? {}
5301
+ }
5302
+ });
5303
+ if (!response.ok) throw new GitHubApiError(response.status, path, (await response.text()).slice(0, 400));
5304
+ return await response.json();
5305
+ };
5306
+ /**
5307
+ * The bearer for the Git Data calls.
5308
+ *
5309
+ * A token is already one. An app has to mint one: sign a JWT with the
5310
+ * private key, which authenticates the *app* and can touch no repository,
5311
+ * then exchange it for an installation token, which expires in an hour and
5312
+ * is what can actually write. Cached until shortly before it lapses,
5313
+ * because a schema edit makes several calls and minting is a round trip.
5314
+ */
5315
+ const bearer = async () => {
5316
+ if (options.auth.kind === "token") return options.auth.token;
5317
+ if (token && token.expiresAtMs - TOKEN_REFRESH_MARGIN_MS > now()) return token.value;
5318
+ const jwt = createAppJwt(options.auth.appId, options.auth.privateKey, now());
5319
+ const minted = await call(`/app/installations/${options.auth.installationId}/access_tokens`, {
5320
+ method: "POST",
5321
+ auth: jwt
5322
+ });
5323
+ token = {
5324
+ value: minted.token,
5325
+ expiresAtMs: Date.parse(minted.expires_at)
5326
+ };
5327
+ return token.value;
5328
+ };
5329
+ return {
5330
+ root: `${owner}/${repo}`,
5331
+ async currentBranch() {
5332
+ return branch;
5333
+ },
5334
+ async dirtyPaths() {
5335
+ return [];
5336
+ },
5337
+ async writeFiles(files) {
5338
+ staged = files;
5339
+ },
5340
+ /**
5341
+ * One file's current contents, read from the branch this commits onto.
5342
+ *
5343
+ * `undefined` for a file that is not there — a collection being created
5344
+ * has no source yet — and that is a normal answer rather than a
5345
+ * failure. Anything else is surfaced: a 403 from a token without
5346
+ * `contents:read` must not be mistaken for an empty repository, or the
5347
+ * editor would rewrite from nothing and the commit would delete
5348
+ * somebody's collection.
5349
+ */
5350
+ async readFile(filePath) {
5351
+ const auth = await bearer();
5352
+ const response = await doFetch(`${GITHUB_API}/repos/${owner}/${repo}/contents/${filePath.split("/").map(encodeURIComponent).join("/")}?ref=${encodeURIComponent(branch)}`, { headers: {
5353
+ accept: "application/vnd.github+json",
5354
+ "x-github-api-version": "2022-11-28",
5355
+ authorization: `Bearer ${auth}`
5356
+ } });
5357
+ if (response.status === 404) return void 0;
5358
+ if (!response.ok) throw new GitHubApiError(response.status, `/repos/${owner}/${repo}/contents/${filePath}`, (await response.text()).slice(0, 400));
5359
+ const body = await response.json();
5360
+ if (typeof body.content !== "string") return void 0;
5361
+ if (body.encoding && body.encoding !== "base64") return void 0;
5362
+ return Buffer.from(body.content, "base64").toString("utf8");
5363
+ },
5364
+ async commit(paths, message) {
5365
+ if (staged.length === 0) throw new Error("Refusing to commit with nothing staged.");
5366
+ const auth = await bearer();
5367
+ const base = `/repos/${owner}/${repo}`;
5368
+ const headSha = (await call(`${base}/git/ref/heads/${encodeURIComponent(branch)}`, { auth })).object.sha;
5369
+ const head = await call(`${base}/git/commits/${headSha}`, { auth });
5370
+ const tree = await Promise.all(staged.filter((file) => paths.includes(file.path)).map(async (file) => {
5371
+ const blob = await call(`${base}/git/blobs`, {
5372
+ method: "POST",
5373
+ auth,
5374
+ body: JSON.stringify({
5375
+ content: file.contents,
5376
+ encoding: "utf-8"
5377
+ })
5378
+ });
5379
+ return {
5380
+ path: file.path,
5381
+ mode: "100644",
5382
+ type: "blob",
5383
+ sha: blob.sha
5384
+ };
5385
+ }));
5386
+ const created = await call(`${base}/git/trees`, {
5387
+ method: "POST",
5388
+ auth,
5389
+ body: JSON.stringify({
5390
+ base_tree: head.tree.sha,
5391
+ tree
5392
+ })
5393
+ });
5394
+ const commit = await call(`${base}/git/commits`, {
5395
+ method: "POST",
5396
+ auth,
5397
+ body: JSON.stringify({
5398
+ message,
5399
+ tree: created.sha,
5400
+ parents: [headSha],
5401
+ ...options.author ? { author: {
5402
+ ...options.author,
5403
+ date: new Date(now()).toISOString()
5404
+ } } : {}
5405
+ })
5406
+ });
5407
+ await call(`${base}/git/refs/heads/${encodeURIComponent(branch)}`, {
5408
+ method: "PATCH",
5409
+ auth,
5410
+ body: JSON.stringify({
5411
+ sha: commit.sha,
5412
+ force: false
5413
+ })
5414
+ });
5415
+ staged = [];
5416
+ return commit.sha;
5417
+ }
5418
+ };
5419
+ }
5420
+ //#endregion
5421
+ //#region src/schema-edit/remote-source.ts
5422
+ /**
5423
+ * Rewriting a collection whose source is not on this machine.
5424
+ *
5425
+ * A bundle deployment — every Cloud tenant, and any self-host running built
5426
+ * output — ships *compiled* collections. The AST editor rewrites the
5427
+ * TypeScript, opening it from a directory on disk, so there is nothing for it
5428
+ * to open. That single fact is why live schema editing was local-only: not
5429
+ * authentication, not the Git Data API, which have worked for a while, but the
5430
+ * editor needing a file.
5431
+ *
5432
+ * So the file is brought to it. Read the current source from the repository,
5433
+ * put it in a scratch directory shaped the way the editor expects, let the
5434
+ * editor do exactly what it does locally, and read the result back. The editor
5435
+ * is unchanged and untouched — which matters, because it is the part that knows
5436
+ * how to preserve a collection file's imports, comments and formatting, and a
5437
+ * second implementation of that for the remote case would be a second thing to
5438
+ * get wrong.
5439
+ *
5440
+ * ## What the scratch directory holds
5441
+ *
5442
+ * One file: the collection being edited. Not the whole directory.
5443
+ *
5444
+ * The editor resolves `<dir>/<id>.ts` and rewrites it in place; it does not
5445
+ * read its siblings to do that. Fetching them all would be a request per
5446
+ * collection on every save, for files nothing reads. A collection that does not
5447
+ * exist yet is simply absent, which is the same state the editor sees locally
5448
+ * when somebody adds one.
5449
+ */
5450
+ /**
5451
+ * Fetch, rewrite, and return the changed file — without leaving anything behind.
5452
+ *
5453
+ * The scratch directory is removed on every path, including the failing one. It
5454
+ * holds a copy of the customer's collection source, and a temp directory that
5455
+ * accumulates those on a long-running server is a slow leak of exactly the
5456
+ * thing worth not leaking.
5457
+ */
5458
+ async function rewriteRemoteCollection(options, collectionId, collection) {
5459
+ const { repository, collectionsPath, edit } = options;
5460
+ if (!repository.readFile) throw new Error("This repository cannot read a file, so the collection source cannot be rewritten for a deployment that has none on disk.");
5461
+ const repoPath = `${collectionsPath.replace(/\/+$/, "")}/${collectionId}.ts`;
5462
+ const existing = await repository.readFile(repoPath);
5463
+ const scratch = await fsp.mkdtemp(path$1.join(os.tmpdir(), "rebase-remote-source-"));
5464
+ try {
5465
+ if (existing !== void 0) await fsp.writeFile(path$1.join(scratch, `${collectionId}.ts`), existing, "utf8");
5466
+ await edit(scratch, collectionId, collection);
5467
+ return [{
5468
+ path: repoPath,
5469
+ contents: await fsp.readFile(path$1.join(scratch, `${collectionId}.ts`), "utf8")
5470
+ }];
5471
+ } finally {
5472
+ await fsp.rm(scratch, {
5473
+ recursive: true,
5474
+ force: true
5475
+ });
5476
+ }
5477
+ }
5478
+ //#endregion
5479
+ //#region src/schema-edit/local-git-repository.ts
5480
+ /**
5481
+ * A {@link SchemaEditRepository} backed by the working tree the server is
5482
+ * running in.
5483
+ *
5484
+ * This is the implementation for the two deployment shapes that have their
5485
+ * source on disk — `rebase dev` on a developer's machine, and a self-host with
5486
+ * the project mounted. Both have a `.git` next to the collections, so a commit
5487
+ * is a local `git commit`: nothing to authenticate, nothing to fetch, no token.
5488
+ *
5489
+ * The remote implementation, for a Cloud tenant whose repository lives on
5490
+ * GitHub, satisfies the same interface and is a separate file. Neither knows
5491
+ * about the other.
5492
+ *
5493
+ * ## Why it stages paths explicitly
5494
+ *
5495
+ * Never `git add -A`. A schema change runs while somebody may have unrelated
5496
+ * work in the tree, and sweeping that into a commit nobody wrote is worse than
5497
+ * refusing — it is a commit that cannot be reviewed because nobody knows what
5498
+ * is in it. `applySchemaChange` refuses up front when the tree already has
5499
+ * *our* files modified; this stages the exact list and nothing else.
5500
+ */
5501
+ var run$1 = promisify$1(execFile);
5502
+ var GitCommandError = class extends Error {
5503
+ constructor(command, detail) {
5504
+ super(`git ${command} failed: ${detail}`);
5505
+ this.name = "GitCommandError";
5506
+ }
5507
+ };
5508
+ /**
5509
+ * `--` before paths, always: a file called `main` would otherwise be read as a
5510
+ * revision, and the failure mode is a command that does something else entirely.
5511
+ */
5512
+ async function git(root, args) {
5513
+ try {
5514
+ const { stdout } = await run$1("git", [
5515
+ "-c",
5516
+ "core.fsmonitor=false",
5517
+ ...args
5518
+ ], {
5519
+ cwd: root,
5520
+ maxBuffer: 32 * 1024 * 1024
5521
+ });
5522
+ return stdout;
5523
+ } catch (err) {
5524
+ const detail = err instanceof Error ? err.stderr?.trim() || err.message : String(err);
5525
+ throw new GitCommandError(args[0] ?? "", detail);
5526
+ }
5527
+ }
5528
+ /**
5529
+ * The top of the working tree containing `startDir`, or undefined.
5530
+ *
5531
+ * Resolved by asking git rather than by walking for a `.git`, because a
5532
+ * worktree's `.git` is a file and a submodule's is neither where nor what a
5533
+ * naive walk expects.
5534
+ */
5535
+ async function findRepositoryRoot(startDir) {
5536
+ try {
5537
+ const root = (await git(startDir, ["rev-parse", "--show-toplevel"])).trim();
5538
+ return root.length > 0 ? root : void 0;
5539
+ } catch {
5540
+ return;
5541
+ }
5542
+ }
5543
+ function createLocalGitRepository(options) {
5544
+ const { root } = options;
5545
+ return {
5546
+ root,
5547
+ async currentBranch() {
5548
+ const branch = (await git(root, [
5549
+ "rev-parse",
5550
+ "--abbrev-ref",
5551
+ "HEAD"
5552
+ ])).trim();
5553
+ if (branch === "HEAD") throw new GitCommandError("rev-parse", "the working tree is on a detached HEAD, so there is no branch to commit to. Check out a branch first.");
5554
+ return branch;
5555
+ },
5556
+ async dirtyPaths() {
5557
+ const out = await git(root, [
5558
+ "status",
5559
+ "--porcelain=v1",
5560
+ "-z"
5561
+ ]);
5562
+ const paths = [];
5563
+ for (const entry of out.split("\0")) {
5564
+ if (!entry) continue;
5565
+ const file = entry.slice(3);
5566
+ if (file) paths.push(file);
5567
+ }
5568
+ return paths;
5569
+ },
5570
+ async writeFiles(files) {
5571
+ for (const file of files) {
5572
+ const absolute = path$2.resolve(root, file.path);
5573
+ if (!absolute.startsWith(path$2.resolve(root) + path$2.sep)) throw new Error(`Refusing to write outside the repository: ${file.path}`);
5574
+ await fs$2.mkdir(path$2.dirname(absolute), { recursive: true });
5575
+ await fs$2.writeFile(absolute, file.contents, "utf8");
5576
+ }
5577
+ },
5578
+ async commit(paths, message) {
5579
+ if (paths.length === 0) throw new Error("Refusing to commit an empty path list.");
5580
+ await git(root, [
5581
+ "add",
5582
+ "--",
5583
+ ...paths
5584
+ ]);
5585
+ if ((await git(root, [
5586
+ "diff",
5587
+ "--cached",
5588
+ "--name-only",
5589
+ "--",
5590
+ ...paths
5591
+ ])).trim() === "") return (await git(root, ["rev-parse", "HEAD"])).trim();
5592
+ const identity = options.author ? [
5593
+ "-c",
5594
+ `user.name=${options.author.name}`,
5595
+ "-c",
5596
+ `user.email=${options.author.email}`
5597
+ ] : [];
5598
+ await git(root, [
5599
+ ...identity,
5600
+ "commit",
5601
+ "--only",
5602
+ "-m",
5603
+ message,
5604
+ "--",
5605
+ ...paths
5606
+ ]);
5607
+ return (await git(root, ["rev-parse", "HEAD"])).trim();
5608
+ }
5609
+ };
5610
+ }
5611
+ //#endregion
5612
+ //#region src/rls-audit/index.ts
5613
+ /**
5614
+ * The row-level-security audit, on a schedule.
5615
+ *
5616
+ * `@rebasepro/rls-check` is a thorough, read-only audit of a database's RLS —
5617
+ * more thorough, on security specifically, than the dashboard advisors it
5618
+ * competes with. And nobody runs it, because it is a command, and a command is
5619
+ * something you have to remember. A check that has to be remembered is a check
5620
+ * that reports a clean database right up until the day it matters.
5621
+ *
5622
+ * This runs it on a timer against the database the server is already using, and
5623
+ * makes the result readable without a terminal. Nothing else about the checker
5624
+ * changes: same package, same checks, same output.
5625
+ *
5626
+ * ## Why it is off by default
5627
+ *
5628
+ * It opens its own connection and introspects the whole catalog. That is cheap
5629
+ * and read-only, but it is not nothing, and a background query nobody asked for
5630
+ * is a poor default for a library. Enabling it is one line, and the boot log
5631
+ * says so when it is off.
5632
+ *
5633
+ * ## Why the scanner is injected rather than imported
5634
+ *
5635
+ * `@rebasepro/server` is engine-agnostic — it has no `pg` and no other database
5636
+ * driver, deliberately. `@rebasepro/rls-check` has `pg`, because it connects to
5637
+ * Postgres. Importing it here would put a Postgres driver in the install of
5638
+ * every Mongo and Firebase user, for a feature none of them can use, and an
5639
+ * audit of row-level security is a Postgres concept living in the one package
5640
+ * that is supposed to know nothing about Postgres.
5641
+ *
5642
+ * So the scan function is handed in:
5643
+ *
5644
+ * ```ts
5645
+ * import { scan } from "@rebasepro/rls-check";
5646
+ * rlsAudit: { enabled: true, scan }
5647
+ * ```
5648
+ *
5649
+ * Everything else — the schedule, the summary, the status, the admin route —
5650
+ * lives here and is engine-agnostic in fact as well as in principle.
5651
+ */
5652
+ var SEVERITY_ORDER = [
5653
+ "info",
5654
+ "low",
5655
+ "medium",
5656
+ "high",
5657
+ "critical"
5658
+ ];
5659
+ /** Rank for comparison. Unknown severities sort lowest rather than throwing. */
5660
+ var rank = (severity) => {
5661
+ const index = SEVERITY_ORDER.indexOf(severity);
5662
+ return index === -1 ? 0 : index;
5663
+ };
5664
+ var DEFAULT_INTERVAL_MS = 1440 * 60 * 1e3;
5665
+ var DEFAULT_STATEMENT_TIMEOUT_MS = 3e4;
5666
+ /** One line naming what the run found, at the right level to be noticed. */
5667
+ function summarize(result, warnAt) {
5668
+ const counts = /* @__PURE__ */ new Map();
5669
+ for (const finding of result.findings) counts.set(finding.severity, (counts.get(finding.severity) ?? 0) + 1);
5670
+ const worst = result.findings.reduce((acc, f) => rank(f.severity) > rank(acc) ? f.severity : acc, "info");
5671
+ if (result.findings.length === 0) return {
5672
+ level: "info",
5673
+ message: `RLS audit clean: ${result.stats.tables} table(s), ${result.stats.policies} polic(ies), ${result.stats.checksRun} check(s), no findings.`
5674
+ };
5675
+ const breakdown = [...SEVERITY_ORDER].reverse().filter((severity) => counts.get(severity)).map((severity) => `${counts.get(severity)} ${severity}`).join(", ");
5676
+ const message = `RLS audit found ${result.findings.length} issue(s) — ${breakdown}. ${result.stats.tablesWithoutRls} table(s) without RLS. Read the detail at GET /api/admin/rls-audit, or run \`npx @rebasepro/rls-check\` for the full report.`;
5677
+ return {
5678
+ level: rank(worst) >= rank(warnAt) ? "warn" : "info",
5679
+ message
5680
+ };
5681
+ }
5682
+ /**
5683
+ * Create the audit. Nothing runs until {@link RlsAudit.start}.
5684
+ *
5685
+ * Because the scanner is supplied by the caller, the schedule, the summary and
5686
+ * the failure handling are all testable without a database and without
5687
+ * `@rebasepro/rls-check` present.
5688
+ */
5689
+ function createRlsAudit(config) {
5690
+ const enabled = config.enabled === true;
5691
+ const intervalMs = config.intervalMs ?? DEFAULT_INTERVAL_MS;
5692
+ const warnAt = config.warnAtSeverity ?? "high";
5693
+ const connectionString = config.connectionString ?? process.env.DATABASE_URL;
5694
+ let timer;
5695
+ let status = { enabled };
5696
+ if (enabled && !config.scan) status = {
5697
+ enabled: false,
5698
+ reason: "The scheduled RLS audit is enabled but no scanner was supplied. Pass `scan` from `@rebasepro/rls-check`: `rlsAudit: { enabled: true, scan }`."
5699
+ };
5700
+ else if (enabled && !connectionString) status = {
5701
+ enabled: false,
5702
+ reason: "The scheduled RLS audit is enabled but no connection string is available. Set DATABASE_URL, or pass `rlsAudit.connectionString`."
5703
+ };
5704
+ const runnable = enabled && !!connectionString && !!config.scan;
5705
+ const run = async () => {
5706
+ if (!runnable) return;
4295
5707
  try {
4296
- await backend.shutdown(timeoutMs);
4297
- if (onCleanup) await onCleanup();
4298
- clearTimeout(forceTimer);
4299
- logger.info("Graceful shutdown complete.");
4300
- exit(0);
5708
+ const result = await config.scan({
5709
+ connectionString,
5710
+ schemas: config.schemas,
5711
+ statementTimeoutMs: config.statementTimeoutMs ?? DEFAULT_STATEMENT_TIMEOUT_MS
5712
+ });
5713
+ const { level, message } = summarize(result, warnAt);
5714
+ logger[level](`[rls-audit] ${message}`);
5715
+ status = {
5716
+ enabled: true,
5717
+ lastRunAt: result.scannedAt,
5718
+ result
5719
+ };
4301
5720
  } catch (err) {
4302
- logger.error("Error during shutdown cleanup:", { error: err instanceof Error ? err : new Error(String(err)) });
4303
- exit(1);
5721
+ const detail = err instanceof Error ? err.message : String(err);
5722
+ logger.error(`[rls-audit] Scan failed: ${detail}`);
5723
+ status = {
5724
+ ...status,
5725
+ enabled: true,
5726
+ lastRunAt: (/* @__PURE__ */ new Date()).toISOString(),
5727
+ lastError: detail
5728
+ };
4304
5729
  }
4305
5730
  };
4306
- const listeners = signals.map((signal) => {
4307
- const listener = () => {
4308
- shutdownSequence(signal);
4309
- };
4310
- process.on(signal, listener);
4311
- return {
4312
- signal,
4313
- listener
4314
- };
4315
- });
4316
- return () => {
4317
- for (const { signal, listener } of listeners) process.removeListener(signal, listener);
4318
- };
4319
- }
4320
- function createShutdown(config) {
4321
- return (timeoutMs = 15e3) => {
4322
- return new Promise((resolve) => {
4323
- (async () => {
4324
- logger.info("Shutting down Rebase Backend...");
4325
- if (config.cronScheduler) {
4326
- config.cronScheduler.stop();
4327
- logger.info("Cron scheduler stopped");
4328
- }
4329
- if (config.jobQueue) {
4330
- await config.jobQueue.stop();
4331
- logger.info("Job queue stopped");
4332
- }
4333
- for (const [key, rt] of Object.entries(config.realtimeServices)) try {
4334
- if (typeof rt.destroy === "function") {
4335
- await rt.destroy();
4336
- logger.info(`Realtime service "${key}" destroyed`);
4337
- } else if (typeof rt.stopListening === "function") {
4338
- await rt.stopListening();
4339
- logger.info(`Realtime service "${key}" LISTEN client stopped`);
4340
- }
4341
- } catch (err) {
4342
- logger.warn(`Error destroying realtime service "${key}":`, { error: err });
4343
- }
4344
- config.server.close(() => {
4345
- logger.info("HTTP server closed");
4346
- resolve();
4347
- });
4348
- if (timeoutMs > 0) setTimeout(() => {
4349
- logger.warn(`Forced shutdown after ${timeoutMs / 1e3}s timeout`);
4350
- resolve();
4351
- }, timeoutMs).unref();
4352
- })();
4353
- });
5731
+ return {
5732
+ start() {
5733
+ if (!runnable) {
5734
+ if (status.reason) logger.warn(`[rls-audit] ${status.reason}`);
5735
+ return;
5736
+ }
5737
+ if (timer) return;
5738
+ timer = setInterval(() => {
5739
+ run();
5740
+ }, intervalMs);
5741
+ timer.unref?.();
5742
+ if (config.runOnBoot !== false) run();
5743
+ },
5744
+ stop() {
5745
+ if (timer) clearInterval(timer);
5746
+ timer = void 0;
5747
+ },
5748
+ runNow: run,
5749
+ status: () => status
4354
5750
  };
4355
5751
  }
4356
5752
  //#endregion
@@ -4383,7 +5779,8 @@ function resolveSurfaces(options) {
4383
5779
  function resolveOwnership(options) {
4384
5780
  return {
4385
5781
  cronScheduler: options?.cronScheduler ?? true,
4386
- jobWorkers: options?.jobWorkers ?? true
5782
+ jobWorkers: options?.jobWorkers ?? true,
5783
+ rlsAudit: options?.rlsAudit ?? true
4387
5784
  };
4388
5785
  }
4389
5786
  /** The surfaces a process is not serving, for a boot log line. */
@@ -10385,8 +11782,35 @@ function createHistoryRoutes(params) {
10385
11782
  * version or not — reads the same live client.
10386
11783
  */
10387
11784
  var INSTANCE_SLOT = Symbol.for("@rebasepro/server:singleton-instance");
11785
+ /**
11786
+ * A *function* that answers "which client is this call for", for hosts where
11787
+ * one answer per process is the wrong shape.
11788
+ *
11789
+ * A Node server boots once and serves every request from the same client, so
11790
+ * {@link INSTANCE_SLOT} is the whole story. An isolate-based host is different
11791
+ * in a way that matters: the environment arrives attached to the **request**,
11792
+ * not to the module, so there is nothing to publish at import time, and an
11793
+ * isolate can be reused across requests that must not share state. Such a host
11794
+ * registers a resolver — typically reading an `AsyncLocalStorage` populated per
11795
+ * request — and it is consulted first.
11796
+ *
11797
+ * This exists now, before there is a host that needs it, because of what it
11798
+ * protects: `rebase` is a lazy Proxy, so every property access already goes
11799
+ * through `getInstance()`. Backing that lookup with a resolver is therefore the
11800
+ * entire porting story for the singleton — **no function anyone has already
11801
+ * written changes**. Take the resolver away and the only alternative is asking
11802
+ * users to thread a client through their handlers, which is a rewrite of every
11803
+ * function file in existence.
11804
+ */
11805
+ var RESOLVER_SLOT = Symbol.for("@rebasepro/server:singleton-resolver");
10388
11806
  function getInstance() {
10389
- return globalThis[INSTANCE_SLOT] ?? null;
11807
+ const global = globalThis;
11808
+ const resolver = global[RESOLVER_SLOT];
11809
+ if (resolver) {
11810
+ const resolved = resolver();
11811
+ if (resolved) return resolved;
11812
+ }
11813
+ return global[INSTANCE_SLOT] ?? null;
10390
11814
  }
10391
11815
  function setInstance(client) {
10392
11816
  globalThis[INSTANCE_SLOT] = client;
@@ -10403,7 +11827,7 @@ function _initRebase(client) {
10403
11827
  * Throws an error if used in a non-test environment to prevent production abuse.
10404
11828
  */
10405
11829
  function _setRebaseMock(mockInstance) {
10406
- if (process.env.NODE_ENV !== "test") throw new Error("_setRebaseMock can only be called in a test environment (NODE_ENV=test).");
11830
+ if (hostEnv().NODE_ENV !== "test") throw new Error("_setRebaseMock can only be called in a test environment (NODE_ENV=test).");
10407
11831
  setInstance({
10408
11832
  ...getInstance() || {},
10409
11833
  ...mockInstance
@@ -10413,7 +11837,7 @@ function _setRebaseMock(mockInstance) {
10413
11837
  * @internal Resets the singleton instance, useful for afterEach() in test suites.
10414
11838
  */
10415
11839
  function _resetRebaseMock() {
10416
- if (process.env.NODE_ENV !== "test") throw new Error("_resetRebaseMock can only be called in a test environment.");
11840
+ if (hostEnv().NODE_ENV !== "test") throw new Error("_resetRebaseMock can only be called in a test environment.");
10417
11841
  setInstance(null);
10418
11842
  }
10419
11843
  /**
@@ -10865,7 +12289,7 @@ async function _initializeRebaseBackend(config) {
10865
12289
  ]) {
10866
12290
  const providerConfig = safeAuthConfig[key];
10867
12291
  if (providerConfig && requiredFields.every((f) => Boolean(providerConfig[f]))) {
10868
- const createFn = (await import("./auth-5Et5mnUA.js").then((n) => n.t))[factory];
12292
+ const createFn = (await import("./auth-C74dqR2B.js").then((n) => n.t))[factory];
10869
12293
  oauthProviders.push(createFn(providerConfig));
10870
12294
  }
10871
12295
  }
@@ -10879,6 +12303,7 @@ async function _initializeRebaseBackend(config) {
10879
12303
  disableSelfRegistration: safeAuthConfig.disableSelfRegistration ?? false,
10880
12304
  allowAnonymous: safeAuthConfig.allowAnonymous ?? false,
10881
12305
  allowUserLookup: safeAuthConfig.allowUserLookup ?? false,
12306
+ captcha: safeAuthConfig.captcha,
10882
12307
  defaultRole: safeAuthConfig.defaultRole,
10883
12308
  oauthProviders,
10884
12309
  allowedRedirectUris: safeAuthConfig.allowedRedirectUris,
@@ -10965,7 +12390,7 @@ async function _initializeRebaseBackend(config) {
10965
12390
  let schemaEditorOff = schemaEditorUnavailable();
10966
12391
  let schemaEditorRoutes;
10967
12392
  if (!schemaEditorOff && config.collectionsDir) try {
10968
- schemaEditorRoutes = (await import("./schema-editor-routes-CV9k0w3G.js")).createSchemaEditorRoutes(config.collectionsDir);
12393
+ schemaEditorRoutes = (await import("./schema-editor-routes-yh2cQcdJ.js")).createSchemaEditorRoutes(config.collectionsDir);
10969
12394
  } catch (err) {
10970
12395
  if (err?.code === "ERR_MODULE_NOT_FOUND") {
10971
12396
  schemaEditorOff = {
@@ -10988,18 +12413,101 @@ async function _initializeRebaseBackend(config) {
10988
12413
  code: schemaEditorOff.code,
10989
12414
  message: schemaEditorOff.message
10990
12415
  } }, 501));
10991
- config.app.route(`${basePath}/schema-editor`, schemaEditorRouter);
10992
- if (schemaEditorRoutes) logger.debug("Schema Editor mounted", { path: `${basePath}/schema-editor` });
10993
- else logger.debug("Schema Editor unavailable", {
10994
- path: `${basePath}/schema-editor`,
10995
- code: schemaEditorOff.code
12416
+ const remoteRepo = config.liveSchema?.repository;
12417
+ const canEditSchema = Boolean(config.collectionsDir) || Boolean(remoteRepo);
12418
+ if (!canEditSchema) {
12419
+ const unconfigured = new Hono();
12420
+ applyAdminGate(unconfigured, "Live schema editing");
12421
+ unconfigured.all("/*", (c) => c.json({ error: {
12422
+ code: "SCHEMA_EDITING_NO_COLLECTIONS_DIR",
12423
+ message: "Live schema editing needs `collectionsDir` — the directory holding this project's collection definitions — or `liveSchema.repository`, a repository to fetch them from. This server was started with neither, so there is nothing for it to edit."
12424
+ } }, 501));
12425
+ mountWithLegacyAlias(config.app, unconfigured, {
12426
+ canonical: `${basePath}/admin/schema`,
12427
+ surface: "Live schema editing (unconfigured)"
12428
+ });
12429
+ }
12430
+ if (canEditSchema) {
12431
+ const collectionsDir = config.collectionsDir;
12432
+ const repositoryRoot = collectionsDir ? await findRepositoryRoot(collectionsDir) : void 0;
12433
+ const liveSchemaRouter = new Hono();
12434
+ applyAdminGate(liveSchemaRouter, "Live schema editing");
12435
+ const commitPaths = remoteRepo ? commitPathsFor(path$1.join("/", remoteRepo.collectionsPath ?? "config/collections"), "/") : collectionsDir && repositoryRoot ? commitPathsFor(collectionsDir, repositoryRoot) : void 0;
12436
+ if (commitPaths) logger.debug("Live schema editing commit paths relocated", { commitPaths });
12437
+ liveSchemaRouter.route("/", createLiveSchemaRoutes({
12438
+ commitPaths,
12439
+ policy: { allowMachineApply: config.liveSchema?.allowMachineApply ?? process.env.REBASE_LIVE_SCHEMA_ALLOW_MACHINE_APPLY === "true" },
12440
+ getCollections: () => collectionRegistry.getRawCollections(),
12441
+ getAdmin: () => defaultDriver.admin,
12442
+ getRepository: (author) => {
12443
+ if (remoteRepo) return createGitHubRepository({
12444
+ auth: remoteRepo.auth,
12445
+ owner: remoteRepo.owner,
12446
+ repo: remoteRepo.repo,
12447
+ branch: remoteRepo.branch ?? "main",
12448
+ author
12449
+ });
12450
+ if (!repositoryRoot) return void 0;
12451
+ return createLocalGitRepository({
12452
+ root: repositoryRoot,
12453
+ author
12454
+ });
12455
+ },
12456
+ writeSource: schemaEditorRoutes ? async (change) => {
12457
+ const applyEdit = async (dir, collectionId, collection) => {
12458
+ const { AstSchemaEditor } = await import("./ast-schema-editor-BpFYpT38.js");
12459
+ await new AstSchemaEditor(dir).saveCollection(collectionId, collection, { partial: false });
12460
+ };
12461
+ if (remoteRepo) {
12462
+ const repository = config.liveSchema?.repository ? createGitHubRepository({
12463
+ auth: remoteRepo.auth,
12464
+ owner: remoteRepo.owner,
12465
+ repo: remoteRepo.repo,
12466
+ branch: remoteRepo.branch ?? "main"
12467
+ }) : void 0;
12468
+ if (!repository) return [];
12469
+ return rewriteRemoteCollection({
12470
+ repository,
12471
+ collectionsPath: remoteRepo.collectionsPath ?? "config/collections",
12472
+ edit: applyEdit
12473
+ }, change.collectionId, change.collection);
12474
+ }
12475
+ await applyEdit(collectionsDir, change.collectionId, change.collection);
12476
+ const file = path$1.join(collectionsDir, `${change.collectionId}.ts`);
12477
+ const contents = await fsp.readFile(file, "utf8");
12478
+ return [{
12479
+ path: path$1.relative(repositoryRoot, file),
12480
+ contents
12481
+ }];
12482
+ } : void 0,
12483
+ sourcePathsFor: (change) => {
12484
+ if (remoteRepo) return [`${(remoteRepo.collectionsPath ?? "config/collections").replace(/\/+$/, "")}/${change.collectionId}.ts`];
12485
+ if (!collectionsDir || !repositoryRoot) return [];
12486
+ return [path$1.relative(repositoryRoot, path$1.join(collectionsDir, `${change.collectionId}.ts`))];
12487
+ }
12488
+ }));
12489
+ mountWithLegacyAlias(config.app, liveSchemaRouter, {
12490
+ canonical: `${basePath}/admin/schema`,
12491
+ surface: "Live schema editing"
12492
+ });
12493
+ logger.debug("Live schema editing repository", { repository: repositoryRoot ?? "(none — refusals only)" });
12494
+ }
12495
+ mountWithLegacyAlias(config.app, schemaEditorRouter, {
12496
+ canonical: `${basePath}/admin/schema-editor`,
12497
+ legacy: `${basePath}/schema-editor`,
12498
+ surface: "Schema editor"
10996
12499
  });
12500
+ if (!schemaEditorRoutes) logger.debug("Schema Editor unavailable", { code: schemaEditorOff.code });
10997
12501
  }
10998
12502
  const storageAuthorizeData = {};
10999
12503
  if (storageController) {
11000
12504
  const storageMaxSize = (config.storage && typeof config.storage === "object" && "type" in config.storage ? config.storage.maxFileSize : void 0) ?? 50 * 1024 * 1024;
12505
+ const storageAccessControl = resolveStorageAccessControl({
12506
+ storagePolicies: config.storagePolicies,
12507
+ storageAuthorize: config.storageAuthorize
12508
+ });
11001
12509
  assertStorageAccessControlConfigured({
11002
- hasAuthorize: !!config.storageAuthorize,
12510
+ hasAuthorize: !!config.storageAuthorize || !!config.storagePolicies?.length,
11003
12511
  publicRead: config.storagePublicRead === true,
11004
12512
  allowAnyAuthenticated: config.storageInsecureAllowAnyAuthenticated === true
11005
12513
  }, isProduction);
@@ -11010,7 +12518,7 @@ async function _initializeRebaseBackend(config) {
11010
12518
  requireAuth: resolveRequireAuth(config.auth),
11011
12519
  publicRead: config.storagePublicRead === true,
11012
12520
  authAdapter,
11013
- authorize: config.storageAuthorize,
12521
+ authorize: storageAccessControl,
11014
12522
  authorizeData: () => storageAuthorizeData.current
11015
12523
  });
11016
12524
  const storageRouter = new Hono();
@@ -11147,9 +12655,9 @@ async function _initializeRebaseBackend(config) {
11147
12655
  if (driver && "client" in driver) driver.client = serverClient;
11148
12656
  }
11149
12657
  if (config.functionsDir && surfaces.functions) {
11150
- const { loadFunctionsWithDiagnostics } = await import("./function-loader-DDS1v7YX.js").then((n) => n.t);
12658
+ const { loadFunctionsWithDiagnostics } = await import("./function-loader-BrLmC_-y.js").then((n) => n.t);
11151
12659
  const { createFunctionRoutes } = await import("./function-routes-Btcez1T-.js").then((n) => n.n);
11152
- const { createFunctionsRequestTimeout, resolveFunctionsTimeoutMs } = await import("./request-timeout-BuFoEKwT.js");
12660
+ const { createFunctionsRequestTimeout, resolveFunctionsTimeoutMs } = await import("./request-timeout-OofPCHQT.js").then((n) => n.t);
11153
12661
  const { functions: allFunctions, problems } = await loadFunctionsWithDiagnostics(config.functionsDir);
11154
12662
  const { selectFunctions } = await import("./selection-_z6TM1DB.js").then((n) => n.n);
11155
12663
  const loadedFunctions = selectFunctions(allFunctions, config.functionsSelection);
@@ -11190,7 +12698,7 @@ async function _initializeRebaseBackend(config) {
11190
12698
  else logger.warn(`Function routes mounted at ${basePath}/functions, but no functions loaded from ${config.functionsDir}. ` + (problems.length > 0 ? `${problems.length} file(s) were skipped — see the messages above.` : "The directory holds no .ts/.js function files."));
11191
12699
  }
11192
12700
  if (config.functionsUpstream && !surfaces.functions) {
11193
- const { createFunctionsProxy } = await import("./proxy-Bj5DVllb.js");
12701
+ const { createFunctionsProxy } = await import("./proxy-QJKSS-CV.js").then((n) => n.t);
11194
12702
  config.app.route(`${basePath}/functions`, createFunctionsProxy({
11195
12703
  upstream: config.functionsUpstream,
11196
12704
  basePath: `${basePath}/functions`
@@ -11202,10 +12710,10 @@ async function _initializeRebaseBackend(config) {
11202
12710
  }
11203
12711
  let cronScheduler;
11204
12712
  if (config.cronsDir && (surfaces.cron || ownership.cronScheduler)) {
11205
- const { loadCronJobsWithDiagnostics } = await import("./cron-loader-YhhQeVBM.js").then((n) => n.t);
11206
- const { CronScheduler } = await import("./cron-scheduler-DIpYBmZP.js").then((n) => n.n);
11207
- const { createCronRoutes } = await import("./cron-routes-maM_RlUu.js").then((n) => n.n);
11208
- const { createCronStore } = await import("./cron-store-DfH_4Cd9.js").then((n) => n.n);
12713
+ const { loadCronJobsWithDiagnostics } = await import("./cron-loader-BMvtW6-J.js").then((n) => n.t);
12714
+ const { CronScheduler } = await import("./cron-scheduler-Buf-uVam.js").then((n) => n.n);
12715
+ const { createCronRoutes } = await import("./cron-routes-BvYk-Kmi.js").then((n) => n.n);
12716
+ const { createCronStore } = await import("./cron-store-CyF-oInV.js").then((n) => n.n);
11209
12717
  const { jobs: loadedCronJobs, problems: cronProblems } = await loadCronJobsWithDiagnostics(config.cronsDir);
11210
12718
  cronScheduler = new CronScheduler();
11211
12719
  cronScheduler.setClient(serverSingleton);
@@ -11220,7 +12728,11 @@ async function _initializeRebaseBackend(config) {
11220
12728
  const cronRouter = new Hono();
11221
12729
  applyAdminGate(cronRouter, "Cron");
11222
12730
  cronRouter.route("/", createCronRoutes(cronScheduler, cronProblems.length));
11223
- if (surfaces.cron) config.app.route(`${basePath}/cron`, cronRouter);
12731
+ if (surfaces.cron) mountWithLegacyAlias(config.app, cronRouter, {
12732
+ canonical: `${basePath}/admin/cron`,
12733
+ legacy: `${basePath}/cron`,
12734
+ surface: "Cron"
12735
+ });
11224
12736
  if (loadedCronJobs.length > 0 && !ownership.cronScheduler) logger.info("Cron jobs registered but NOT scheduled in this process", {
11225
12737
  count: loadedCronJobs.length,
11226
12738
  reason: "this process does not own the cron scheduler"
@@ -11235,7 +12747,7 @@ async function _initializeRebaseBackend(config) {
11235
12747
  }
11236
12748
  let jobQueue;
11237
12749
  if (config.jobs?.enabled) {
11238
- const { createJobStore, createJobQueue } = await import("./jobs-CyOKXXlu.js").then((n) => n.t);
12750
+ const { createJobStore, createJobQueue } = await import("./jobs-Bn6nhY8J.js").then((n) => n.t);
11239
12751
  const store = createJobStore(defaultDriver);
11240
12752
  if (store) {
11241
12753
  await store.ensureTable();
@@ -11246,8 +12758,9 @@ async function _initializeRebaseBackend(config) {
11246
12758
  } else logger.info("Job queue available for enqueue, workers NOT started in this process", { tasks: Object.keys(config.jobs.tasks ?? {}).length });
11247
12759
  } else logger.warn("Job queue requested but unavailable on this driver — `rebase.jobs.enqueue` will throw, and webhook deliveries fall back to the in-memory queue.");
11248
12760
  }
12761
+ let rlsAudit;
11249
12762
  if (surfaces.admin) {
11250
- const { createBackupRoutes, parseBackupDestination } = await import("./backup-C6ljYVTp.js").then((n) => n.t);
12763
+ const { createBackupRoutes, parseBackupDestination } = await import("./backup-BJ86ah4T.js").then((n) => n.t);
11251
12764
  const backupRouter = new Hono();
11252
12765
  applyAdminGate(backupRouter, "Backup");
11253
12766
  backupRouter.route("/", createBackupRoutes({
@@ -11260,16 +12773,29 @@ async function _initializeRebaseBackend(config) {
11260
12773
  config.app.route(`${basePath}/admin/backups`, backupRouter);
11261
12774
  logger.debug("Backup admin routes mounted", { path: `${basePath}/admin/backups` });
11262
12775
  }
12776
+ if (surfaces.admin) {
12777
+ const rlsAuditRouter = new Hono();
12778
+ applyAdminGate(rlsAuditRouter, "RLS audit");
12779
+ rlsAuditRouter.get("/", (c) => c.json(rlsAudit ? rlsAudit.status() : {
12780
+ enabled: false,
12781
+ reason: config.rlsAudit?.enabled ? "This process serves the admin surface but does not own the RLS audit." : "The scheduled RLS audit is off. Set `rlsAudit.enabled` to turn it on."
12782
+ }));
12783
+ config.app.route(`${basePath}/admin/rls-audit`, rlsAuditRouter);
12784
+ logger.debug("RLS audit admin routes mounted", { path: `${basePath}/admin/rls-audit` });
12785
+ }
11263
12786
  if (surfaces.admin) {
11264
12787
  const { default: logsRoutes } = await import("./logs-routes-CWBLQj2l.js").then((n) => n.n);
11265
12788
  const logsRouter = new Hono();
11266
12789
  applyAdminGate(logsRouter, "Logs");
11267
12790
  logsRouter.route("/", logsRoutes);
11268
- config.app.route(`${basePath}/logs`, logsRouter);
11269
- logger.debug("Logs routes mounted", { path: `${basePath}/logs` });
12791
+ mountWithLegacyAlias(config.app, logsRouter, {
12792
+ canonical: `${basePath}/admin/logs`,
12793
+ legacy: `${basePath}/logs`,
12794
+ surface: "Logs"
12795
+ });
11270
12796
  }
11271
12797
  if (surfaces.meta) {
11272
- const { createContractRoutes } = await import("./contract-routes-DZ-LBpSL.js").then((n) => n.t);
12798
+ const { createContractRoutes } = await import("./contract-routes-BEq7euZg.js").then((n) => n.t);
11273
12799
  const contractRouter = new Hono();
11274
12800
  if (adminSurfacesGated) {
11275
12801
  if (apiKeyPreAuth) contractRouter.use("/contract", apiKeyPreAuth);
@@ -11298,10 +12824,15 @@ async function _initializeRebaseBackend(config) {
11298
12824
  logger.debug("Rebase Backend Initialized");
11299
12825
  const authSchemaCheck = authConfigResult?.schemaHealthCheck;
11300
12826
  const healthCheck = createHealthCheck(defaultDriver, authSchemaCheck ? () => authSchemaCheck.call(authConfigResult) : void 0);
12827
+ if (config.rlsAudit?.enabled && ownership.rlsAudit) {
12828
+ rlsAudit = createRlsAudit(config.rlsAudit);
12829
+ rlsAudit.start();
12830
+ }
11301
12831
  const shutdown = createShutdown({
11302
12832
  server: config.server,
11303
12833
  cronScheduler,
11304
12834
  jobQueue,
12835
+ rlsAudit,
11305
12836
  realtimeServices
11306
12837
  });
11307
12838
  /**
@@ -11385,6 +12916,276 @@ function defineFunction(definition) {
11385
12916
  return returned instanceof Hono ? returned : app;
11386
12917
  }
11387
12918
  //#endregion
12919
+ //#region src/functions/context.ts
12920
+ function read(c, key) {
12921
+ return c.get(key);
12922
+ }
12923
+ /**
12924
+ * The authenticated caller, or `undefined` for an anonymous request.
12925
+ *
12926
+ * **`undefined` is not a permission decision.** The functions router mounts its
12927
+ * auth middleware with `requireAuth: false` on purpose — a webhook receiver has
12928
+ * no token to send — so an anonymous caller reaches the handler and reads
12929
+ * `undefined` here while the handler runs on regardless. Use {@link requireAuth}
12930
+ * (or a `!user` branch that returns 401) to make it a decision.
12931
+ *
12932
+ * A caller who presented a *bad* token never gets this far: both auth
12933
+ * middlewares reject an unverifiable token with 401 before the router is
12934
+ * reached, precisely so an expired session cannot be silently downgraded to an
12935
+ * anonymous one.
12936
+ */
12937
+ function getUser(c) {
12938
+ const raw = read(c, "user");
12939
+ if (!raw || typeof raw !== "object") return void 0;
12940
+ const record = raw;
12941
+ const uid = typeof record.uid === "string" ? record.uid : typeof record.userId === "string" ? record.userId : void 0;
12942
+ if (uid === void 0) return void 0;
12943
+ const roles = Array.isArray(record.roles) ? record.roles.filter((role) => typeof role === "string") : [];
12944
+ return {
12945
+ ...record,
12946
+ uid,
12947
+ roles
12948
+ };
12949
+ }
12950
+ /** The caller's id, or `undefined` when nobody is signed in. */
12951
+ function getUserId(c) {
12952
+ return getUser(c)?.uid;
12953
+ }
12954
+ /** The caller's roles. Empty for an anonymous request — never `undefined`. */
12955
+ function getRoles(c) {
12956
+ return getUser(c)?.roles ?? [];
12957
+ }
12958
+ /**
12959
+ * Whether the caller holds **any** of the named roles.
12960
+ *
12961
+ * Any rather than all, because that is what a route guard means by a list of
12962
+ * roles; require several by calling this more than once.
12963
+ */
12964
+ function hasRole(c, ...roles) {
12965
+ if (roles.length === 0) return false;
12966
+ const held = new Set(getRoles(c));
12967
+ return roles.some((role) => held.has(role));
12968
+ }
12969
+ /**
12970
+ * Whether the caller holds an administrative role.
12971
+ *
12972
+ * Delegates to the single definition in `auth/admin-roles.ts` — which is
12973
+ * `admin` **or** `schema-admin` — rather than comparing against `"admin"`.
12974
+ * Those two lists disagreed once, and the gap made every public registrant an
12975
+ * administrator; see that file.
12976
+ */
12977
+ function isAdmin(c) {
12978
+ return hasAdministrativeRole(getRoles(c));
12979
+ }
12980
+ /** Whether the request carries an identity at all. */
12981
+ function isAuthenticated(c) {
12982
+ return getUser(c) !== void 0;
12983
+ }
12984
+ /**
12985
+ * The request-scoped data driver: reads and writes run as **the caller**, with
12986
+ * your row-level security policies evaluated against their identity.
12987
+ *
12988
+ * This is the accessor to reach for when a function serves user-facing data.
12989
+ * `rebase.dataAsAdmin` is the other one, and it is not the same thing — it runs
12990
+ * as `{ uid: "service", roles: ["admin"] }` for every caller alike, which is
12991
+ * correct for trusted background work and wrong for a request.
12992
+ *
12993
+ * `undefined` only when no Rebase auth middleware ran (see
12994
+ * {@link identityResolved}); inside a function mounted by the framework it is
12995
+ * always present, anonymous requests included — they get an anon-scoped driver
12996
+ * so policies still have an identity to evaluate.
12997
+ */
12998
+ function getDriver(c) {
12999
+ return read(c, "driver");
13000
+ }
13001
+ /**
13002
+ * {@link getDriver}, but throws instead of handing back `undefined`.
13003
+ *
13004
+ * For the common case where a handler cannot proceed without it and would
13005
+ * otherwise write `c.get("driver")!` — an assertion that turns a wiring problem
13006
+ * into `Cannot read properties of undefined (reading 'fetchCollection')` twenty
13007
+ * lines away from the cause.
13008
+ */
13009
+ function requireDriver(c) {
13010
+ const driver = getDriver(c);
13011
+ if (!driver) throw new Error("No request-scoped driver on this context. A Rebase auth middleware populates it before any custom function runs, so this means the handler was mounted outside the functions router — e.g. added to your own Hono app directly. Mount it from the functions directory, or use `rebase.dataAsAdmin` if the work is genuinely service-scoped.");
13012
+ return driver;
13013
+ }
13014
+ /**
13015
+ * The API key this request authenticated with, masked, or `undefined` when it
13016
+ * did not use one.
13017
+ *
13018
+ * Useful for attribution and for per-key behaviour. The permission check itself
13019
+ * has already happened — reaching a handler means the key was allowed to.
13020
+ */
13021
+ function getApiKey(c) {
13022
+ return read(c, "apiKey");
13023
+ }
13024
+ /**
13025
+ * The correlation id for this request — generated, or taken from an inbound
13026
+ * `X-Request-ID`.
13027
+ *
13028
+ * Log it. It is the only thing that ties a line written inside a function to
13029
+ * the framework's own lines for the same request.
13030
+ */
13031
+ function getRequestId(c) {
13032
+ return read(c, "requestId");
13033
+ }
13034
+ /**
13035
+ * Whether a Rebase auth middleware has run on this request.
13036
+ *
13037
+ * Both middlewares populate `driver` for *every* outcome, anonymous included,
13038
+ * and populate `user` whenever there is one. So "neither is set" does not mean
13039
+ * "anonymous" — it means nothing resolved the identity, and treating that as
13040
+ * anonymous is the dangerous reading. The guards use this to tell a genuinely
13041
+ * anonymous caller (401) from a misconfigured mount (500), because answering
13042
+ * 401 to the second sends whoever is debugging it to look at the token.
13043
+ */
13044
+ function identityResolved(c) {
13045
+ return read(c, "user") !== void 0 || read(c, "driver") !== void 0;
13046
+ }
13047
+ //#endregion
13048
+ //#region src/functions/guards.ts
13049
+ /**
13050
+ * The answer to "a guard ran, but no middleware had resolved anything".
13051
+ *
13052
+ * Deliberately a 500 and not a 401. A 401 tells the caller their credential is
13053
+ * the problem, and here the caller's credential was never looked at — sending
13054
+ * them to check their token is sending them to the one place the answer is not.
13055
+ */
13056
+ function unresolvedIdentity() {
13057
+ return { error: {
13058
+ message: "This route's identity was never resolved: no Rebase auth middleware ran before the guard. A function loaded from the functions directory always has one. This usually means the Hono app was mounted onto your own server directly, bypassing the functions router.",
13059
+ code: "AUTH_MIDDLEWARE_MISSING"
13060
+ } };
13061
+ }
13062
+ /**
13063
+ * Reject callers holding none of the named roles with 403.
13064
+ *
13065
+ * Any of them, not all — require several by chaining the guard twice. Naming no
13066
+ * role at all is a programming error and throws at module load rather than at
13067
+ * request time, because `requireRole()` with an empty list would otherwise read
13068
+ * as a guard while admitting everyone.
13069
+ *
13070
+ * @example
13071
+ * ```ts
13072
+ * app.post("/publish", requireAuth, requireRole("editor", "admin"), handler);
13073
+ * ```
13074
+ */
13075
+ function requireRole(...roles) {
13076
+ if (roles.length === 0) throw new Error("requireRole() needs at least one role. An empty list would admit every signed-in caller while reading as a restriction.");
13077
+ const allowed = new Set(roles);
13078
+ return async (c, next) => {
13079
+ if (!getUser(c)) {
13080
+ if (!identityResolved(c)) return c.json(unresolvedIdentity(), 500);
13081
+ return c.json({ error: {
13082
+ message: "Authentication required",
13083
+ code: "UNAUTHORIZED"
13084
+ } }, 401);
13085
+ }
13086
+ if (!getRoles(c).some((role) => allowed.has(role))) return c.json({ error: {
13087
+ message: `This operation requires one of these roles: ${roles.join(", ")}`,
13088
+ code: "FORBIDDEN"
13089
+ } }, 403);
13090
+ return next();
13091
+ };
13092
+ }
13093
+ //#endregion
13094
+ //#region src/functions/runtime-env.ts
13095
+ /**
13096
+ * Every environment variable visible to this request.
13097
+ *
13098
+ * `c.env` on workerd, `Deno.env` on Deno, `process.env` on Node, Bun and
13099
+ * Vercel's edge runtime. Reading it through here rather than through `process`
13100
+ * is the whole of what makes a function's configuration portable.
13101
+ */
13102
+ function getEnv(c) {
13103
+ return env(c);
13104
+ }
13105
+ /**
13106
+ * One environment variable, or `undefined`.
13107
+ *
13108
+ * Trimmed, and a blank value counts as absent — declaring a variable and
13109
+ * leaving it empty is the ordinary way to write a compose file or a `.env`
13110
+ * line, and nobody has ever meant `""` by it.
13111
+ */
13112
+ function env$1(c, name) {
13113
+ const raw = getEnv(c)[name];
13114
+ if (typeof raw !== "string") return void 0;
13115
+ const trimmed = raw.trim();
13116
+ return trimmed === "" ? void 0 : trimmed;
13117
+ }
13118
+ /**
13119
+ * One environment variable, or a thrown error naming it.
13120
+ *
13121
+ * For configuration a handler cannot run without. Throwing here — inside the
13122
+ * request — is deliberately better than the module-scope `process.env.KEY!`
13123
+ * it replaces: that one takes the whole *file* down at load time, and the
13124
+ * loader reports it as "this function could not be imported", which names the
13125
+ * file but not the variable. This fails one request, with the name in the
13126
+ * message, while every other route in the file keeps serving.
13127
+ */
13128
+ function requireEnv(c, name) {
13129
+ const value = env$1(c, name);
13130
+ if (value === void 0) throw new Error(`Missing required environment variable ${name}. Set it on the process (or as a binding, on a host that has them) — and read it inside the handler, not at module scope, so the same file works on both.`);
13131
+ return value;
13132
+ }
13133
+ /**
13134
+ * Which host this is: `"node"`, `"workerd"`, `"deno"`, `"bun"`, `"edge-light"`,
13135
+ * `"fastly"` or `"other"`.
13136
+ *
13137
+ * Use it to *degrade*, not to branch a whole implementation — a function that
13138
+ * needs two implementations is two functions. It is honest about one thing in
13139
+ * particular: a Node-only capability, such as `rebase.sql()`, can check this
13140
+ * and say so, instead of failing at the call.
13141
+ */
13142
+ function runtimeKey() {
13143
+ return getRuntimeKey();
13144
+ }
13145
+ /** Whether this is a Node-like host — Node itself, or Bun's Node compatibility. */
13146
+ function isNodeRuntime() {
13147
+ const key = getRuntimeKey();
13148
+ return key === "node" || key === "bun";
13149
+ }
13150
+ /**
13151
+ * Build something expensive once per environment, on first use, from inside a
13152
+ * request.
13153
+ *
13154
+ * This is the sanctioned replacement for the module-scope client:
13155
+ *
13156
+ * ```ts
13157
+ * // Don't: runs at import time, before bindings exist on some hosts.
13158
+ * const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
13159
+ *
13160
+ * // Do: runs on first request, reads that request's configuration.
13161
+ * const stripe = lazyResource(env => new Stripe(env.STRIPE_SECRET_KEY!));
13162
+ * app.post("/", async (c) => { await stripe(c).charges.list(); });
13163
+ * ```
13164
+ *
13165
+ * The factory runs once per distinct environment object and the result is
13166
+ * cached against it. On Node that is once per process, because `process.env` is
13167
+ * one object for the life of the process — identical to the module-scope
13168
+ * version in cost, and unlike it, deferred until configuration exists. On a
13169
+ * host that hands each request a fresh binding object it is once per object,
13170
+ * which is the only correct answer there: caching across bindings would serve
13171
+ * one tenant's client to another.
13172
+ *
13173
+ * A {@link WeakMap} holds the cache, so an environment object the host has
13174
+ * finished with takes the resource with it.
13175
+ */
13176
+ function lazyResource(factory) {
13177
+ const cache = /* @__PURE__ */ new WeakMap();
13178
+ return (c) => {
13179
+ const bindings = getEnv(c);
13180
+ if (!bindings || typeof bindings !== "object") return factory(bindings ?? {});
13181
+ const existing = cache.get(bindings);
13182
+ if (existing !== void 0) return existing;
13183
+ const created = factory(bindings);
13184
+ cache.set(bindings, created);
13185
+ return created;
13186
+ };
13187
+ }
13188
+ //#endregion
11388
13189
  //#region src/cron/define-cron.ts
11389
13190
  /**
11390
13191
  * Typed authoring helper for a cron job file. Identity at runtime —
@@ -11469,6 +13270,130 @@ var authJwt = () => {
11469
13270
  return sql.raw(RLS_JWT_SQL);
11470
13271
  };
11471
13272
  //#endregion
13273
+ //#region src/dev-secrets.ts
13274
+ /**
13275
+ * Development secrets that survive a restart.
13276
+ *
13277
+ * `loadEnv` generates `JWT_SECRET` and `REBASE_SERVICE_KEY` when they are unset
13278
+ * outside production, so a developer can start without setting anything up. It
13279
+ * generated them **freshly on every boot**, which meant every restart of the dev
13280
+ * server silently invalidated the previous run's tokens: you were logged out of
13281
+ * your own app because you edited a file, and the API key you had just made
13282
+ * stopped working.
13283
+ *
13284
+ * The CLI already worked around it for scaffolded projects by writing a stable
13285
+ * `JWT_SECRET` into `.env` at `rebase init` — the comment there says exactly
13286
+ * why. That leaves anyone who did not scaffold, and anyone running from a
13287
+ * container with no `.env`, with the logout-on-restart behaviour.
13288
+ *
13289
+ * So the generated values are cached in a file beside the other dev-state
13290
+ * files — `.rebase-dev-port` and `.rebase-dev-url` follow the same convention
13291
+ * and are gitignored in the same place.
13292
+ *
13293
+ * ## What this does not change
13294
+ *
13295
+ * Production. `loadEnv` refuses to start when a secret was auto-generated and
13296
+ * `NODE_ENV=production`, and that is untouched: this only makes the
13297
+ * already-generated development values stable. There is no path by which a
13298
+ * cached secret becomes a production one.
13299
+ *
13300
+ * ## Failing soft
13301
+ *
13302
+ * A read-only filesystem, a container without a writable working directory, a
13303
+ * permissions problem — none of these should stop a development server from
13304
+ * starting. Every failure here falls back to the previous behaviour, an
13305
+ * ephemeral secret, and says so once at debug level.
13306
+ */
13307
+ /** Beside `.rebase-dev-port` and `.rebase-dev-url`, and gitignored with them. */
13308
+ var DEV_SECRETS_FILENAME = ".rebase-dev-secrets.json";
13309
+ /** Owner read/write only. These are secrets, even development ones. */
13310
+ var FILE_MODE = 384;
13311
+ /** Whether these environment variables describe a test runner. */
13312
+ function detectTestRunner(env) {
13313
+ return Boolean(env.JEST_WORKER_ID || env.VITEST || env.VITEST_WORKER_ID || env.NODE_ENV === "test");
13314
+ }
13315
+ /**
13316
+ * Whether a test runner started this process, decided **once at import**.
13317
+ *
13318
+ * Read at call time it would be wrong, and provably so: `env.test.ts` does
13319
+ * `process.env = {}` in its `beforeEach`, which erases `NODE_ENV` and
13320
+ * `JEST_WORKER_ID` alike. A predicate consulting the live environment therefore
13321
+ * sees a bare object and concludes "development", which is how the first version
13322
+ * of this dropped a `.rebase-dev-secrets.json` into `packages/server` on every
13323
+ * test run.
13324
+ *
13325
+ * Whether a test runner is hosting the process is a fact about the process, not
13326
+ * about whatever a test has since done to `process.env`, so it is captured
13327
+ * before any test can rewrite it.
13328
+ */
13329
+ var TEST_RUNNER_DETECTED = detectTestRunner(process.env);
13330
+ /**
13331
+ * Whether generated secrets should be cached at all.
13332
+ *
13333
+ * Not in production — `loadEnv` refuses to boot there on a generated secret, so
13334
+ * there is nothing to cache and no path by which a cached one becomes a
13335
+ * production one.
13336
+ *
13337
+ * Not under a test runner: a test must not inherit a secret a previous run left
13338
+ * behind, and a library call has no business writing into a suite's working
13339
+ * directory. `inTestRunner` is a parameter only so this predicate can be tested
13340
+ * on its own; callers pass nothing.
13341
+ */
13342
+ function shouldCacheDevSecrets(env = process.env, inTestRunner = TEST_RUNNER_DETECTED) {
13343
+ if (inTestRunner) return false;
13344
+ return env.NODE_ENV !== "production";
13345
+ }
13346
+ /** `REBASE_DEV_SECRETS_FILE`, or the conventional name in the working directory. */
13347
+ function resolveDevSecretsFile(env = process.env) {
13348
+ const override = env.REBASE_DEV_SECRETS_FILE?.trim();
13349
+ if (override) return path$3.resolve(override);
13350
+ return path$3.join(process.cwd(), DEV_SECRETS_FILENAME);
13351
+ }
13352
+ /**
13353
+ * Open the cache.
13354
+ *
13355
+ * Reading a malformed file is treated as an empty cache rather than an error:
13356
+ * the file is ours, it holds regenerable values, and refusing to boot over a
13357
+ * corrupted convenience cache would be worse than rewriting it.
13358
+ */
13359
+ function openDevSecretStore(file = resolveDevSecretsFile()) {
13360
+ let cache = {};
13361
+ let usable = true;
13362
+ try {
13363
+ if (fs$3.existsSync(file)) {
13364
+ const parsed = JSON.parse(fs$3.readFileSync(file, "utf-8"));
13365
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
13366
+ for (const [key, value] of Object.entries(parsed)) if (typeof value === "string" && value.length > 0) cache[key] = value;
13367
+ }
13368
+ }
13369
+ } catch {
13370
+ cache = {};
13371
+ }
13372
+ const flush = () => {
13373
+ try {
13374
+ fs$3.mkdirSync(path$3.dirname(file), { recursive: true });
13375
+ fs$3.writeFileSync(file, `${JSON.stringify(cache, null, 2)}\n`, { mode: FILE_MODE });
13376
+ fs$3.chmodSync(file, FILE_MODE);
13377
+ } catch (err) {
13378
+ if (usable) {
13379
+ usable = false;
13380
+ logger.debug(`[dev-secrets] Could not write ${file}, so generated development secrets will not survive a restart: ${err instanceof Error ? err.message : String(err)}`);
13381
+ }
13382
+ }
13383
+ };
13384
+ return {
13385
+ file,
13386
+ get usable() {
13387
+ return usable;
13388
+ },
13389
+ get: (name) => cache[name],
13390
+ set: (name, value) => {
13391
+ cache[name] = value;
13392
+ flush();
13393
+ }
13394
+ };
13395
+ }
13396
+ //#endregion
11472
13397
  //#region src/env.ts
11473
13398
  /**
11474
13399
  * Generate a cryptographically secure random secret (hex-encoded).
@@ -11586,14 +13511,24 @@ function loadEnv(options) {
11586
13511
  const isProduction = process.env.NODE_ENV === "production";
11587
13512
  const autoGeneratedSecrets = [];
11588
13513
  if (!isProduction) {
11589
- if (!process.env.JWT_SECRET) {
11590
- process.env.JWT_SECRET = generateSecret();
11591
- autoGeneratedSecrets.push("JWT_SECRET");
11592
- }
11593
- if (!process.env.REBASE_SERVICE_KEY) {
11594
- process.env.REBASE_SERVICE_KEY = generateSecret();
11595
- autoGeneratedSecrets.push("REBASE_SERVICE_KEY");
11596
- }
13514
+ const devSecrets = shouldCacheDevSecrets() ? openDevSecretStore() : void 0;
13515
+ const reused = [];
13516
+ const ensure = (name) => {
13517
+ if (process.env[name]) return;
13518
+ const cached = devSecrets?.get(name);
13519
+ if (cached) {
13520
+ process.env[name] = cached;
13521
+ reused.push(name);
13522
+ } else {
13523
+ const generated = generateSecret();
13524
+ process.env[name] = generated;
13525
+ devSecrets?.set(name, generated);
13526
+ }
13527
+ autoGeneratedSecrets.push(name);
13528
+ };
13529
+ ensure("JWT_SECRET");
13530
+ ensure("REBASE_SERVICE_KEY");
13531
+ if (autoGeneratedSecrets.length > 0 && reused.length !== autoGeneratedSecrets.length && devSecrets?.usable) logger.info(`Generated development ${autoGeneratedSecrets.length > 1 ? "secrets" : "secret"} (${autoGeneratedSecrets.join(", ")}) and cached them in ${DEV_SECRETS_FILENAME} so a restart does not log you out. Set them explicitly for anything that is not a development machine.`);
11597
13532
  }
11598
13533
  const env = (options?.extend ? rebaseEnvSchema.merge(options.extend) : rebaseEnvSchema).superRefine((data, ctx) => {
11599
13534
  const d = data;
@@ -12338,7 +14273,7 @@ function serveSPA(app, config) {
12338
14273
  const rawBase = config.basePath ?? "/";
12339
14274
  const basePath = rawBase !== "/" ? rawBase.replace(/\/+$/, "") : "/";
12340
14275
  const isRoot = basePath === "/";
12341
- if (!fs$2.existsSync(frontendPath)) {
14276
+ if (!fs$3.existsSync(frontendPath)) {
12342
14277
  logger.warn(`⚠️ Frontend build path does not exist: ${frontendPath}`);
12343
14278
  logger.warn(" SPA serving is disabled. Build your frontend first.");
12344
14279
  return;
@@ -12352,7 +14287,7 @@ function serveSPA(app, config) {
12352
14287
  });
12353
14288
  app.use(scope, responseCompression());
12354
14289
  app.use(scope, serveStatic({
12355
- root: path$2.relative(process.cwd(), frontendPath),
14290
+ root: path$3.relative(process.cwd(), frontendPath),
12356
14291
  precompressed: true,
12357
14292
  ...isRoot ? {} : { rewriteRequestPath: (p) => p.slice(basePath.length) || "/" }
12358
14293
  }));
@@ -12365,7 +14300,7 @@ function serveSPA(app, config) {
12365
14300
  app.get(scope, async (c, next) => {
12366
14301
  if (allExcludePaths.some((p) => isUnderPath(c.req.path, p))) return next();
12367
14302
  if (isAssetRequest(c.req.path, c.req.header("sec-fetch-dest"))) return c.text("Not found", 404);
12368
- const indexPath = path$2.join(frontendPath, indexFile);
14303
+ const indexPath = path$3.join(frontendPath, indexFile);
12369
14304
  if (!cachedHtml) try {
12370
14305
  cachedHtml = await fsp.readFile(indexPath, "utf-8");
12371
14306
  } catch {
@@ -12774,6 +14709,18 @@ var bootEnvExtension = object({
12774
14709
  "false",
12775
14710
  ""
12776
14711
  ]).default("false").transform((v) => v === "true"),
14712
+ /**
14713
+ * Bot protection. Enabled by setting both — a provider without a secret
14714
+ * cannot verify anything, and is refused at boot rather than ignored.
14715
+ */
14716
+ CAPTCHA_PROVIDER: _enum([
14717
+ "turnstile",
14718
+ "hcaptcha",
14719
+ ""
14720
+ ]).optional(),
14721
+ CAPTCHA_SECRET: string().optional(),
14722
+ /** Comma-separated: register, login, forgotPassword, magicLink. */
14723
+ CAPTCHA_ROUTES: string().optional(),
12777
14724
  AUTH_COOKIE_SAME_SITE: _enum([
12778
14725
  "Strict",
12779
14726
  "Lax",
@@ -12874,6 +14821,19 @@ var bootEnvExtension = object({
12874
14821
  ""
12875
14822
  ]).optional().transform((v) => v === void 0 || v === "" ? void 0 : v === "true"),
12876
14823
  /**
14824
+ * Override whether this process runs the scheduled RLS audit. Unset follows
14825
+ * the role.
14826
+ *
14827
+ * Unlike cron and the job workers this is not claim-protected — every owner
14828
+ * scans on its own timer, which is redundant rather than unsafe — so a split
14829
+ * deployment sets it false everywhere but one process.
14830
+ */
14831
+ REBASE_RLS_AUDIT: _enum([
14832
+ "true",
14833
+ "false",
14834
+ ""
14835
+ ]).optional().transform((v) => v === void 0 || v === "" ? void 0 : v === "true"),
14836
+ /**
12877
14837
  * Comma-separated function names this process serves. Unset means all.
12878
14838
  *
12879
14839
  * `functions` role only. A name that is not in the bundle is a boot failure,
@@ -13023,7 +14983,8 @@ var ROLES = {
13023
14983
  },
13024
14984
  ownership: {
13025
14985
  cronScheduler: false,
13026
- jobWorkers: false
14986
+ jobWorkers: false,
14987
+ rlsAudit: false
13027
14988
  },
13028
14989
  provisionSchema: false
13029
14990
  },
@@ -13067,6 +15028,7 @@ function resolveRole(env) {
13067
15028
  const ownership = { ...base.ownership };
13068
15029
  if (env.REBASE_CRON_SCHEDULER !== void 0) ownership.cronScheduler = env.REBASE_CRON_SCHEDULER;
13069
15030
  if (env.REBASE_JOB_WORKERS !== void 0) ownership.jobWorkers = env.REBASE_JOB_WORKERS;
15031
+ if (env.REBASE_RLS_AUDIT !== void 0) ownership.rlsAudit = env.REBASE_RLS_AUDIT;
13070
15032
  return {
13071
15033
  role,
13072
15034
  surfaces: { ...base.surfaces },
@@ -13504,13 +15466,51 @@ async function initializeDataSources(sources, schema, resolveFrom = []) {
13504
15466
  //#endregion
13505
15467
  //#region src/boot/options.ts
13506
15468
  /**
13507
- * Build the email configuration, or `undefined` when no SMTP host is set.
15469
+ * True when an emailed link would be followable — i.e. when there is an
15470
+ * absolute base to build one from. Mirrors `assertEmailLinkBases`, which
15471
+ * refuses the boot otherwise.
15472
+ */
15473
+ function hasAbsoluteLinkBase(value) {
15474
+ if (!value) return false;
15475
+ try {
15476
+ const url = new URL(value);
15477
+ return url.protocol === "http:" || url.protocol === "https:";
15478
+ } catch {
15479
+ return false;
15480
+ }
15481
+ }
15482
+ /**
15483
+ * Build the email configuration.
13508
15484
  *
13509
- * Without it the auth adapter still works; password-reset and verification mail
13510
- * simply has nowhere to go, which the auth routes report for themselves.
15485
+ * With `SMTP_HOST`, that is a real SMTP transport. Without it, outside
15486
+ * production, it is the development sink: mail is captured and its links are
15487
+ * printed, so `POST /auth/magic-link` and `POST /auth/forgot-password` complete
15488
+ * on a fresh project instead of answering `503 EMAIL_NOT_CONFIGURED`. The token
15489
+ * was always minted and valid; only delivery was missing.
15490
+ *
15491
+ * Three conditions, all required, none configurable:
15492
+ *
15493
+ * - no `SMTP_HOST` — a configured mail server always wins;
15494
+ * - `NODE_ENV` is not `production` — a captured reset mail carries a working
15495
+ * token, so this must not be reachable there (see `dev-sink.ts`);
15496
+ * - an absolute `FRONTEND_URL`, or the emailed link has no base and is dead on
15497
+ * arrival — which is the condition `assertEmailLinkBases` fails the boot on.
15498
+ *
15499
+ * When any of those does not hold this returns `undefined`, exactly as before,
15500
+ * and the auth routes report the missing service for themselves.
13511
15501
  */
13512
15502
  function resolveEmailOptions(env) {
13513
- if (!env.SMTP_HOST) return void 0;
15503
+ if (!env.SMTP_HOST) {
15504
+ if (env.NODE_ENV === "production") return void 0;
15505
+ if (!hasAbsoluteLinkBase(env.FRONTEND_URL)) return void 0;
15506
+ return {
15507
+ from: env.SMTP_FROM || `${env.APP_NAME} <noreply@rebase.pro>`,
15508
+ sendEmail: createDevEmailSink().sendEmail,
15509
+ appName: env.APP_NAME,
15510
+ logoUrl: env.EMAIL_LOGO_URL,
15511
+ resetPasswordUrl: env.FRONTEND_URL
15512
+ };
15513
+ }
13514
15514
  return {
13515
15515
  from: env.SMTP_FROM || `${env.APP_NAME} <noreply@rebase.pro>`,
13516
15516
  smtp: {
@@ -13529,6 +15529,24 @@ function resolveEmailOptions(env) {
13529
15529
  };
13530
15530
  }
13531
15531
  /**
15532
+ * Bot protection from the environment, or `undefined` when it is not configured.
15533
+ *
15534
+ * Both halves are required together. A provider with no secret cannot verify
15535
+ * anything, so it is left off here and `resolveCaptchaVerifier` refuses the boot
15536
+ * if something else turns it on — the one failure this feature must not have is
15537
+ * being silently absent while the config says otherwise.
15538
+ */
15539
+ function resolveCaptchaOptions(env) {
15540
+ if (!env.CAPTCHA_PROVIDER || !env.CAPTCHA_SECRET) return void 0;
15541
+ const routes = env.CAPTCHA_ROUTES?.split(",").map((part) => part.trim()).filter(Boolean);
15542
+ return {
15543
+ enabled: true,
15544
+ provider: env.CAPTCHA_PROVIDER,
15545
+ secret: env.CAPTCHA_SECRET,
15546
+ ...routes?.length ? { routes } : {}
15547
+ };
15548
+ }
15549
+ /**
13532
15550
  * Build the auth configuration from the environment and the bundle's users
13533
15551
  * collection.
13534
15552
  *
@@ -13552,6 +15570,7 @@ function resolveAuthOptions(env, usersCollection) {
13552
15570
  disableSelfRegistration: env.DISABLE_SELF_REGISTRATION,
13553
15571
  allowAnonymous: env.ALLOW_ANONYMOUS,
13554
15572
  allowUserLookup: env.AUTH_ALLOW_USER_LOOKUP,
15573
+ captcha: resolveCaptchaOptions(env),
13555
15574
  email: resolveEmailOptions(env),
13556
15575
  cookieAuth: { sameSite: env.AUTH_COOKIE_SAME_SITE || "Lax" }
13557
15576
  };
@@ -14322,6 +16341,6 @@ function warnOnUnusableBundleShape(bundle) {
14322
16341
  logger.warn(`This bundle declares a config package at "${bundle.manifest.entry.config}", but no collections directory resolved inside it, so no collections were loaded and no tables will be created. Rebuild with \`rebase build\` and check the manifest's \`entry.collections\`.`);
14323
16342
  }
14324
16343
  //#endregion
14325
- export { ApiError, BlockedUrlError, BundleError, CONCURRENT_DDL_SQLSTATES, CronScheduler, DEFAULT_DRIVER_ID, DEFAULT_MAX_FILE_SIZE, DEFAULT_STORAGE_ID, DOCUMENT_MIME_TYPES, DefaultDriverRegistry, DefaultStorageRegistry, GCSStorageController, IMAGE_MIME_TYPES, LocalStorageController, MetricsRegistry, RawHtml, S3StorageController, SMTPEmailService, TransformCache, TusHandler, UnknownStorageSourceError, WEBHOOK_DELIVERY_TASK, WebhookDispatcher, _resetRebaseMock, _setRebaseMock, applyCollectionDefaults, assertAllowedOutboundUrl, assertCollectionConfigs, assertDistinctSuffixes, assertEmailLinkBases, assertWriteRequestValid, authJwt, authRoles, authUid, blockedAddressReason, bootFromBundle, classifySurface, cleanupDevPortFile, createAppleProvider, createBackupRoutes, createBitbucketProvider, createBuiltinAuthAdapter, createContractRoutes, createCronRoutes, createCronStore, createCustomAuthAdapter, createDdlBootstrapper, createDiscordProvider, createEmailService, createFacebookProvider, createFunctionRoutes, createGitHubProvider, createGitLabProvider, createGoogleProvider, createHistoryRoutes, createJobQueue, createJobStore, createJwksRoutes, createLinkedinProvider, createMetricsMiddleware, createMetricsRoutes, createMicrosoftProvider, createSlackProvider, createSourceBundle, createSpotifyProvider, createStorageController, createStorageRoutes, createTwitterProvider, defaultBackoff, defineCron, defineFunction, envSuffixForKey, errorHandler, escapeHtml, extractUserFromToken, fileTokenAuth, findCollectionConfigProblems, generateSecurePassword, getEmailVerificationTemplate, getJwks, getMagicLinkTemplate, getPasswordResetTemplate, getUserInvitationTemplate, getWelcomeEmailTemplate, hasAsymmetricSigningKey, hashPassword, html, httpMethodToOperation, initializeDataSource, initializeDataSources, initializeRebaseBackend, installShutdownHandlers, isApiKeyToken, isAuthAdapter, isConcurrentDdlRace, isDatabaseAdapter, isDuplicateObjectRace, isLocalhostOrigin, isOperationAllowed, isTransformableImage, listBackupObjects, listenWithPortRetry, loadBootEnv, loadBundle, loadBundleConfigExports, loadBundleSchema, loadCollectionsFromDirectory, loadCronJobsFromDirectory, loadCronJobsWithDiagnostics, loadDeclaredStorageSources, loadEnv, loadFunctionsFromDirectory, loadUsersCollection, logger, oauthCodeFlowSchema, optionalAuth, parseBackupDestination, parseBackupTimestamp, parseTransformOptions, pkceTokenParams, providerVerifiedEmail, queryTokenAuth, raw, readBackupBytes, readBundleManifest, rebase, requireAdmin, requireAuth, resolveAuthHooks, resolveAuthOptions, resolveCorsOrigin, resolveDataSources, resolveEmailBranding, resolveEmailLinkBase, resolveEmailOptions, resolveRequireAuth, resolveStorageBackend, resolveStorageSources, runFromBundle, safeCompare, serveSPA, transformImage, tryVerifyOidcIdToken, unknownKeyPolicyFromEnv, validateApiKey, validateCronExpression, validatePasswordStrength, verifyOidcIdToken, verifyPassword };
16344
+ export { ApiError, BlockedUrlError, BundleError, CONCURRENT_DDL_SQLSTATES, CronScheduler, DEFAULT_DRIVER_ID, DEFAULT_MAX_FILE_SIZE, DEFAULT_STORAGE_ID, DOCUMENT_MIME_TYPES, DefaultDriverRegistry, DefaultStorageRegistry, GCSStorageController, IMAGE_MIME_TYPES, LocalStorageController, MetricsRegistry, RawHtml, S3StorageController, SMTPEmailService, StoragePolicyError, TransformCache, TusHandler, UnknownStorageSourceError, WEBHOOK_DELIVERY_TASK, WebhookDispatcher, _resetRebaseMock, _setRebaseMock, applyCollectionDefaults, assertAllowedOutboundUrl, assertCollectionConfigs, assertDistinctSuffixes, assertEmailLinkBases, assertWriteRequestValid, authJwt, authRoles, authUid, blockedAddressReason, bootFromBundle, classifySurface, cleanupDevPortFile, compileStoragePolicies, createAppleProvider, createBackupRoutes, createBitbucketProvider, createBuiltinAuthAdapter, createContractRoutes, createCronRoutes, createCronStore, createCustomAuthAdapter, createDdlBootstrapper, createDevEmailSink, createDiscordProvider, createEmailService, createFacebookProvider, createFunctionRoutes, createGitHubProvider, createGitLabProvider, createGoogleProvider, createHistoryRoutes, createJobQueue, createJobStore, createJwksRoutes, createLinkedinProvider, createMetricsMiddleware, createMetricsRoutes, createMicrosoftProvider, createRlsAudit, createSlackProvider, createSourceBundle, createSpotifyProvider, createStorageController, createStorageRoutes, createTwitterProvider, defaultBackoff, defineCron, defineFunction, drainBackgroundWork, envSuffixForKey, errorHandler, escapeHtml, extractLinks, extractUserFromToken, fileTokenAuth, findCollectionConfigProblems, generateSecurePassword, getApiKey, getDriver, getEmailVerificationTemplate, getEnv, getJwks, getMagicLinkTemplate, getPasswordResetTemplate, getRequestId, getRoles, getUser, getUserId, getUserInvitationTemplate, getWelcomeEmailTemplate, hasAsymmetricSigningKey, hasRole, hashPassword, html, httpMethodToOperation, identityResolved, initializeDataSource, initializeDataSources, initializeRebaseBackend, installShutdownHandlers, isAdmin, isApiKeyToken, isAuthAdapter, isAuthenticated, isConcurrentDdlRace, isDatabaseAdapter, isDuplicateObjectRace, isLocalhostOrigin, isNodeRuntime, isOperationAllowed, isTransformableImage, lazyResource, listBackupObjects, listenWithPortRetry, loadBootEnv, loadBundle, loadBundleConfigExports, loadBundleSchema, loadCollectionsFromDirectory, loadCronJobsFromDirectory, loadCronJobsWithDiagnostics, loadDeclaredStorageSources, loadEnv, loadFunctionsFromDirectory, loadFunctionsWithDiagnostics, loadUsersCollection, logger, oauthCodeFlowSchema, optionalAuth, parseBackupDestination, parseBackupTimestamp, parseTransformOptions, pendingBackgroundWork, pkceTokenParams, providerVerifiedEmail, queryTokenAuth, raw, readBackupBytes, readBundleManifest, rebase, requireAdmin, requireAuth, requireDriver, requireEnv, requireRole, resolveAuthHooks, resolveAuthOptions, resolveCorsOrigin, resolveDataSources, resolveEmailBranding, resolveEmailLinkBase, resolveEmailOptions, resolveRequireAuth, resolveStorageAccessControl, resolveStorageBackend, resolveStorageSources, runFromBundle, runtimeKey, safeCompare, serveSPA, summarize as summarizeRlsScan, transformImage, tryVerifyOidcIdToken, unknownKeyPolicyFromEnv, validateApiKey, validateCronExpression, validatePasswordStrength, verifyOidcIdToken, verifyPassword, waitUntil };
14326
16345
 
14327
16346
  //# sourceMappingURL=index.es.js.map