@rebasepro/server 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/README.md +1 -1
  2. package/bin/rebase-server.js +4 -2
  3. package/dist/{GCSStorageController-CjrA4PMo.js → GCSStorageController-BSiP1c-f.js} +22 -8
  4. package/dist/GCSStorageController-BSiP1c-f.js.map +1 -0
  5. package/dist/{S3StorageController-B6pKDNVj.js → S3StorageController-CAwFRgjV.js} +19 -7
  6. package/dist/S3StorageController-CAwFRgjV.js.map +1 -0
  7. package/dist/api/ast-schema-editor.d.ts +92 -1
  8. package/dist/api/errors.d.ts +9 -0
  9. package/dist/api/live-schema-routes.d.ts +38 -8
  10. package/dist/api/logs-routes.d.ts +39 -1
  11. package/dist/api/openapi-generator.d.ts +17 -0
  12. package/dist/api/rest/api-generator.d.ts +44 -10
  13. package/dist/api/rest/write-validation.d.ts +2 -2
  14. package/dist/api/types.d.ts +17 -1
  15. package/dist/{ast-schema-editor-Mvr50v_S.js → ast-schema-editor-CWqS_sLJ.js} +309 -11
  16. package/dist/ast-schema-editor-CWqS_sLJ.js.map +1 -0
  17. package/dist/auth/access.d.ts +105 -0
  18. package/dist/auth/adapter-middleware.d.ts +2 -1
  19. package/dist/auth/address-ownership.d.ts +16 -1
  20. package/dist/auth/admin-roles-route.d.ts +4 -2
  21. package/dist/auth/admin-roles.d.ts +17 -20
  22. package/dist/auth/admin-users-route.d.ts +1 -0
  23. package/dist/auth/api-keys/api-key-middleware.d.ts +56 -55
  24. package/dist/auth/api-keys/api-key-routes.d.ts +41 -11
  25. package/dist/auth/api-keys/api-key-store.d.ts +31 -8
  26. package/dist/auth/api-keys/api-key-types.d.ts +14 -16
  27. package/dist/auth/api-keys/http-operation.d.ts +19 -0
  28. package/dist/auth/api-keys/index.d.ts +11 -11
  29. package/dist/auth/api-keys/key-grant.d.ts +41 -0
  30. package/dist/auth/api-keys/legacy-permissions.d.ts +33 -0
  31. package/dist/auth/auth-hooks.d.ts +46 -7
  32. package/dist/auth/builtin-auth-adapter.d.ts +8 -0
  33. package/dist/auth/cookie-utils.d.ts +7 -0
  34. package/dist/auth/deliverable-address.d.ts +6 -0
  35. package/dist/auth/email-change-routes.d.ts +41 -0
  36. package/dist/auth/expired-token-sweep.d.ts +67 -0
  37. package/dist/auth/impersonation.d.ts +110 -0
  38. package/dist/auth/index.d.ts +4 -2
  39. package/dist/auth/interfaces.d.ts +110 -59
  40. package/dist/auth/jwt.d.ts +49 -3
  41. package/dist/auth/magic-link-routes.d.ts +2 -6
  42. package/dist/auth/mfa-routes.d.ts +2 -9
  43. package/dist/auth/middleware.d.ts +17 -5
  44. package/dist/auth/otp-routes.d.ts +2 -6
  45. package/dist/auth/passwordless-signup.d.ts +27 -0
  46. package/dist/auth/platform-token.d.ts +122 -0
  47. package/dist/auth/rate-limiter.d.ts +41 -0
  48. package/dist/auth/routes.d.ts +45 -0
  49. package/dist/auth/scope-routes.d.ts +22 -0
  50. package/dist/auth/session-routes.d.ts +11 -6
  51. package/dist/auth/token-revocation.d.ts +50 -1
  52. package/dist/auth/verify-credential.d.ts +28 -0
  53. package/dist/{auth-B-GIMpDG.js → auth-DMLngxn_.js} +2159 -569
  54. package/dist/auth-DMLngxn_.js.map +1 -0
  55. package/dist/backend-DTAOsLQc.js.map +1 -1
  56. package/dist/backup/backup-common.d.ts +10 -0
  57. package/dist/backup/backup-routes.d.ts +24 -4
  58. package/dist/backup/backup-schedule.d.ts +33 -0
  59. package/dist/backup/backup-storage.d.ts +14 -0
  60. package/dist/backup/index.d.ts +2 -0
  61. package/dist/backup-CN0s50D2.js +444 -0
  62. package/dist/backup-CN0s50D2.js.map +1 -0
  63. package/dist/boot/bundle.d.ts +19 -0
  64. package/dist/boot/env.d.ts +49 -4
  65. package/dist/boot/security-headers.d.ts +26 -0
  66. package/dist/boot/static-routing.d.ts +56 -0
  67. package/dist/collection_patch-BRu-BvDv.js +472 -0
  68. package/dist/collection_patch-BRu-BvDv.js.map +1 -0
  69. package/dist/{contract-routes-CbFjuBwa.js → contract-routes-fz8i4pxs.js} +17 -4
  70. package/dist/contract-routes-fz8i4pxs.js.map +1 -0
  71. package/dist/cron/cron-scheduler.d.ts +25 -20
  72. package/dist/cron/cron-store.d.ts +6 -2
  73. package/dist/{cron-loader-DfTj2Hbi.js → cron-loader-CwaANlOG.js} +4 -4
  74. package/dist/cron-loader-CwaANlOG.js.map +1 -0
  75. package/dist/{cron-routes-eE8nif_b.js → cron-routes-Bc-SB0Se.js} +10 -7
  76. package/dist/cron-routes-Bc-SB0Se.js.map +1 -0
  77. package/dist/{cron-scheduler-B0pLfAix.js → cron-scheduler-CYQgco86.js} +52 -34
  78. package/dist/cron-scheduler-CYQgco86.js.map +1 -0
  79. package/dist/{cron-store-TcoGz-xS.js → cron-store-D2Q9-Aco.js} +10 -15
  80. package/dist/cron-store-D2Q9-Aco.js.map +1 -0
  81. package/dist/{ddl-bootstrap-C6mo0Kmz.js → ddl-bootstrap-BaqMSa4Y.js} +2 -2
  82. package/dist/{ddl-bootstrap-C6mo0Kmz.js.map → ddl-bootstrap-BaqMSa4Y.js.map} +1 -1
  83. package/dist/email/index.d.ts +2 -2
  84. package/dist/email/templates.d.ts +22 -0
  85. package/dist/email/types.d.ts +26 -0
  86. package/dist/env.d.ts +1 -2
  87. package/dist/{errors-DWsX4yTd.js → errors-D6_y86c5.js} +102 -8
  88. package/dist/errors-D6_y86c5.js.map +1 -0
  89. package/dist/{function-loader-xnbDAPfa.js → function-loader-D7o5Epjj.js} +2 -2
  90. package/dist/{function-loader-xnbDAPfa.js.map → function-loader-D7o5Epjj.js.map} +1 -1
  91. package/dist/{function-routes-Chet4-lB.js → function-routes-CaNG4waN.js} +24 -12
  92. package/dist/function-routes-CaNG4waN.js.map +1 -0
  93. package/dist/functions/context.d.ts +17 -6
  94. package/dist/functions/guards.d.ts +22 -5
  95. package/dist/functions/index.d.ts +2 -2
  96. package/dist/functions/index.js +90 -36
  97. package/dist/functions/index.js.map +1 -1
  98. package/dist/{history-recorder-B4MpJfJK.js → history-recorder-Nr8zLvoU.js} +4 -4
  99. package/dist/{history-recorder-B4MpJfJK.js.map → history-recorder-Nr8zLvoU.js.map} +1 -1
  100. package/dist/{history-store-BhxWOuz9.js → history-store-rcAm_xFR.js} +2 -2
  101. package/dist/{history-store-BhxWOuz9.js.map → history-store-rcAm_xFR.js.map} +1 -1
  102. package/dist/index.d.ts +8 -2
  103. package/dist/index.es.js +3084 -753
  104. package/dist/index.es.js.map +1 -1
  105. package/dist/init/health.d.ts +17 -2
  106. package/dist/init/shutdown.d.ts +10 -0
  107. package/dist/init.d.ts +54 -0
  108. package/dist/{jobs-CazMYhyy.js → jobs-DqYNfquG.js} +5 -5
  109. package/dist/{jobs-CazMYhyy.js.map → jobs-DqYNfquG.js.map} +1 -1
  110. package/dist/{jwt-DnQHNFCl.js → jwt-R6bSPMjk.js} +39 -15
  111. package/dist/{jwt-DnQHNFCl.js.map → jwt-R6bSPMjk.js.map} +1 -1
  112. package/dist/{keys-CogCQpxG.js → keys-GAVZqbqx.js} +3 -17
  113. package/dist/{keys-CogCQpxG.js.map → keys-GAVZqbqx.js.map} +1 -1
  114. package/dist/{logger-DO2PZc4i.js → logger-D-S-hO5e.js} +26 -3
  115. package/dist/logger-D-S-hO5e.js.map +1 -0
  116. package/dist/{logs-routes-Bj4TYYUl.js → logs-routes-DAdv37GI.js} +48 -8
  117. package/dist/logs-routes-DAdv37GI.js.map +1 -0
  118. package/dist/mcp/consent-page.d.ts +1 -1
  119. package/dist/mcp/mcp-routes.d.ts +7 -0
  120. package/dist/mcp/mcp-tools.d.ts +15 -9
  121. package/dist/mcp/oauth-metadata.d.ts +21 -16
  122. package/dist/mcp/oauth-routes.d.ts +7 -1
  123. package/dist/{openapi-generator-O_O24MAT.js → openapi-generator-DAq_XVDu.js} +104 -13
  124. package/dist/openapi-generator-DAq_XVDu.js.map +1 -0
  125. package/dist/{proxy-Czngl3p9.js → proxy-qRlqeUmO.js} +2 -2
  126. package/dist/{proxy-Czngl3p9.js.map → proxy-qRlqeUmO.js.map} +1 -1
  127. package/dist/{query-parser-DGRVFNM3.js → query-parser-BgiKJKvc.js} +6 -56
  128. package/dist/query-parser-BgiKJKvc.js.map +1 -0
  129. package/dist/{request-timeout-C_4C2BeR.js → request-timeout-DgH7j8qO.js} +3 -3
  130. package/dist/{request-timeout-C_4C2BeR.js.map → request-timeout-DgH7j8qO.js.map} +1 -1
  131. package/dist/schema-edit/apply-schema-change.d.ts +63 -3
  132. package/dist/schema-edit/project-root.d.ts +3 -2
  133. package/dist/schema-edit/remote-source.d.ts +9 -4
  134. package/dist/{schema-editor-routes-C5-lh_jO.js → schema-editor-routes-oIyuWl3L.js} +12 -7
  135. package/dist/schema-editor-routes-oIyuWl3L.js.map +1 -0
  136. package/dist/serve-spa.d.ts +58 -0
  137. package/dist/services/routed-realtime-service.d.ts +11 -0
  138. package/dist/soft-delete-params-BWPilMPF.js +59 -0
  139. package/dist/soft-delete-params-BWPilMPF.js.map +1 -0
  140. package/dist/{src-vkcwKXbT.js → src-CatHFUym.js} +439 -20
  141. package/dist/src-CatHFUym.js.map +1 -0
  142. package/dist/{src-pmvW7BFx.js → src-I3aG1PcY.js} +252 -70
  143. package/dist/src-I3aG1PcY.js.map +1 -0
  144. package/dist/storage/GCSStorageController.d.ts +2 -0
  145. package/dist/storage/LocalStorageController.d.ts +2 -0
  146. package/dist/storage/S3StorageController.d.ts +2 -0
  147. package/dist/storage/index.d.ts +2 -2
  148. package/dist/storage/property-limits.d.ts +41 -6
  149. package/dist/storage/request-keys.d.ts +15 -0
  150. package/dist/storage/requested-object.d.ts +74 -0
  151. package/dist/storage/routes.d.ts +36 -18
  152. package/dist/storage/tus-handler.d.ts +30 -5
  153. package/dist/storage/types.d.ts +19 -0
  154. package/dist/types-BfKcm9do.js.map +1 -1
  155. package/dist/utils/logger.d.ts +12 -0
  156. package/package.json +5 -5
  157. package/dist/GCSStorageController-CjrA4PMo.js.map +0 -1
  158. package/dist/S3StorageController-B6pKDNVj.js.map +0 -1
  159. package/dist/admin-roles-vYdp_Pil.js +0 -36
  160. package/dist/admin-roles-vYdp_Pil.js.map +0 -1
  161. package/dist/admin_block-DxKLmdiv.js +0 -206
  162. package/dist/admin_block-DxKLmdiv.js.map +0 -1
  163. package/dist/ast-schema-editor-Mvr50v_S.js.map +0 -1
  164. package/dist/auth/api-keys/api-key-permission-guard.d.ts +0 -65
  165. package/dist/auth-B-GIMpDG.js.map +0 -1
  166. package/dist/backup-D7YR94N3.js +0 -253
  167. package/dist/backup-D7YR94N3.js.map +0 -1
  168. package/dist/contract-routes-CbFjuBwa.js.map +0 -1
  169. package/dist/cron-loader-DfTj2Hbi.js.map +0 -1
  170. package/dist/cron-routes-eE8nif_b.js.map +0 -1
  171. package/dist/cron-scheduler-B0pLfAix.js.map +0 -1
  172. package/dist/cron-store-TcoGz-xS.js.map +0 -1
  173. package/dist/errors-DWsX4yTd.js.map +0 -1
  174. package/dist/function-routes-Chet4-lB.js.map +0 -1
  175. package/dist/logger-DO2PZc4i.js.map +0 -1
  176. package/dist/logs-routes-Bj4TYYUl.js.map +0 -1
  177. package/dist/openapi-generator-O_O24MAT.js.map +0 -1
  178. package/dist/query-parser-DGRVFNM3.js.map +0 -1
  179. package/dist/schema-editor-routes-C5-lh_jO.js.map +0 -1
  180. package/dist/src-pmvW7BFx.js.map +0 -1
  181. package/dist/src-vkcwKXbT.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"backend-DTAOsLQc.js","names":[],"sources":["../../types/src/types/backend.ts"],"sourcesContent":["import type { CollectionConfig, FilterValues, WhereFilterOp } from \"./collections\";\nimport type { CollectionCallbacks } from \"./entity_callbacks\";\nimport type { OrderByTuple } from \"./filter-operators\";\nimport type { LogicalCondition } from \"../controllers/data\";\nimport type { AuthAdapter } from \"./auth_adapter\";\nimport type { HistoryConfig } from \"../controllers/client\";\nimport type { ChannelBusSetting } from \"./channel_bus\";\nimport type { SchemaEditingAdmin } from \"./schema_editing\";\n\n// =============================================================================\n// DATABASE CONNECTION INTERFACES\n// =============================================================================\n\n/**\n * Abstract database connection interface.\n * Represents a connection to any database system.\n */\nexport interface DatabaseConnection {\n /**\n * Type identifier for this database (e.g., 'postgres', 'mongodb', 'mysql')\n */\n readonly type: string;\n\n /**\n * Whether the connection is currently active\n */\n readonly isConnected?: boolean;\n\n /**\n * Close the database connection and release resources.\n */\n close?(): Promise<void>;\n}\n\n// =============================================================================\n// QUERY BUILDING INTERFACES\n// =============================================================================\n\n/**\n * A single filter condition for database queries\n */\nexport interface QueryFilter {\n field: string;\n operator: WhereFilterOp;\n value: unknown;\n}\n\n/**\n * Options for fetching a collection of entities\n */\nexport interface FetchCollectionOptions<M extends Record<string, unknown> = Record<string, unknown>> {\n filter?: FilterValues<Extract<keyof M, string>>;\n /** See `FetchCollectionProps.orderBy`: a field name plus `order`, or a list of tuples. */\n orderBy?: string | OrderByTuple[];\n order?: \"desc\" | \"asc\";\n limit?: number;\n offset?: number;\n startAfter?: unknown;\n searchString?: string;\n databaseId?: string;\n collection?: CollectionConfig;\n}\n\n/**\n * Options for searching entities\n */\nexport interface SearchOptions<M extends Record<string, unknown> = Record<string, unknown>> {\n filter?: FilterValues<Extract<keyof M, string>>;\n /** See `FetchCollectionProps.orderBy`: a field name plus `order`, or a list of tuples. */\n orderBy?: string | OrderByTuple[];\n order?: \"desc\" | \"asc\";\n limit?: number;\n databaseId?: string;\n collection?: CollectionConfig;\n}\n\n/**\n * Options for counting entities\n */\nexport interface CountOptions<M extends Record<string, unknown> = Record<string, unknown>> {\n filter?: FilterValues<Extract<keyof M, string>>;\n /**\n * An `or(...)`/`and(...)` group, alongside `filter`.\n *\n * Counted as well as fetched, or `total` describes a different set of rows\n * from the one that was served — the same reason `filter` is here.\n */\n logical?: LogicalCondition;\n searchString?: string;\n databaseId?: string;\n}\n\n/**\n * Abstract condition builder interface.\n * Implementations translate Rebase filter conditions to database-specific queries.\n *\n * Note: This interface can be implemented as instance methods or as a class with static methods.\n * For static implementations (like DrizzleConditionBuilder), use the ConditionBuilderStatic type.\n *\n * @template T The type of condition returned by the builder (e.g., SQL for PostgreSQL, Filter<Document> for MongoDB)\n */\nexport interface ConditionBuilder<T = unknown> {\n /**\n * Build filter conditions from Rebase FilterValues\n */\n buildFilterConditions<M extends Record<string, unknown>>(\n filter: FilterValues<Extract<keyof M, string>>,\n collectionPath: string,\n ...args: unknown[]\n ): T[];\n\n /**\n * Build search conditions for text search.\n *\n * At most one condition comes back, already complete: callers OR what they\n * get, and OR is right across the searchable *fields* but wrong across the\n * *terms* of the search string — a person typing two words means both, and\n * the two halves of a name are two different fields. The empty array still\n * means \"nothing here can be searched\", which every caller reads as \"no\n * rows\".\n */\n buildSearchConditions(\n searchString: string,\n properties: Record<string, unknown>,\n ...args: unknown[]\n ): T[];\n\n /**\n * Combine multiple conditions with AND operator\n */\n combineConditionsWithAnd(conditions: T[]): T | undefined;\n\n /**\n * Combine multiple conditions with OR operator\n */\n combineConditionsWithOr(conditions: T[]): T | undefined;\n}\n\n/**\n * Static condition builder type for implementations using static methods.\n * Use this type when the class provides static methods rather than instance methods.\n *\n * @example\n * // DrizzleConditionBuilder satisfies this type\n * const builder: ConditionBuilderStatic<SQL> = DrizzleConditionBuilder;\n */\nexport type ConditionBuilderStatic<T = unknown> = {\n buildFilterConditions<M extends Record<string, unknown>>(\n filter: FilterValues<Extract<keyof M, string>>,\n ...args: unknown[]\n ): T[];\n buildSearchConditions(\n searchString: string,\n properties: Record<string, unknown>,\n ...args: unknown[]\n ): T[];\n combineConditionsWithAnd(conditions: T[]): T | undefined;\n combineConditionsWithOr(conditions: T[]): T | undefined;\n};\n\n// =============================================================================\n// ENTITY REPOSITORY INTERFACES\n// =============================================================================\n\n/**\n * Abstract entity repository interface.\n * Handles all CRUD operations for entities in the database.\n *\n * Implementations should handle:\n * - Entity serialization/deserialization\n * - Relation resolution\n * - ID generation and conversion\n */\nexport interface DataRepository {\n /**\n * Fetch a single entity by ID\n */\n fetchOne<M extends Record<string, unknown>>(\n collectionPath: string,\n id: string | number,\n databaseId?: string\n ): Promise<Record<string, unknown> | undefined>;\n\n /**\n * Fetch a collection of entities with optional filtering, ordering, and pagination\n */\n fetchCollection<M extends Record<string, unknown>>(\n collectionPath: string,\n options?: FetchCollectionOptions<M>\n ): Promise<Record<string, unknown>[]>;\n\n /**\n * Search entities by text\n */\n searchRows<M extends Record<string, unknown>>(\n collectionPath: string,\n searchString: string,\n options?: SearchOptions<M>\n ): Promise<Record<string, unknown>[]>;\n\n /**\n * Count entities in a collection\n */\n count<M extends Record<string, unknown>>(\n collectionPath: string,\n options?: CountOptions<M>\n ): Promise<number>;\n\n /**\n * Save a entity (create or update)\n */\n save<M extends Record<string, unknown>>(\n collectionPath: string,\n values: Partial<M>,\n id?: string | number,\n databaseId?: string\n ): Promise<Record<string, unknown>>;\n\n /**\n * Delete a entity by ID\n */\n delete(\n collectionPath: string,\n id: string | number,\n databaseId?: string\n ): Promise<void>;\n\n /**\n * Check if a field value is unique in a collection\n */\n checkUniqueField(\n collectionPath: string,\n fieldName: string,\n value: unknown,\n excludeEntityId?: string,\n databaseId?: string\n ): Promise<boolean>;\n\n}\n\n// =============================================================================\n// REALTIME INTERFACES\n// =============================================================================\n\n/**\n * Configuration for subscribing to a collection\n */\nexport interface CollectionSubscriptionConfig {\n clientId: string;\n path: string;\n filter?: unknown;\n /**\n * An `or(...)`/`and(...)` group, applied alongside `filter`.\n *\n * Declared here because a subscription is a query, and every field a query\n * has this one needs too. It was missing, so the type-checked boundary\n * dropped it: the client sent the group, nothing rejected it, and the\n * subscription re-fetched with the group gone — pushing every row the\n * caller's policies allowed rather than the ones they asked for. The same\n * defect `FetchCollectionProps.logical` documents, one layer up.\n */\n logical?: LogicalCondition;\n /**\n * Where the subscription's page starts. Missing for the same reason, with\n * a quieter symptom: a subscriber watching page two was pushed page one,\n * and a `collection_update` frame carries no window for it to notice with.\n */\n offset?: number;\n /** See `FetchCollectionProps.orderBy`: a field name plus `order`, or a list of tuples. */\n orderBy?: string | OrderByTuple[];\n order?: \"desc\" | \"asc\";\n limit?: number;\n startAfter?: unknown;\n databaseId?: string;\n searchString?: string;\n /** Ask each row which declared search field matched. */\n searchExplain?: boolean;\n}\n\n/**\n * Configuration for subscribing to a single entity\n */\nexport interface SingleSubscriptionConfig {\n clientId: string;\n path: string;\n id: string | number;\n}\n\n/**\n * Opt-in retention for one set of broadcast channels.\n *\n * Retention is configured on the server and nowhere else. A channel is created\n * by whoever names it, so letting a client ask for its own history depth would\n * let any visitor commit the backend to unbounded storage; and presence-only or\n * notification-only channels — the overwhelming majority — must not pay for a\n * feature they never use. With no rules configured nothing is written, no table\n * is created, and broadcast behaves exactly as it did before history existed.\n */\nexport interface ChannelRetentionRule {\n /**\n * Channel name to match. Either exact (`\"doc:42\"`) or a trailing-`*` prefix\n * (`\"doc:*\"`). Deliberately not a full glob or RegExp: this decides what\n * gets written to disk, and a rule whose blast radius is not obvious at a\n * glance is the wrong shape for that.\n */\n match: string;\n /** Keep at most this many of the most recent messages per channel. */\n limit?: number;\n /**\n * Keep messages for at most this long. Accepts a millisecond count or a\n * short duration string (`\"30s\"`, `\"15m\"`, `\"24h\"`, `\"7d\"`).\n */\n ttl?: number | string;\n}\n\n/**\n * Server-side realtime options.\n *\n * The channel bus contract and its config live in `./channel_bus` so that a\n * transport shipped as its own package depends on the contract alone.\n */\nexport interface RealtimeChannelsConfig {\n /**\n * Retention rules, most specific first — the first match wins. Omitted or\n * empty means no channel retains anything.\n */\n channels?: ChannelRetentionRule[];\n /**\n * How channel broadcast and presence reach other backend instances.\n * Defaults to `{ type: \"memory\" }` — i.e. they don't.\n */\n bus?: ChannelBusSetting;\n}\n\n/**\n * Abstract realtime provider interface.\n * Handles real-time subscriptions and notifications for entity changes.\n */\nexport interface RealtimeProvider {\n /**\n * Subscribe to collection changes.\n *\n * `onError` is called when a fetch behind the subscription fails, a\n * refetch after a change included, with the error as it was thrown. It is\n * not called for a fetch that newer rows have already overtaken, or after\n * the subscription is gone. Without it, a subscriber whose fetch failed\n * was told nothing and kept waiting for rows.\n */\n subscribeToCollection(\n subscriptionId: string,\n config: CollectionSubscriptionConfig,\n callback?: (rows: Record<string, unknown>[]) => void,\n onError?: (error: unknown) => void\n ): void;\n\n /**\n * Subscribe to single entity changes. `onError` as for\n * {@link RealtimeProvider.subscribeToCollection}.\n */\n subscribeToOne(\n subscriptionId: string,\n config: SingleSubscriptionConfig,\n callback?: (row: Record<string, unknown> | null) => void,\n onError?: (error: unknown) => void\n ): void;\n\n /**\n * Unsubscribe from a subscription\n */\n unsubscribe(subscriptionId: string): void;\n\n /**\n * Notify all relevant subscribers of a entity update\n */\n notifyUpdate(\n path: string,\n id: string,\n row: Record<string, unknown> | null,\n databaseId?: string\n ): Promise<void>;\n\n /**\n * Called when the HTTP server is ready and listening.\n * Useful for providers that need the server address for callbacks.\n */\n onServerReady?(serverInfo: { port: number; hostname?: string }): void;\n\n /**\n * Gracefully shut down the realtime provider.\n * Called during server shutdown to clean up resources.\n */\n destroy?(): Promise<void>;\n\n /**\n * Stop the internal LISTEN client (e.g., PostgreSQL LISTEN/NOTIFY).\n * Called during graceful shutdown before closing database connections.\n */\n stopListening?(): Promise<void>;\n}\n\n// =============================================================================\n// COLLECTION REGISTRY INTERFACES\n// =============================================================================\n\n/**\n * Abstract collection registry interface.\n * Manages registration and lookup of entity collections.\n */\nexport interface CollectionRegistryInterface {\n /**\n * Register a collection\n */\n register(collection: CollectionConfig): void;\n\n /**\n * Get a collection by its path\n */\n getCollectionByPath(path: string): CollectionConfig | undefined;\n\n /**\n * Get all registered collections\n */\n getCollections(): CollectionConfig[];\n\n /**\n * Get the currently registered global callbacks, if any.\n */\n getGlobalCallbacks(): any | undefined;\n\n /**\n * Take the global callbacks declared on `initializeRebaseBackend({ callbacks })`.\n *\n * A driver resolves callbacks from the registry it builds for itself, so\n * this is how the backend's global ones reach it: the coordinator hands\n * them over after `initializeDriver` returns. Optional so a registry for a\n * driver that runs no callbacks still type-checks, but a project that\n * declares global callbacks over a registry without it is refused at boot\n * — hooks that never run look exactly like hooks that passed.\n */\n setGlobalCallbacks?(callbacks: CollectionCallbacks): void;\n}\n\n// =============================================================================\n// DATA TRANSFORMER INTERFACES\n// =============================================================================\n\n/**\n * Abstract data transformer interface.\n * Handles serialization/deserialization between frontend and database formats.\n */\nexport interface DataTransformer {\n /**\n * Transform entity data for storage in the database\n */\n serializeToDatabase<M extends Record<string, unknown>>(\n entity: M,\n collection: CollectionConfig\n ): Record<string, unknown>;\n\n /**\n * Transform database data back to entity format\n */\n deserializeFromDatabase<M extends Record<string, unknown>>(\n data: Record<string, unknown>,\n collection: CollectionConfig\n ): Promise<M>;\n}\n\n// =============================================================================\n// DATABASE ADMIN — CAPABILITY-SPECIFIC INTERFACES (1.3)\n// =============================================================================\n\n/**\n * Administrative operations for SQL-based databases (PostgreSQL, MySQL, etc.).\n * Used by the SQL Editor, RLS Editor, and schema browser.\n *\n * @group Admin\n */\nexport interface SQLAdmin {\n /**\n * Execute raw SQL against the database.\n *\n * `isolateSession` runs it on a connection of its own and resets that\n * connection's session state — role, session authorization, settings —\n * before it is reused. For SQL a person wrote (the Studio editor), which\n * may `SET ROLE` and must not leave that on a pooled connection the\n * server's own queries use next.\n */\n executeSql(sql: string, options?: { database?: string; role?: string; params?: unknown[]; isolateSession?: boolean }): Promise<Record<string, unknown>[]>;\n\n /**\n * Fetch the available databases on the server.\n */\n fetchAvailableDatabases?(): Promise<string[]>;\n\n /**\n * Fetch the available *native PostgreSQL* database roles (from `pg_roles`).\n *\n * These are connection-level roles — what the SQL editor can `SET ROLE` to,\n * and what `SecurityRule.pgRoles` targets. They are NOT application roles;\n * for those use {@link fetchApplicationRoles}.\n */\n fetchAvailableRoles?(): Promise<string[]>;\n\n /**\n * Fetch the *application-level* roles in use in this project.\n *\n * These are the strings stored on the users table's `roles` column and\n * exposed to policies as `rebase.roles()` — what `SecurityRule.roles`\n * matches against. Distinct from {@link fetchAvailableRoles}; the two are\n * not interchangeable.\n */\n fetchApplicationRoles?(): Promise<string[]>;\n\n /**\n * Fetch the current database name.\n */\n fetchCurrentDatabase?(): Promise<string | undefined>;\n}\n\n/**\n * Administrative operations for document-based databases (MongoDB, Firestore, etc.).\n * Used by future document administration tools.\n *\n * @group Admin\n */\nexport interface DocumentAdmin {\n /**\n * Execute an aggregation pipeline or equivalent query.\n */\n executeAggregate?(pipeline: Record<string, unknown>[]): Promise<Record<string, unknown>[]>;\n\n /**\n * Fetch statistics for a collection (document count, size, etc.).\n */\n fetchCollectionStats?(collectionName: string): Promise<{ count: number; sizeBytes?: number }>;\n}\n\n/**\n * Administrative operations for schema management.\n * Shared across SQL and document databases.\n *\n * @group Admin\n */\nexport interface SchemaAdmin {\n /**\n * Fetch database tables/collections not yet mapped to a Rebase collection.\n */\n fetchUnmappedTables?(mappedPaths?: string[]): Promise<string[]>;\n\n /**\n * Fetch column/field metadata for a single table/collection.\n * The return type is generic — SQL backends return TableMetadata,\n * document backends may return a different shape.\n */\n fetchTableMetadata?(tableName: string): Promise<unknown>;\n}\n\n/**\n * Metadata for a database branch.\n * @group Admin\n */\nexport interface BranchInfo {\n /** Branch name (without prefix). */\n name: string;\n /** The database this branch was created from. */\n parentDatabase: string;\n /** When the branch was created. */\n createdAt: Date;\n /** Size in bytes, if available from the server. */\n sizeBytes?: number;\n}\n\n/**\n * Administrative operations for database branching.\n * Allows creating isolated database copies for development/preview workflows.\n *\n * @group Admin\n */\nexport interface BranchAdmin {\n /** Create a new branch (database copy) from the current or specified source database. */\n createBranch(name: string, options?: { source?: string }): Promise<BranchInfo>;\n\n /** Delete a branch database. Cannot delete the main/default database. */\n deleteBranch(name: string): Promise<void>;\n\n /** List all branches (databases that were created via branching). */\n listBranches(): Promise<BranchInfo[]>;\n\n /** Get info about a specific branch. */\n getBranchInfo(name: string): Promise<BranchInfo | undefined>;\n}\n\n/**\n * Union type for all admin capabilities.\n * A backend may implement any combination of these interfaces.\n *\n * Use type guards (`isSQLAdmin`, `isDocumentAdmin`, `isSchemaAdmin`, `isBranchAdmin`)\n * to safely narrow the type before calling methods.\n *\n * @group Admin\n */\nexport type DatabaseAdmin = Partial<SQLAdmin> & Partial<DocumentAdmin> & Partial<SchemaAdmin>\n & Partial<BranchAdmin> & Partial<SchemaEditingAdmin>;\n\n/**\n * Type guard: can this admin plan a live schema change?\n *\n * Planning is engine-specific — it renders DDL, a Drizzle schema and the\n * declarative SQL artifacts — so the implementation lives in the driver\n * package. The server detects the capability structurally, exactly as it does\n * for SQL, rather than importing an engine it is supposed to know nothing\n * about.\n *\n * @group Admin\n */\nexport function isSchemaEditingAdmin(admin: DatabaseAdmin | undefined): admin is SchemaEditingAdmin {\n return !!admin && typeof (admin as SchemaEditingAdmin).planSchemaChange === \"function\";\n}\n\n/**\n * Type guard: does this admin support SQL operations?\n * @group Admin\n */\nexport function isSQLAdmin(admin: DatabaseAdmin | undefined): admin is SQLAdmin {\n return !!admin && typeof (admin as SQLAdmin).executeSql === \"function\";\n}\n\n/**\n * Type guard: does this admin support document operations?\n * @group Admin\n */\nexport function isDocumentAdmin(admin: DatabaseAdmin | undefined): admin is DocumentAdmin {\n return !!admin && (\n typeof (admin as DocumentAdmin).executeAggregate === \"function\" ||\n typeof (admin as DocumentAdmin).fetchCollectionStats === \"function\"\n );\n}\n\n/**\n * Type guard: does this admin support schema management?\n * @group Admin\n */\nexport function isSchemaAdmin(admin: DatabaseAdmin | undefined): admin is SchemaAdmin {\n return !!admin && (\n typeof (admin as SchemaAdmin).fetchUnmappedTables === \"function\" ||\n typeof (admin as SchemaAdmin).fetchTableMetadata === \"function\"\n );\n}\n\n/**\n * Type guard: does this admin support database branching?\n * @group Admin\n */\nexport function isBranchAdmin(admin: DatabaseAdmin | undefined): admin is BranchAdmin {\n return !!admin && typeof (admin as BranchAdmin).createBranch === \"function\";\n}\n\n// =============================================================================\n// LIFECYCLE INTERFACES (1.4)\n// =============================================================================\n\n/**\n * Health check result returned by `healthCheck()`.\n * @group Lifecycle\n */\nexport interface HealthCheckResult {\n /** Whether the backend is healthy and able to serve requests. */\n healthy: boolean;\n /** Round-trip latency to the database in milliseconds. */\n latencyMs: number;\n /** Optional details (e.g., pool stats, replication lag). */\n details?: Record<string, unknown>;\n}\n\n/**\n * Lifecycle contract for backend components that hold resources\n * (database connections, WebSocket pools, timers, etc.).\n *\n * All methods are optional — simple backends (e.g., in-memory) can skip them.\n * @group Lifecycle\n */\nexport interface BackendLifecycle {\n /**\n * Initialize the backend: open connections, run migrations, seed data.\n * Called once during startup. Idempotent.\n */\n initialize?(): Promise<void>;\n\n /**\n * Check whether the backend is healthy and reachable.\n * Should be fast (< 1 s) and safe to call frequently.\n */\n healthCheck?(): Promise<HealthCheckResult>;\n\n /**\n * Gracefully shut down: close connections, flush buffers, cancel timers.\n * After calling `destroy()`, no other methods should be called.\n */\n destroy?(): Promise<void>;\n}\n\n// =============================================================================\n// BACKEND FACTORY INTERFACES\n// =============================================================================\n\n/**\n * Configuration for creating a database backend\n */\nexport interface BackendConfig {\n /**\n * Type of database backend\n */\n type: string;\n\n /**\n * Database connection (implementation-specific)\n */\n connection: unknown;\n\n /**\n * Schema definition (implementation-specific, e.g., Drizzle schema for PostgreSQL)\n */\n schema?: unknown;\n}\n\n/**\n * A complete backend instance with all required services.\n *\n * Now includes optional lifecycle management and admin capabilities.\n */\nexport interface BackendInstance extends BackendLifecycle {\n /**\n * Entity repository for CRUD operations\n */\n entityRepository: DataRepository;\n\n /**\n * Realtime provider for subscriptions\n */\n realtimeProvider: RealtimeProvider;\n\n /**\n * Collection registry\n */\n collectionRegistry: CollectionRegistryInterface;\n\n /**\n * The underlying database connection\n */\n connection: DatabaseConnection;\n\n /**\n * Administrative operations (SQL, schema, documents).\n * What's available depends on the backend type — use type guards\n * (`isSQLAdmin`, `isSchemaAdmin`, etc.) to narrow.\n */\n admin?: DatabaseAdmin;\n}\n\n/**\n * Factory function type for creating backend instances\n */\nexport type BackendFactory<TConfig extends BackendConfig = BackendConfig> =\n (config: TConfig) => BackendInstance;\n\n// =============================================================================\n// BACKEND BOOTSTRAPPER (1.2)\n// =============================================================================\n\n/**\n * A caller of the data API as its rate limiter buckets one outside an HTTP\n * request: a realtime socket's frame, counted in the same bucket the same\n * person's HTTP requests are.\n * @group Backend\n */\nexport interface DataRateLimitCaller {\n /**\n * The signed-in user's uid. Absent, or the anonymous principal, buckets\n * the caller by address at the anonymous allowance.\n */\n uid?: string;\n /**\n * A header of the request that opened the connection, by lower-case name:\n * the proxy headers an address is read from when proxies are declared.\n */\n header(name: string): string | undefined;\n /** The address the connection comes from. */\n socketAddress?: string;\n}\n\n/**\n * The data API's request limits, handed to the realtime socket so its frames\n * carry the ones an HTTP request to the same rows does.\n * @group Backend\n */\nexport interface RealtimeSocketLimits {\n /**\n * The largest frame accepted, in bytes — the data API's body limit. `0` or\n * less for none. Defaults to the server's default body limit.\n */\n maxPayload?: number;\n /**\n * Count one data request from `caller` in the data API's per-caller\n * buckets and say whether it is allowed; `null` for a caller who is not\n * limited. Absent when the deployment has rate limiting off.\n */\n dataRateLimit?: (caller: DataRateLimitCaller) => Promise<{ allowed: boolean; retryAfterMs: number } | null>;\n}\n\n/**\n * A `BackendBootstrapper` encapsulates all driver-specific initialization logic.\n *\n * Instead of hard-coding Postgres setup into `initializeRebaseBackend()`,\n * each database backend provides its own bootstrapper that knows how to:\n * - Create the DataDriver from a config object\n * - Optionally initialize auth tables\n * - Optionally create a realtime service\n * - Mount driver-specific API routes\n *\n * The main `initializeRebaseBackend()` becomes a **coordinator** that iterates\n * registered bootstrappers, calls their hooks, and wires the results together.\n *\n * @group Backend\n *\n * @example\n * ```typescript\n * // Third-party MySQL bootstrapper\n * const mysqlBootstrapper: BackendBootstrapper = {\n * type: \"mysql\",\n * initializeDriver: async (config) => new MySQLDataDriver(config.connection),\n * initializeRealtime: async (config) => new MySQLChangeStreamRealtime(config.connection),\n * };\n *\n * initializeRebaseBackend({\n * ...config,\n * bootstrappers: [postgresBootstrapper, mysqlBootstrapper]\n * });\n * ```\n */\nexport interface BackendBootstrapper {\n /**\n * Which driver type this bootstrapper handles.\n * Must match the `type` field on the driver config object\n * (e.g., `\"postgres\"`, `\"mongodb\"`, `\"mysql\"`).\n */\n type: string;\n\n /**\n * Unique identifier for this bootstrapper instance.\n * Used to register the driver in the driver registry.\n * Defaults to `type` if not set.\n */\n id?: string;\n\n /**\n * Whether this bootstrapper provides the default driver.\n * When true, the coordinator uses this driver as the primary one.\n */\n isDefault?: boolean;\n\n /**\n * Run database migrations for this driver.\n * Called by the coordinator after all drivers are initialized.\n */\n runMigrations?(config: unknown, driverResult: InitializedDriver): Promise<void>;\n\n /**\n * Create a DataDriver from the given config.\n * This is the only **required** method.\n */\n initializeDriver(config: unknown): Promise<InitializedDriver>;\n\n /**\n * Initialize auth tables / services if this driver supports them.\n * Return undefined if auth is not supported by this backend.\n */\n initializeAuth?(config: unknown, driverResult: InitializedDriver): Promise<BootstrappedAuth | undefined>;\n\n /**\n * Initialize history tables / services if this driver supports them.\n * Return undefined if history is not supported by this backend.\n */\n initializeHistory?(config: HistoryConfig, driverResult: InitializedDriver): Promise<{ historyService: unknown } | undefined>;\n\n /**\n * Create a realtime provider for this driver.\n * Return undefined if the driver does not support realtime.\n */\n initializeRealtime?(config: unknown, driverResult: InitializedDriver): Promise<RealtimeProvider | undefined>;\n\n /**\n * Mount any driver-specific HTTP routes (e.g., custom admin endpoints).\n * Called after all drivers are initialized.\n */\n mountRoutes?(app: unknown, basePath: string, driverResult: InitializedDriver): void;\n\n /**\n * Return admin capabilities for this driver.\n */\n getAdmin?(driverResult: InitializedDriver): DatabaseAdmin | undefined;\n\n /**\n * Ask the database whether it is there, before anything else touches it.\n *\n * Boot's first database call is not `initializeDriver` — it is the schema\n * provisioning that runs ahead of it, and a driver's connection diagnosis\n * therefore never got the chance to run. A stopped database produced\n * `Failed query: [redacted]` and a stack through drizzle internals: no host,\n * no port, no `ECONNREFUSED`, and no hint about starting the thing.\n *\n * Implementations MUST issue the cheapest round trip they have (`SELECT 1`),\n * MUST throw an error whose message names the host, the port and the\n * driver's own reason, and MAY log a fuller diagnosis first. They MUST NOT\n * throw for a reachable database that merely answered something unexpected —\n * the caller treats a throw as fatal.\n *\n * `driverResult` is optional for the same reason as\n * {@link ensureCollectionSchema}: this runs before `initializeDriver`, so an\n * adapter that was constructed with its own connection has to fall back to\n * it.\n */\n verifyConnection?(driverResult?: InitializedDriver): Promise<void>;\n\n /**\n * Bring the database's collection tables up to date, additively.\n *\n * Optional because it is only meaningful for schema-ful drivers. A managed\n * runtime boots a compiled project against a database it has never seen; auth\n * tables are ensured on boot but collection tables were created by nothing,\n * so every data request answered 500 on a missing relation. The CLI's `db\n * push` cannot fill the gap — it needs Atlas, and the runtime image ships no\n * CLI.\n *\n * Implementations MUST be additive-only: create missing tables, columns and\n * enum types, and never drop, narrow or rewrite anything. This runs\n * unattended against live customer data with nobody reading a diff, so the\n * destructive half stays a deliberate migration.\n *\n * `driverResult` is optional: this runs before `initializeDriver`, and only\n * the bundle path has a pre-init stand-in to pass. An adapter built by an\n * application already holds its own connection and MUST use it when this is\n * `undefined` — dereferencing it unconditionally works for managed tenants\n * and breaks every app that builds its own adapter.\n */\n ensureCollectionSchema?(\n collections: unknown[],\n driverResult?: InitializedDriver,\n log?: (message: string) => void\n ): Promise<{ applied: number }>;\n\n /**\n * Apply the collections' row-level-security policies, additively and\n * idempotently — the companion to {@link ensureCollectionSchema}.\n *\n * That method creates the tables; a table with RLS disabled and no policies\n * is not servable, because authenticated requests run as a restricted role:\n * a read with no `SELECT` policy returns nothing (a public collection\n * answers 401) and a write with no `INSERT`/`UPDATE` policy is denied. The\n * `db push` CLI applies these from the same collections, but it cannot reach\n * a managed tenant's in-cluster database — the runtime, already connected,\n * is the only thing that can.\n *\n * MUST be idempotent (re-run on every boot) and MUST NOT be destructive.\n * Runs after auth initialization, because the generated policies call the\n * `auth.*` helper functions and `CREATE POLICY` validates they exist.\n */\n ensureCollectionPolicies?(\n collections: unknown[],\n driverResult?: InitializedDriver,\n log?: (message: string) => void\n ): Promise<{ applied: number }>;\n\n /**\n * Create the RLS helper functions on this source's database. See\n * `DatabaseAdapter.ensureRlsRuntime`; needed on every source that is not\n * the default, whose helpers arrive with the auth tables.\n */\n ensureRlsRuntime?(driverResult?: InitializedDriver): Promise<void>;\n\n /**\n * Re-check, after the schema exists, that requests will actually be\n * constrained by the database's own authorization.\n *\n * A driver that isolates user requests by switching to a restricted role has\n * to decide at connect time whether the switch is needed — and on a fresh\n * database that question is asked before there is anything to answer with.\n * The process then creates the schema, becomes its owner, and an owner is\n * exempt from the policies on what it owns. So the answer that was true when\n * the driver initialized can be false by the time it serves a request.\n *\n * This is where a driver asks again. It runs once, after collection tables,\n * auth tables and policies are all in place, and it MUST fail rather than\n * serve when the answer changed and cannot be acted on: booting anyway\n * produces exactly the unenforced server this exists to prevent.\n *\n * Optional, because it is only meaningful for drivers whose isolation\n * depends on state the schema affects. A driver with nothing to re-check\n * omits it.\n */\n finalizeSecurityPosture?(driverResult: InitializedDriver): Promise<void>;\n\n /**\n * Read the collections schema version this database was last provisioned\n * from, or `null` when nothing has ever stamped it.\n *\n * The companion to {@link stampCollectionsSchemaVersion}: one process writes\n * what it applied, every other process compares itself to it. This is what\n * lets a split deployment — several processes over one database, only one of\n * which provisions — notice that a unit is serving against a schema it was\n * not built for. That failure is otherwise silent in both directions: a\n * column that does not exist is a SQL error on one route, and a policy that\n * was never applied is a 200 with no rows.\n *\n * `null` is not an error and MUST NOT be treated as one — every database\n * provisioned before the stamp existed reads this way, and so does every\n * fresh one until its first provisioning boot finishes.\n */\n readCollectionsSchemaVersion?(\n driverResult?: InitializedDriver\n ): Promise<string | null>;\n\n /**\n * Record the collections schema version this process just applied.\n *\n * Called only by the process that provisions, and only after both\n * {@link ensureCollectionSchema} and {@link ensureCollectionPolicies} have\n * run — a stamp written before the policies would claim a schema that is\n * only half in place, and the half that is missing is the one that fails\n * without an error.\n */\n stampCollectionsSchemaVersion?(\n version: string,\n driverResult?: InitializedDriver\n ): Promise<void>;\n\n /**\n * Initialize WebSocket server for realtime operations.\n */\n initializeWebsockets?(server: unknown, realtimeService: RealtimeProvider, driver: import(\"../controllers/data_driver\").DataDriver, config?: unknown, authAdapter?: AuthAdapter, limits?: RealtimeSocketLimits): Promise<void> | void;\n}\n\n/**\n * Result of `BackendBootstrapper.initializeDriver()`.\n * @group Backend\n */\nexport interface InitializedDriver {\n /** The DataDriver instance, ready for use. */\n driver: import(\"../controllers/data_driver\").DataDriver;\n\n /** The realtime service, if the driver created one during init. */\n realtimeProvider?: RealtimeProvider;\n\n /** A collection registry to register schema / tables into. */\n collectionRegistry?: CollectionRegistryInterface;\n\n /**\n * Collections the driver derived from the live database schema.\n *\n * Set by drivers that introspect in `baas` mode; the server serves these\n * instead of collections loaded from config files.\n */\n collections?: import(\"./collections\").CollectionConfig[];\n\n /** The underlying database connection (for lifecycle management). */\n connection?: DatabaseConnection;\n\n /**\n * Opaque handle that the bootstrapper can use in subsequent hooks\n * (e.g., `initializeAuth`, `mountRoutes`) to access driver internals.\n * Not used by the coordinator.\n */\n internals?: unknown;\n}\n\n/**\n * Result of `BackendBootstrapper.initializeAuth()`.\n * @group Backend\n */\nexport interface BootstrappedAuth {\n /** User management service. */\n userService: unknown;\n /** Role management service (optional, roles are now simple strings). */\n roleService?: unknown;\n /** Email service (optional). */\n emailService?: unknown;\n /** Combined Auth Repository for unified token and user management. */\n authRepository?: unknown;\n /**\n * Whether the auth schema in the database is one this runtime can serve.\n *\n * Folded into `healthCheck()` so a schema mismatch shows up as a degraded\n * health response. Without it, a server whose auth is entirely broken still\n * reports healthy — the database connection it probes is fine, and the\n * mismatch is only discovered one failed login at a time.\n */\n schemaHealthCheck?(): Promise<AuthSchemaHealth>;\n}\n\n/**\n * Result of {@link BootstrappedAuth.schemaHealthCheck}.\n * @group Lifecycle\n */\nexport interface AuthSchemaHealth {\n /** False when this runtime cannot be trusted to serve auth against this database. */\n healthy: boolean;\n /** Human-readable descriptions of each mismatch found. Empty when healthy. */\n problems: string[];\n /** Auth schema version recorded in the database, when it records one. */\n databaseVersion?: number | null;\n /** Auth schema version this runtime expects. */\n runtimeVersion?: number;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAwmBA,SAAgB,qBAAqB,OAA+D;CAChG,OAAO,CAAC,CAAC,SAAS,OAAQ,MAA6B,qBAAqB;AAChF;;;;;AAMA,SAAgB,WAAW,OAAqD;CAC5E,OAAO,CAAC,CAAC,SAAS,OAAQ,MAAmB,eAAe;AAChE"}
1
+ {"version":3,"file":"backend-DTAOsLQc.js","names":[],"sources":["../../types/src/types/backend.ts"],"sourcesContent":["import type { CollectionConfig, FilterValues, WhereFilterOp } from \"./collections\";\nimport type { CollectionCallbacks } from \"./entity_callbacks\";\nimport type { OrderByTuple } from \"./filter-operators\";\nimport type { LogicalCondition } from \"../controllers/data\";\nimport type { AuthAdapter } from \"./auth_adapter\";\nimport type { HistoryConfig } from \"../controllers/client\";\nimport type { ChannelBusSetting } from \"./channel_bus\";\nimport type { SchemaEditingAdmin } from \"./schema_editing\";\n\n// =============================================================================\n// DATABASE CONNECTION INTERFACES\n// =============================================================================\n\n/**\n * Abstract database connection interface.\n * Represents a connection to any database system.\n */\nexport interface DatabaseConnection {\n /**\n * Type identifier for this database (e.g., 'postgres', 'mongodb', 'mysql')\n */\n readonly type: string;\n\n /**\n * Whether the connection is currently active\n */\n readonly isConnected?: boolean;\n\n /**\n * Close the database connection and release resources.\n */\n close?(): Promise<void>;\n}\n\n// =============================================================================\n// QUERY BUILDING INTERFACES\n// =============================================================================\n\n/**\n * A single filter condition for database queries\n */\nexport interface QueryFilter {\n field: string;\n operator: WhereFilterOp;\n value: unknown;\n}\n\n/**\n * Options for fetching a collection of entities\n */\nexport interface FetchCollectionOptions<M extends Record<string, unknown> = Record<string, unknown>> {\n filter?: FilterValues<Extract<keyof M, string>>;\n /** See `FetchCollectionProps.orderBy`: a field name plus `order`, or a list of tuples. */\n orderBy?: string | OrderByTuple[];\n order?: \"desc\" | \"asc\";\n limit?: number;\n offset?: number;\n startAfter?: unknown;\n searchString?: string;\n databaseId?: string;\n collection?: CollectionConfig;\n}\n\n/**\n * Options for searching entities\n */\nexport interface SearchOptions<M extends Record<string, unknown> = Record<string, unknown>> {\n filter?: FilterValues<Extract<keyof M, string>>;\n /** See `FetchCollectionProps.orderBy`: a field name plus `order`, or a list of tuples. */\n orderBy?: string | OrderByTuple[];\n order?: \"desc\" | \"asc\";\n limit?: number;\n databaseId?: string;\n collection?: CollectionConfig;\n}\n\n/**\n * Options for counting entities\n */\nexport interface CountOptions<M extends Record<string, unknown> = Record<string, unknown>> {\n filter?: FilterValues<Extract<keyof M, string>>;\n /**\n * An `or(...)`/`and(...)` group, alongside `filter`.\n *\n * Counted as well as fetched, or `total` describes a different set of rows\n * from the one that was served — the same reason `filter` is here.\n */\n logical?: LogicalCondition;\n searchString?: string;\n databaseId?: string;\n}\n\n/**\n * Abstract condition builder interface.\n * Implementations translate Rebase filter conditions to database-specific queries.\n *\n * Note: This interface can be implemented as instance methods or as a class with static methods.\n * For static implementations (like DrizzleConditionBuilder), use the ConditionBuilderStatic type.\n *\n * @template T The type of condition returned by the builder (e.g., SQL for PostgreSQL, Filter<Document> for MongoDB)\n */\nexport interface ConditionBuilder<T = unknown> {\n /**\n * Build filter conditions from Rebase FilterValues\n */\n buildFilterConditions<M extends Record<string, unknown>>(\n filter: FilterValues<Extract<keyof M, string>>,\n collectionPath: string,\n ...args: unknown[]\n ): T[];\n\n /**\n * Build search conditions for text search.\n *\n * At most one condition comes back, already complete: callers OR what they\n * get, and OR is right across the searchable *fields* but wrong across the\n * *terms* of the search string — a person typing two words means both, and\n * the two halves of a name are two different fields. The empty array still\n * means \"nothing here can be searched\", which every caller reads as \"no\n * rows\".\n */\n buildSearchConditions(\n searchString: string,\n properties: Record<string, unknown>,\n ...args: unknown[]\n ): T[];\n\n /**\n * Combine multiple conditions with AND operator\n */\n combineConditionsWithAnd(conditions: T[]): T | undefined;\n\n /**\n * Combine multiple conditions with OR operator\n */\n combineConditionsWithOr(conditions: T[]): T | undefined;\n}\n\n/**\n * Static condition builder type for implementations using static methods.\n * Use this type when the class provides static methods rather than instance methods.\n *\n * @example\n * // DrizzleConditionBuilder satisfies this type\n * const builder: ConditionBuilderStatic<SQL> = DrizzleConditionBuilder;\n */\nexport type ConditionBuilderStatic<T = unknown> = {\n buildFilterConditions<M extends Record<string, unknown>>(\n filter: FilterValues<Extract<keyof M, string>>,\n ...args: unknown[]\n ): T[];\n buildSearchConditions(\n searchString: string,\n properties: Record<string, unknown>,\n ...args: unknown[]\n ): T[];\n combineConditionsWithAnd(conditions: T[]): T | undefined;\n combineConditionsWithOr(conditions: T[]): T | undefined;\n};\n\n// =============================================================================\n// ENTITY REPOSITORY INTERFACES\n// =============================================================================\n\n/**\n * Abstract entity repository interface.\n * Handles all CRUD operations for entities in the database.\n *\n * Implementations should handle:\n * - Entity serialization/deserialization\n * - Relation resolution\n * - ID generation and conversion\n */\nexport interface DataRepository {\n /**\n * Fetch a single entity by ID\n */\n fetchOne<M extends Record<string, unknown>>(\n collectionPath: string,\n id: string | number,\n databaseId?: string\n ): Promise<Record<string, unknown> | undefined>;\n\n /**\n * Fetch a collection of entities with optional filtering, ordering, and pagination\n */\n fetchCollection<M extends Record<string, unknown>>(\n collectionPath: string,\n options?: FetchCollectionOptions<M>\n ): Promise<Record<string, unknown>[]>;\n\n /**\n * Search entities by text\n */\n searchRows<M extends Record<string, unknown>>(\n collectionPath: string,\n searchString: string,\n options?: SearchOptions<M>\n ): Promise<Record<string, unknown>[]>;\n\n /**\n * Count entities in a collection\n */\n count<M extends Record<string, unknown>>(\n collectionPath: string,\n options?: CountOptions<M>\n ): Promise<number>;\n\n /**\n * Save a entity (create or update)\n */\n save<M extends Record<string, unknown>>(\n collectionPath: string,\n values: Partial<M>,\n id?: string | number,\n databaseId?: string\n ): Promise<Record<string, unknown>>;\n\n /**\n * Delete a entity by ID\n */\n delete(\n collectionPath: string,\n id: string | number,\n databaseId?: string\n ): Promise<void>;\n\n /**\n * Check if a field value is unique in a collection\n */\n checkUniqueField(\n collectionPath: string,\n fieldName: string,\n value: unknown,\n excludeEntityId?: string,\n databaseId?: string\n ): Promise<boolean>;\n\n}\n\n// =============================================================================\n// REALTIME INTERFACES\n// =============================================================================\n\n/**\n * Configuration for subscribing to a collection\n */\nexport interface CollectionSubscriptionConfig {\n clientId: string;\n path: string;\n filter?: unknown;\n /**\n * An `or(...)`/`and(...)` group, applied alongside `filter`.\n *\n * Declared here because a subscription is a query, and every field a query\n * has this one needs too. It was missing, so the type-checked boundary\n * dropped it: the client sent the group, nothing rejected it, and the\n * subscription re-fetched with the group gone — pushing every row the\n * caller's policies allowed rather than the ones they asked for. The same\n * defect `FetchCollectionProps.logical` documents, one layer up.\n */\n logical?: LogicalCondition;\n /**\n * Where the subscription's page starts. Missing for the same reason, with\n * a quieter symptom: a subscriber watching page two was pushed page one,\n * and a `collection_update` frame carries no window for it to notice with.\n */\n offset?: number;\n /** See `FetchCollectionProps.orderBy`: a field name plus `order`, or a list of tuples. */\n orderBy?: string | OrderByTuple[];\n order?: \"desc\" | \"asc\";\n limit?: number;\n startAfter?: unknown;\n databaseId?: string;\n searchString?: string;\n /** Ask each row which declared search field matched. */\n searchExplain?: boolean;\n}\n\n/**\n * Configuration for subscribing to a single entity\n */\nexport interface SingleSubscriptionConfig {\n clientId: string;\n path: string;\n id: string | number;\n}\n\n/**\n * Opt-in retention for one set of broadcast channels.\n *\n * Retention is configured on the server and nowhere else. A channel is created\n * by whoever names it, so letting a client ask for its own history depth would\n * let any visitor commit the backend to unbounded storage; and presence-only or\n * notification-only channels — the overwhelming majority — must not pay for a\n * feature they never use. With no rules configured nothing is written, no table\n * is created, and broadcast behaves exactly as it did before history existed.\n */\nexport interface ChannelRetentionRule {\n /**\n * Channel name to match. Either exact (`\"doc:42\"`) or a trailing-`*` prefix\n * (`\"doc:*\"`). Deliberately not a full glob or RegExp: this decides what\n * gets written to disk, and a rule whose blast radius is not obvious at a\n * glance is the wrong shape for that.\n */\n match: string;\n /** Keep at most this many of the most recent messages per channel. */\n limit?: number;\n /**\n * Keep messages for at most this long. Accepts a millisecond count or a\n * short duration string (`\"30s\"`, `\"15m\"`, `\"24h\"`, `\"7d\"`).\n */\n ttl?: number | string;\n}\n\n/**\n * Server-side realtime options.\n *\n * The channel bus contract and its config live in `./channel_bus` so that a\n * transport shipped as its own package depends on the contract alone.\n */\nexport interface RealtimeChannelsConfig {\n /**\n * Retention rules, most specific first — the first match wins. Omitted or\n * empty means no channel retains anything.\n */\n channels?: ChannelRetentionRule[];\n /**\n * How channel broadcast and presence reach other backend instances.\n * Defaults to `{ type: \"memory\" }` — i.e. they don't.\n */\n bus?: ChannelBusSetting;\n /**\n * How many subscriptions one socket may hold; the next is refused with\n * `TOO_MANY_SUBSCRIPTIONS`. A positive whole number, checked at boot.\n * Defaults to 1000. `REALTIME_MAX_SUBSCRIPTIONS_PER_SOCKET` wins over it.\n */\n maxSubscriptionsPerSocket?: number;\n}\n\n/**\n * Abstract realtime provider interface.\n * Handles real-time subscriptions and notifications for entity changes.\n */\nexport interface RealtimeProvider {\n /**\n * Subscribe to collection changes.\n *\n * `onError` is called when a fetch behind the subscription fails, a\n * refetch after a change included, with the error as it was thrown. It is\n * not called for a fetch that newer rows have already overtaken, or after\n * the subscription is gone. Without it, a subscriber whose fetch failed\n * was told nothing and kept waiting for rows.\n */\n subscribeToCollection(\n subscriptionId: string,\n config: CollectionSubscriptionConfig,\n callback?: (rows: Record<string, unknown>[]) => void,\n onError?: (error: unknown) => void\n ): void;\n\n /**\n * Subscribe to single entity changes. `onError` as for\n * {@link RealtimeProvider.subscribeToCollection}.\n */\n subscribeToOne(\n subscriptionId: string,\n config: SingleSubscriptionConfig,\n callback?: (row: Record<string, unknown> | null) => void,\n onError?: (error: unknown) => void\n ): void;\n\n /**\n * Unsubscribe from a subscription\n */\n unsubscribe(subscriptionId: string): void;\n\n /**\n * Notify all relevant subscribers of a entity update\n */\n notifyUpdate(\n path: string,\n id: string,\n row: Record<string, unknown> | null,\n databaseId?: string\n ): Promise<void>;\n\n /**\n * Called when the HTTP server is ready and listening.\n * Useful for providers that need the server address for callbacks.\n */\n onServerReady?(serverInfo: { port: number; hostname?: string }): void;\n\n /**\n * Gracefully shut down the realtime provider.\n * Called during server shutdown to clean up resources.\n */\n destroy?(): Promise<void>;\n\n /**\n * Stop the internal LISTEN client (e.g., PostgreSQL LISTEN/NOTIFY).\n * Called during graceful shutdown before closing database connections.\n */\n stopListening?(): Promise<void>;\n\n /**\n * The long-lived connections realtime depends on, for `/health`. A change\n * feed that has gone quiet — a half-open LISTEN connection — loses every\n * external and cross-instance change while writes through this instance\n * still look live, so it is reported rather than inferred.\n */\n health?(): RealtimeListenerHealth[];\n}\n\n/**\n * One long-lived realtime connection, as `/health` reports it.\n * @group Backend\n */\nexport interface RealtimeListenerHealth {\n /** What it carries — `\"cdc\"`, `\"cross-instance\"`, `\"channel-bus\"`. */\n name: string;\n /** Listening on a connection that answered its last heartbeat. */\n connected: boolean;\n /** When (epoch ms) it went down, while it is down. */\n downSince?: number;\n}\n\n// =============================================================================\n// COLLECTION REGISTRY INTERFACES\n// =============================================================================\n\n/**\n * Abstract collection registry interface.\n * Manages registration and lookup of entity collections.\n */\nexport interface CollectionRegistryInterface {\n /**\n * Register a collection\n */\n register(collection: CollectionConfig): void;\n\n /**\n * Get a collection by its path\n */\n getCollectionByPath(path: string): CollectionConfig | undefined;\n\n /**\n * Get all registered collections\n */\n getCollections(): CollectionConfig[];\n\n /**\n * Get the currently registered global callbacks, if any.\n */\n getGlobalCallbacks(): any | undefined;\n\n /**\n * Take the global callbacks declared on `initializeRebaseBackend({ callbacks })`.\n *\n * A driver resolves callbacks from the registry it builds for itself, so\n * this is how the backend's global ones reach it: the coordinator hands\n * them over after `initializeDriver` returns. Optional so a registry for a\n * driver that runs no callbacks still type-checks, but a project that\n * declares global callbacks over a registry without it is refused at boot\n * — hooks that never run look exactly like hooks that passed.\n */\n setGlobalCallbacks?(callbacks: CollectionCallbacks): void;\n}\n\n// =============================================================================\n// DATA TRANSFORMER INTERFACES\n// =============================================================================\n\n/**\n * Abstract data transformer interface.\n * Handles serialization/deserialization between frontend and database formats.\n */\nexport interface DataTransformer {\n /**\n * Transform entity data for storage in the database\n */\n serializeToDatabase<M extends Record<string, unknown>>(\n entity: M,\n collection: CollectionConfig\n ): Record<string, unknown>;\n\n /**\n * Transform database data back to entity format\n */\n deserializeFromDatabase<M extends Record<string, unknown>>(\n data: Record<string, unknown>,\n collection: CollectionConfig\n ): Promise<M>;\n}\n\n// =============================================================================\n// DATABASE ADMIN — CAPABILITY-SPECIFIC INTERFACES (1.3)\n// =============================================================================\n\n/**\n * The table column a value of a {@link SqlScriptResult} was read from, as the\n * database itself reported it — not as the query's text suggests.\n *\n * @group Admin\n */\nexport interface SqlScriptColumnSource {\n schema: string;\n table: string;\n column: string;\n}\n\n/**\n * One column of a {@link SqlScriptResult}, in result order.\n *\n * @group Admin\n */\nexport interface SqlScriptColumn {\n /** The name the result gives it. Two columns of one result may share it. */\n name: string;\n /** Its type, as the database names it: `integer`, `text[]`, `timestamp with time zone`. */\n type?: string;\n /**\n * Where the database says the value came from: a plain reference to a\n * column of a table or view, through any number of subqueries.\n *\n * Absent for everything no stored row holds — an expression, a cast that\n * changes the type, a literal, an aggregate, a function's output, a\n * `UNION`.\n */\n source?: SqlScriptColumnSource;\n}\n\n/**\n * A table or view some column of a {@link SqlScriptResult} came from.\n *\n * @group Admin\n */\nexport interface SqlScriptTable {\n schema: string;\n table: string;\n /** What the relation is. Only a `table` or a `partitioned table` holds rows that can be updated by key. */\n kind: \"table\" | \"partitioned table\" | \"view\" | \"materialized view\" | \"foreign table\" | \"other\";\n /** Its primary key columns, in key order. Empty when it has none. */\n primaryKey: string[];\n /**\n * Other tables inherit this one (by `INHERITS`, not as partitions). A row\n * read from it may live in one of them, and a primary key is not unique\n * across them.\n */\n hasInheritors: boolean;\n}\n\n/**\n * Something the database said while a script ran: `there is no transaction in\n * progress` for a ROLLBACK with nothing to end, a `RAISE NOTICE`.\n *\n * @group Admin\n */\nexport interface SqlScriptNotice {\n severity: string;\n message: string;\n}\n\n/**\n * What a SQL script a person wrote returned. See {@link SQLAdmin.runSqlScript}.\n *\n * @group Admin\n */\nexport interface SqlScriptResult {\n /**\n * The last statement's rows, every value as the text the database wrote\n * for it and `null` for SQL NULL — so a value written back is the value\n * that was read, whatever its type. Keyed by column name: of two columns\n * that share a name, the row holds the last.\n */\n rows: Record<string, string | null>[];\n /** The last statement's columns, in order. */\n columns: SqlScriptColumn[];\n /** The tables and views {@link SqlScriptColumn.source} names. */\n tables: SqlScriptTable[];\n /** The last statement's command: `SELECT`, `UPDATE`, `CREATE TABLE` … */\n command?: string;\n /** How many rows the last statement returned or changed, when its command reports a count. */\n rowCount?: number;\n /** What the database said while the script ran, in order. */\n notices: SqlScriptNotice[];\n /**\n * The database role the script ran as, when it asked for one — said by\n * the transport that ran it. The role asked for, unless the server has\n * role switching turned off (`DISABLE_DB_ROLE_SWITCHING`) and runs every\n * statement as the connection owner, who is named here instead.\n */\n effectiveRole?: string;\n}\n\n/**\n * Administrative operations for SQL-based databases (PostgreSQL, MySQL, etc.).\n * Used by the SQL Editor, RLS Editor, and schema browser.\n *\n * @group Admin\n */\nexport interface SQLAdmin {\n /**\n * Execute raw SQL against the database.\n *\n * `isolateSession` runs it on a connection of its own and resets that\n * connection's session state — role, session authorization, settings —\n * before it is reused. For SQL a person wrote (the Studio editor), which\n * may `SET ROLE` and must not leave that on a pooled connection the\n * server's own queries use next.\n */\n executeSql(sql: string, options?: { database?: string; role?: string; params?: unknown[]; isolateSession?: boolean }): Promise<Record<string, unknown>[]>;\n\n /**\n * Run a script a person wrote — the Studio SQL console — and describe what\n * it returned.\n *\n * On a session of its own, reset afterwards, as `role` for every statement\n * of it. Values come back as the database's text, and each column says\n * which table column it was read from, when the database says so: what\n * the console needs to write a cell back to the row it came from and to\n * no other. A script that leaves a transaction open is refused, and the\n * transaction rolled back: nothing outlives the run to be committed or\n * rolled back by a later one.\n */\n runSqlScript?(sql: string, options?: { database?: string; role?: string }): Promise<SqlScriptResult>;\n\n /**\n * Fetch the available databases on the server.\n */\n fetchAvailableDatabases?(): Promise<string[]>;\n\n /**\n * Fetch the available *native PostgreSQL* database roles (from `pg_roles`).\n *\n * These are connection-level roles — what the SQL editor can `SET ROLE` to,\n * and what `SecurityRule.pgRoles` targets. They are NOT application roles;\n * for those use {@link fetchApplicationRoles}.\n */\n fetchAvailableRoles?(): Promise<string[]>;\n\n /**\n * Fetch the *application-level* roles in use in this project.\n *\n * These are the strings stored on the users table's `roles` column and\n * exposed to policies as `rebase.roles()` — what `SecurityRule.roles`\n * matches against. Distinct from {@link fetchAvailableRoles}; the two are\n * not interchangeable.\n */\n fetchApplicationRoles?(): Promise<string[]>;\n\n /**\n * Fetch the current database name.\n */\n fetchCurrentDatabase?(): Promise<string | undefined>;\n}\n\n/**\n * Administrative operations for document-based databases (MongoDB, Firestore, etc.).\n * Used by future document administration tools.\n *\n * @group Admin\n */\nexport interface DocumentAdmin {\n /**\n * Execute an aggregation pipeline or equivalent query.\n */\n executeAggregate?(pipeline: Record<string, unknown>[]): Promise<Record<string, unknown>[]>;\n\n /**\n * Fetch statistics for a collection (document count, size, etc.).\n */\n fetchCollectionStats?(collectionName: string): Promise<{ count: number; sizeBytes?: number }>;\n}\n\n/**\n * Administrative operations for schema management.\n * Shared across SQL and document databases.\n *\n * @group Admin\n */\nexport interface SchemaAdmin {\n /**\n * Fetch database tables/collections not yet mapped to a Rebase collection.\n */\n fetchUnmappedTables?(mappedPaths?: string[]): Promise<string[]>;\n\n /**\n * Fetch column/field metadata for a single table/collection.\n * The return type is generic — SQL backends return TableMetadata,\n * document backends may return a different shape.\n */\n fetchTableMetadata?(tableName: string): Promise<unknown>;\n}\n\n/**\n * Metadata for a database branch.\n * @group Admin\n */\nexport interface BranchInfo {\n /** Branch name (without prefix). */\n name: string;\n /**\n * The PostgreSQL database the branch is — what to connect to, and what to\n * name as the source when copying it. As the branch's record says, never\n * derived from `name` again.\n */\n database: string;\n /** The database this branch was created from. */\n parentDatabase: string;\n /** When the branch was created. */\n createdAt: Date;\n /** Size in bytes, if available from the server. */\n sizeBytes?: number;\n}\n\n/**\n * Administrative operations for database branching.\n * Allows creating isolated database copies for development/preview workflows.\n *\n * @group Admin\n */\nexport interface BranchAdmin {\n /** Create a new branch (database copy) from the current or specified source database. */\n createBranch(name: string, options?: { source?: string }): Promise<BranchInfo>;\n\n /** Delete a branch database. Cannot delete the main/default database. */\n deleteBranch(name: string): Promise<void>;\n\n /** List all branches (databases that were created via branching). */\n listBranches(): Promise<BranchInfo[]>;\n\n /** Get info about a specific branch. */\n getBranchInfo(name: string): Promise<BranchInfo | undefined>;\n}\n\n/**\n * Union type for all admin capabilities.\n * A backend may implement any combination of these interfaces.\n *\n * Use type guards (`isSQLAdmin`, `isDocumentAdmin`, `isSchemaAdmin`, `isBranchAdmin`)\n * to safely narrow the type before calling methods.\n *\n * @group Admin\n */\nexport type DatabaseAdmin = Partial<SQLAdmin> & Partial<DocumentAdmin> & Partial<SchemaAdmin>\n & Partial<BranchAdmin> & Partial<SchemaEditingAdmin>;\n\n/**\n * Type guard: can this admin plan a live schema change?\n *\n * Planning is engine-specific — it renders DDL, a Drizzle schema and the\n * declarative SQL artifacts — so the implementation lives in the driver\n * package. The server detects the capability structurally, exactly as it does\n * for SQL, rather than importing an engine it is supposed to know nothing\n * about.\n *\n * @group Admin\n */\nexport function isSchemaEditingAdmin(admin: DatabaseAdmin | undefined): admin is SchemaEditingAdmin {\n return !!admin && typeof (admin as SchemaEditingAdmin).planSchemaChange === \"function\";\n}\n\n/**\n * Type guard: does this admin support SQL operations?\n * @group Admin\n */\nexport function isSQLAdmin(admin: DatabaseAdmin | undefined): admin is SQLAdmin {\n return !!admin && typeof (admin as SQLAdmin).executeSql === \"function\";\n}\n\n/**\n * Type guard: does this admin support document operations?\n * @group Admin\n */\nexport function isDocumentAdmin(admin: DatabaseAdmin | undefined): admin is DocumentAdmin {\n return !!admin && (\n typeof (admin as DocumentAdmin).executeAggregate === \"function\" ||\n typeof (admin as DocumentAdmin).fetchCollectionStats === \"function\"\n );\n}\n\n/**\n * Type guard: does this admin support schema management?\n * @group Admin\n */\nexport function isSchemaAdmin(admin: DatabaseAdmin | undefined): admin is SchemaAdmin {\n return !!admin && (\n typeof (admin as SchemaAdmin).fetchUnmappedTables === \"function\" ||\n typeof (admin as SchemaAdmin).fetchTableMetadata === \"function\"\n );\n}\n\n/**\n * Type guard: does this admin support database branching?\n * @group Admin\n */\nexport function isBranchAdmin(admin: DatabaseAdmin | undefined): admin is BranchAdmin {\n return !!admin && typeof (admin as BranchAdmin).createBranch === \"function\";\n}\n\n// =============================================================================\n// LIFECYCLE INTERFACES (1.4)\n// =============================================================================\n\n/**\n * Health check result returned by `healthCheck()`.\n * @group Lifecycle\n */\nexport interface HealthCheckResult {\n /** Whether the backend is healthy and able to serve requests. */\n healthy: boolean;\n /** Round-trip latency to the database in milliseconds. */\n latencyMs: number;\n /** Optional details (e.g., pool stats, replication lag). */\n details?: Record<string, unknown>;\n}\n\n/**\n * Lifecycle contract for backend components that hold resources\n * (database connections, WebSocket pools, timers, etc.).\n *\n * All methods are optional — simple backends (e.g., in-memory) can skip them.\n * @group Lifecycle\n */\nexport interface BackendLifecycle {\n /**\n * Initialize the backend: open connections, run migrations, seed data.\n * Called once during startup. Idempotent.\n */\n initialize?(): Promise<void>;\n\n /**\n * Check whether the backend is healthy and reachable.\n * Should be fast (< 1 s) and safe to call frequently.\n */\n healthCheck?(): Promise<HealthCheckResult>;\n\n /**\n * Gracefully shut down: close connections, flush buffers, cancel timers.\n * After calling `destroy()`, no other methods should be called.\n */\n destroy?(): Promise<void>;\n}\n\n// =============================================================================\n// BACKEND FACTORY INTERFACES\n// =============================================================================\n\n/**\n * Configuration for creating a database backend\n */\nexport interface BackendConfig {\n /**\n * Type of database backend\n */\n type: string;\n\n /**\n * Database connection (implementation-specific)\n */\n connection: unknown;\n\n /**\n * Schema definition (implementation-specific, e.g., Drizzle schema for PostgreSQL)\n */\n schema?: unknown;\n}\n\n/**\n * A complete backend instance with all required services.\n *\n * Now includes optional lifecycle management and admin capabilities.\n */\nexport interface BackendInstance extends BackendLifecycle {\n /**\n * Entity repository for CRUD operations\n */\n entityRepository: DataRepository;\n\n /**\n * Realtime provider for subscriptions\n */\n realtimeProvider: RealtimeProvider;\n\n /**\n * Collection registry\n */\n collectionRegistry: CollectionRegistryInterface;\n\n /**\n * The underlying database connection\n */\n connection: DatabaseConnection;\n\n /**\n * Administrative operations (SQL, schema, documents).\n * What's available depends on the backend type — use type guards\n * (`isSQLAdmin`, `isSchemaAdmin`, etc.) to narrow.\n */\n admin?: DatabaseAdmin;\n}\n\n/**\n * Factory function type for creating backend instances\n */\nexport type BackendFactory<TConfig extends BackendConfig = BackendConfig> =\n (config: TConfig) => BackendInstance;\n\n// =============================================================================\n// BACKEND BOOTSTRAPPER (1.2)\n// =============================================================================\n\n/**\n * A caller of the data API as its rate limiter buckets one outside an HTTP\n * request: a realtime socket's frame, counted in the same bucket the same\n * person's HTTP requests are.\n * @group Backend\n */\nexport interface DataRateLimitCaller {\n /**\n * The signed-in user's uid. Absent, or the anonymous principal, buckets\n * the caller by address at the anonymous allowance.\n */\n uid?: string;\n /**\n * The API key the connection authenticated with. Its frames count in the\n * key's own bucket at the key's own `rate_limit`, the bucket its HTTP\n * requests count in, rather than as `uid`.\n */\n apiKey?: { id: string; rate_limit: number | null };\n /**\n * A header of the request that opened the connection, by lower-case name:\n * the proxy headers an address is read from when proxies are declared.\n */\n header(name: string): string | undefined;\n /** The address the connection comes from. */\n socketAddress?: string;\n}\n\n/**\n * What the realtime socket shares with the HTTP data API: its request limits,\n * so a frame carries the ones an HTTP request to the same rows does, and its\n * API-key verification, so a key means the same thing on both.\n * @group Backend\n */\nexport interface RealtimeSocketOptions {\n /**\n * Verify an `rk_` API key presented to `AUTHENTICATE`: the identity it acts\n * as and the scopes it holds, or why it does not authenticate. Absent when\n * the deployment has no API-key store, and then keys cannot authenticate\n * the socket.\n */\n resolveApiKey?: (token: string) => Promise<{\n uid: string;\n roles: string[];\n scopes: string[];\n /** The key itself: its id and `rate_limit` decide which rate-limit bucket its frames count in. */\n apiKey?: { id: string; rate_limit: number | null };\n } | { message: string }>;\n /**\n * The largest frame accepted, in bytes — the data API's body limit. `0` or\n * less for none. Defaults to the server's default body limit.\n */\n maxPayload?: number;\n /**\n * Count one data request from `caller` in the data API's per-caller\n * buckets and say whether it is allowed; `null` for a caller who is not\n * limited. Absent when the deployment has rate limiting off.\n */\n dataRateLimit?: (caller: DataRateLimitCaller) => Promise<{ allowed: boolean; retryAfterMs: number } | null>;\n /**\n * How often a socket that only listens has its identity re-read, in ms\n * (default 30 000). A socket that sends frames is re-checked before each\n * one; this bounds how long a subscription keeps receiving rows as an\n * identity that has since signed out, been revoked, demoted or deleted.\n */\n identityRecheckIntervalMs?: number;\n}\n\n/**\n * A `BackendBootstrapper` encapsulates all driver-specific initialization logic.\n *\n * Instead of hard-coding Postgres setup into `initializeRebaseBackend()`,\n * each database backend provides its own bootstrapper that knows how to:\n * - Create the DataDriver from a config object\n * - Optionally initialize auth tables\n * - Optionally create a realtime service\n * - Mount driver-specific API routes\n *\n * The main `initializeRebaseBackend()` becomes a **coordinator** that iterates\n * registered bootstrappers, calls their hooks, and wires the results together.\n *\n * @group Backend\n *\n * @example\n * ```typescript\n * // Third-party MySQL bootstrapper\n * const mysqlBootstrapper: BackendBootstrapper = {\n * type: \"mysql\",\n * initializeDriver: async (config) => new MySQLDataDriver(config.connection),\n * initializeRealtime: async (config) => new MySQLChangeStreamRealtime(config.connection),\n * };\n *\n * initializeRebaseBackend({\n * ...config,\n * bootstrappers: [postgresBootstrapper, mysqlBootstrapper]\n * });\n * ```\n */\nexport interface BackendBootstrapper {\n /**\n * Which driver type this bootstrapper handles.\n * Must match the `type` field on the driver config object\n * (e.g., `\"postgres\"`, `\"mongodb\"`, `\"mysql\"`).\n */\n type: string;\n\n /**\n * Unique identifier for this bootstrapper instance.\n * Used to register the driver in the driver registry.\n * Defaults to `type` if not set.\n */\n id?: string;\n\n /**\n * Whether this bootstrapper provides the default driver.\n * When true, the coordinator uses this driver as the primary one.\n */\n isDefault?: boolean;\n\n /**\n * Run database migrations for this driver.\n * Called by the coordinator after all drivers are initialized.\n */\n runMigrations?(config: unknown, driverResult: InitializedDriver): Promise<void>;\n\n /**\n * Create a DataDriver from the given config.\n * This is the only **required** method.\n */\n initializeDriver(config: unknown): Promise<InitializedDriver>;\n\n /**\n * Initialize auth tables / services if this driver supports them.\n * Return undefined if auth is not supported by this backend.\n */\n initializeAuth?(config: unknown, driverResult: InitializedDriver): Promise<BootstrappedAuth | undefined>;\n\n /**\n * Initialize history tables / services if this driver supports them.\n * Return undefined if history is not supported by this backend.\n */\n initializeHistory?(config: HistoryConfig, driverResult: InitializedDriver): Promise<{ historyService: unknown } | undefined>;\n\n /**\n * Create a realtime provider for this driver.\n * Return undefined if the driver does not support realtime.\n */\n initializeRealtime?(config: unknown, driverResult: InitializedDriver): Promise<RealtimeProvider | undefined>;\n\n /**\n * Mount any driver-specific HTTP routes (e.g., custom admin endpoints).\n * Called after all drivers are initialized.\n */\n mountRoutes?(app: unknown, basePath: string, driverResult: InitializedDriver): void;\n\n /**\n * Return admin capabilities for this driver.\n */\n getAdmin?(driverResult: InitializedDriver): DatabaseAdmin | undefined;\n\n /**\n * Ask the database whether it is there, before anything else touches it.\n *\n * Boot's first database call is not `initializeDriver` — it is the schema\n * provisioning that runs ahead of it, and a driver's connection diagnosis\n * therefore never got the chance to run. A stopped database produced\n * `Failed query: [redacted]` and a stack through drizzle internals: no host,\n * no port, no `ECONNREFUSED`, and no hint about starting the thing.\n *\n * Implementations MUST issue the cheapest round trip they have (`SELECT 1`),\n * MUST throw an error whose message names the host, the port and the\n * driver's own reason, and MAY log a fuller diagnosis first. They MUST NOT\n * throw for a reachable database that merely answered something unexpected —\n * the caller treats a throw as fatal.\n *\n * `driverResult` is optional for the same reason as\n * {@link ensureCollectionSchema}: this runs before `initializeDriver`, so an\n * adapter that was constructed with its own connection has to fall back to\n * it.\n */\n verifyConnection?(driverResult?: InitializedDriver): Promise<void>;\n\n /**\n * Bring the database's collection tables up to date, additively.\n *\n * Optional because it is only meaningful for schema-ful drivers. A managed\n * runtime boots a compiled project against a database it has never seen; auth\n * tables are ensured on boot but collection tables were created by nothing,\n * so every data request answered 500 on a missing relation. The CLI's `db\n * push` cannot fill the gap — it needs Atlas, and the runtime image ships no\n * CLI.\n *\n * Implementations MUST be additive-only: create missing tables, columns and\n * enum types, and never drop, narrow or rewrite anything. This runs\n * unattended against live customer data with nobody reading a diff, so the\n * destructive half stays a deliberate migration.\n *\n * `driverResult` is optional: this runs before `initializeDriver`, and only\n * the bundle path has a pre-init stand-in to pass. An adapter built by an\n * application already holds its own connection and MUST use it when this is\n * `undefined` — dereferencing it unconditionally works for managed tenants\n * and breaks every app that builds its own adapter.\n */\n ensureCollectionSchema?(\n collections: unknown[],\n driverResult?: InitializedDriver,\n log?: (message: string) => void\n ): Promise<{ applied: number }>;\n\n /**\n * Apply the collections' row-level-security policies, additively and\n * idempotently — the companion to {@link ensureCollectionSchema}.\n *\n * That method creates the tables; a table with RLS disabled and no policies\n * is not servable, because authenticated requests run as a restricted role:\n * a read with no `SELECT` policy returns nothing (a public collection\n * answers 401) and a write with no `INSERT`/`UPDATE` policy is denied. The\n * `db push` CLI applies these from the same collections, but it cannot reach\n * a managed tenant's in-cluster database — the runtime, already connected,\n * is the only thing that can.\n *\n * MUST be idempotent (re-run on every boot) and MUST NOT be destructive.\n * Runs after auth initialization, because the generated policies call the\n * `auth.*` helper functions and `CREATE POLICY` validates they exist.\n */\n ensureCollectionPolicies?(\n collections: unknown[],\n driverResult?: InitializedDriver,\n log?: (message: string) => void\n ): Promise<{ applied: number }>;\n\n /**\n * Create the RLS helper functions on this source's database. See\n * `DatabaseAdapter.ensureRlsRuntime`; needed on every source that is not\n * the default, whose helpers arrive with the auth tables.\n */\n ensureRlsRuntime?(driverResult?: InitializedDriver): Promise<void>;\n\n /**\n * Re-check, after the schema exists, that requests will actually be\n * constrained by the database's own authorization.\n *\n * A driver that isolates user requests by switching to a restricted role has\n * to decide at connect time whether the switch is needed — and on a fresh\n * database that question is asked before there is anything to answer with.\n * The process then creates the schema, becomes its owner, and an owner is\n * exempt from the policies on what it owns. So the answer that was true when\n * the driver initialized can be false by the time it serves a request.\n *\n * This is where a driver asks again. It runs once, after collection tables,\n * auth tables and policies are all in place, and it MUST fail rather than\n * serve when the answer changed and cannot be acted on: booting anyway\n * produces exactly the unenforced server this exists to prevent.\n *\n * Optional, because it is only meaningful for drivers whose isolation\n * depends on state the schema affects. A driver with nothing to re-check\n * omits it.\n */\n finalizeSecurityPosture?(driverResult: InitializedDriver): Promise<void>;\n\n /**\n * Read the collections schema version this database was last provisioned\n * from, or `null` when nothing has ever stamped it.\n *\n * The companion to {@link stampCollectionsSchemaVersion}: one process writes\n * what it applied, every other process compares itself to it. This is what\n * lets a split deployment — several processes over one database, only one of\n * which provisions — notice that a unit is serving against a schema it was\n * not built for. That failure is otherwise silent in both directions: a\n * column that does not exist is a SQL error on one route, and a policy that\n * was never applied is a 200 with no rows.\n *\n * `null` is not an error and MUST NOT be treated as one — every database\n * provisioned before the stamp existed reads this way, and so does every\n * fresh one until its first provisioning boot finishes.\n */\n readCollectionsSchemaVersion?(\n driverResult?: InitializedDriver\n ): Promise<string | null>;\n\n /**\n * Record the collections schema version this process just applied.\n *\n * Called only by the process that provisions, and only after both\n * {@link ensureCollectionSchema} and {@link ensureCollectionPolicies} have\n * run — a stamp written before the policies would claim a schema that is\n * only half in place, and the half that is missing is the one that fails\n * without an error.\n */\n stampCollectionsSchemaVersion?(\n version: string,\n driverResult?: InitializedDriver\n ): Promise<void>;\n\n /**\n * Initialize WebSocket server for realtime operations.\n */\n initializeWebsockets?(server: unknown, realtimeService: RealtimeProvider, driver: import(\"../controllers/data_driver\").DataDriver, config?: unknown, authAdapter?: AuthAdapter, limits?: RealtimeSocketOptions): Promise<void> | void;\n}\n\n/**\n * Result of `BackendBootstrapper.initializeDriver()`.\n * @group Backend\n */\nexport interface InitializedDriver {\n /** The DataDriver instance, ready for use. */\n driver: import(\"../controllers/data_driver\").DataDriver;\n\n /** The realtime service, if the driver created one during init. */\n realtimeProvider?: RealtimeProvider;\n\n /** A collection registry to register schema / tables into. */\n collectionRegistry?: CollectionRegistryInterface;\n\n /**\n * Collections the driver derived from the live database schema.\n *\n * Set by drivers that introspect in `baas` mode; the server serves these\n * instead of collections loaded from config files.\n */\n collections?: import(\"./collections\").CollectionConfig[];\n\n /** The underlying database connection (for lifecycle management). */\n connection?: DatabaseConnection;\n\n /**\n * Opaque handle that the bootstrapper can use in subsequent hooks\n * (e.g., `initializeAuth`, `mountRoutes`) to access driver internals.\n * Not used by the coordinator.\n */\n internals?: unknown;\n}\n\n/**\n * Result of `BackendBootstrapper.initializeAuth()`.\n * @group Backend\n */\nexport interface BootstrappedAuth {\n /** User management service. */\n userService: unknown;\n /** Email service (optional). */\n emailService?: unknown;\n /** Combined Auth Repository for unified token and user management. */\n authRepository?: unknown;\n /**\n * Whether the auth schema in the database is one this runtime can serve.\n *\n * Folded into `healthCheck()` so a schema mismatch shows up as a degraded\n * health response. Without it, a server whose auth is entirely broken still\n * reports healthy — the database connection it probes is fine, and the\n * mismatch is only discovered one failed login at a time.\n */\n schemaHealthCheck?(): Promise<AuthSchemaHealth>;\n}\n\n/**\n * Result of {@link BootstrappedAuth.schemaHealthCheck}.\n * @group Lifecycle\n */\nexport interface AuthSchemaHealth {\n /** False when this runtime cannot be trusted to serve auth against this database. */\n healthy: boolean;\n /** Human-readable descriptions of each mismatch found. Empty when healthy. */\n problems: string[];\n /** Auth schema version recorded in the database, when it records one. */\n databaseVersion?: number | null;\n /** Auth schema version this runtime expects. */\n runtimeVersion?: number;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAuvBA,SAAgB,qBAAqB,OAA+D;CAChG,OAAO,CAAC,CAAC,SAAS,OAAQ,MAA6B,qBAAqB;AAChF;;;;;AAMA,SAAgB,WAAW,OAAqD;CAC5E,OAAO,CAAC,CAAC,SAAS,OAAQ,MAAmB,eAAe;AAChE"}
@@ -19,6 +19,16 @@ export declare function parseBackupDestination(out: string): BackupDestination;
19
19
  * Returns `null` for names that don't match, so foreign objects are ignored.
20
20
  */
21
21
  export declare function parseBackupTimestamp(fileName: string): Date | null;
22
+ /**
23
+ * Every key under a prefix, across pages.
24
+ *
25
+ * S3 and GCS answer a listing a page at a time, in ascending key order, and a
26
+ * backup's key is `rebase-<db>-<UTC timestamp>`. Reading one page therefore
27
+ * returned the OLDEST thousand objects: with retention unset and an hourly
28
+ * schedule, the newest backup the panel and `rebase db backups list` showed
29
+ * stopped advancing after about three weeks, which reads as "backups stopped".
30
+ */
31
+ export declare function listAllObjectKeys(storage: StorageController, prefix: string, bucket: string): Promise<string[]>;
22
32
  /**
23
33
  * List the backups at a destination as {@link BackupInfo}, newest first.
24
34
  * One entry per `.dump`, with its `.globals.sql` sidecar attached as
@@ -1,7 +1,9 @@
1
1
  import { Hono } from "hono";
2
+ import type { BackupScheduleStatus } from "@rebasepro/types";
2
3
  import type { HonoEnv } from "../api/types.js";
3
- import type { StorageController } from "../storage/index.js";
4
+ import type { StorageController } from "../storage/types.js";
4
5
  import { BackupDestination } from "./backup-common.js";
6
+ import { type ObjectBackupDestination } from "./backup-storage.js";
5
7
  export interface BackupRoutesConfig {
6
8
  /**
7
9
  * Resolve the current backup destination, or `null` when backups are not
@@ -9,14 +11,32 @@ export interface BackupRoutesConfig {
9
11
  * required to pick up config.
10
12
  */
11
13
  getDestination: () => BackupDestination | null;
12
- /** Storage controller for object-storage destinations. */
13
- storage?: StorageController;
14
+ /**
15
+ * The controller that reads an object-storage destination. It has to
16
+ * address the destination's own bucket — never the app's file storage,
17
+ * which is another bucket, a local directory, or nothing. Defaults to
18
+ * {@link backupStorageFromEnv} over `process.env`, which is how the backup
19
+ * cron builds the one it writes with.
20
+ */
21
+ storageFor?: (dest: ObjectBackupDestination) => StorageController;
22
+ /**
23
+ * The scheduled backup job and its last run — see `readBackupSchedule`.
24
+ * Omitted where this process has no cron scheduler; the listing then
25
+ * answers `schedule: null`.
26
+ */
27
+ getSchedule?: () => Promise<BackupScheduleStatus | null>;
28
+ /**
29
+ * True when this process does not run the scheduled jobs (the `api` role
30
+ * of a split deployment). A local destination read here is then this
31
+ * process's disk, not the one the scheduled backup writes to.
32
+ */
33
+ scheduledElsewhere?: boolean;
14
34
  }
15
35
  /**
16
36
  * Admin REST routes for the Backups panel.
17
37
  *
18
38
  * Routes (mounted under `/admin/backups`, admin-guarded by the caller):
19
- * GET / → list available backups
39
+ * GET / → list available backups, and the scheduled job's last run
20
40
  * GET /download → download a backup's bytes (?key=…)
21
41
  */
22
42
  export declare function createBackupRoutes(config: BackupRoutesConfig): Hono<HonoEnv>;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The scheduled backup job, as the Backups panel reports it.
3
+ *
4
+ * A backup that fails every night has to be visible where the backups are
5
+ * listed. Before this the panel read the destination and nothing else, so a
6
+ * cron that could not run `pg_dump` left an empty list under "wait for the next
7
+ * scheduled run" — while each failure sat in `cron_logs`, one panel away.
8
+ */
9
+ import type { BackupScheduleStatus, CronJobDefinition, CronJobLogEntry, CronJobStatus, RejectedCronJob } from "@rebasepro/types";
10
+ /**
11
+ * The mark `createBackupCron` (in `@rebasepro/server-postgres`) puts on the
12
+ * definition it returns.
13
+ *
14
+ * A registry symbol, so it is the same value across two installed copies of
15
+ * either package, and neither package has to import the other: `server` must
16
+ * not depend on `server-postgres`. The job cannot be recognised any other way —
17
+ * its id is the cron file's name and its display name is configurable.
18
+ */
19
+ export declare const BACKUP_CRON_MARK: unique symbol;
20
+ /** Whether a cron definition is the scheduled backup. */
21
+ export declare function isBackupCronDefinition(definition: CronJobDefinition): boolean;
22
+ /** The part of the cron scheduler this reads. */
23
+ export interface BackupScheduleSource {
24
+ jobIdsWhere(predicate: (definition: CronJobDefinition) => boolean): string[];
25
+ rejectedJobsWhere(predicate: (definition: CronJobDefinition) => boolean): RejectedCronJob[];
26
+ fetchJob(id: string): Promise<CronJobStatus | undefined>;
27
+ getJobLogsFromDb(id: string, limit?: number): Promise<CronJobLogEntry[]>;
28
+ }
29
+ /**
30
+ * The scheduled backup job and its last run, or `null` when the deployment
31
+ * has none — registered or refused.
32
+ */
33
+ export declare function readBackupSchedule(source: BackupScheduleSource): Promise<BackupScheduleStatus | null>;
@@ -0,0 +1,14 @@
1
+ import type { StorageController } from "../storage/types.js";
2
+ import type { BackupDestination } from "./backup-common.js";
3
+ /** A destination in object storage — `s3://bucket/prefix` or `gs://bucket/prefix`. */
4
+ export type ObjectBackupDestination = Exclude<BackupDestination, {
5
+ kind: "local";
6
+ }>;
7
+ /** The destination as `BACKUP_DESTINATION` spells it. */
8
+ export declare function describeBackupDestination(dest: BackupDestination): string;
9
+ /**
10
+ * A controller for an object-storage destination's bucket, built from the
11
+ * environment the way the backup cron and `rebase db backups list` build
12
+ * theirs. Throws when an S3 destination has no credentials to read it with.
13
+ */
14
+ export declare function backupStorageFromEnv(dest: ObjectBackupDestination, env: Record<string, string | undefined>): StorageController;
@@ -5,3 +5,5 @@
5
5
  export * from "./backup-common.js";
6
6
  export { createBackupRoutes } from "./backup-routes.js";
7
7
  export type { BackupRoutesConfig } from "./backup-routes.js";
8
+ export { readBackupSchedule, isBackupCronDefinition, BACKUP_CRON_MARK } from "./backup-schedule.js";
9
+ export type { BackupScheduleSource } from "./backup-schedule.js";