@rebasepro/server-postgres 0.25.1 → 0.26.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 (83) hide show
  1. package/dist/{BranchService-ClklxyAQ.js → BranchService-CWBnDhcv.js} +25 -9
  2. package/dist/BranchService-CWBnDhcv.js.map +1 -0
  3. package/dist/PostgresBackendDriver.d.ts +13 -0
  4. package/dist/PostgresBootstrapper.d.ts +7 -0
  5. package/dist/auth/services.d.ts +8 -1
  6. package/dist/auth/verify-auth-tables.d.ts +21 -0
  7. package/dist/{backup-cli-gijeXe0u.js → backup-cli-PoRG-zEq.js} +3 -3
  8. package/dist/{backup-cli-gijeXe0u.js.map → backup-cli-PoRG-zEq.js.map} +1 -1
  9. package/dist/{boot-ddl-DYH95Qeg.js → boot-ddl-EsgqDkx0.js} +2 -2
  10. package/dist/{boot-ddl-DYH95Qeg.js.map → boot-ddl-EsgqDkx0.js.map} +1 -1
  11. package/dist/branch-diff-CxE4VlxT.js +1396 -0
  12. package/dist/branch-diff-CxE4VlxT.js.map +1 -0
  13. package/dist/branch-diff-report-DpvVf7hR.js +48 -0
  14. package/dist/branch-diff-report-DpvVf7hR.js.map +1 -0
  15. package/dist/branching/branch-diff-report.d.ts +7 -0
  16. package/dist/branching/branch-diff.d.ts +115 -0
  17. package/dist/{cli-console-CQrQ0lAY.js → cli-console-f0k6_TsJ.js} +38 -5
  18. package/dist/cli-console-f0k6_TsJ.js.map +1 -0
  19. package/dist/cli-console.d.ts +17 -1
  20. package/dist/{cli-errors-DiA0XFIS.js → cli-errors-DsYyNYzd.js} +53 -2
  21. package/dist/cli-errors-DsYyNYzd.js.map +1 -0
  22. package/dist/cli.js +102 -122
  23. package/dist/cli.js.map +1 -1
  24. package/dist/{collections-schema-version-T6cGvrDi.js → collections-schema-version-D7bDI0PJ.js} +13 -9
  25. package/dist/collections-schema-version-D7bDI0PJ.js.map +1 -0
  26. package/dist/{deferred-index-build-BE0I6sYU.js → deferred-index-build-CqaOv1Xx.js} +3 -3
  27. package/dist/{deferred-index-build-BE0I6sYU.js.map → deferred-index-build-CqaOv1Xx.js.map} +1 -1
  28. package/dist/{doctor-BT--1kq3.js → doctor-DW_mlTzz.js} +81 -33
  29. package/dist/doctor-DW_mlTzz.js.map +1 -0
  30. package/dist/{ensure-collection-policies-BnGOURpG.js → ensure-collection-policies-DLWgabQ2.js} +5 -5
  31. package/dist/{ensure-collection-policies-BnGOURpG.js.map → ensure-collection-policies-DLWgabQ2.js.map} +1 -1
  32. package/dist/{ensure-collection-tables-Cet4650m.js → ensure-collection-tables-CFIRq8ni.js} +5 -5
  33. package/dist/{ensure-collection-tables-Cet4650m.js.map → ensure-collection-tables-CFIRq8ni.js.map} +1 -1
  34. package/dist/{ensure-tables-l_OgFQjN.js → ensure-tables-4nA-tfwi.js} +4 -4
  35. package/dist/{ensure-tables-l_OgFQjN.js.map → ensure-tables-4nA-tfwi.js.map} +1 -1
  36. package/dist/{enum-labels-kept-nheku1pB.js → enum-labels-kept-PcPW65xy.js} +2 -2
  37. package/dist/{enum-labels-kept-nheku1pB.js.map → enum-labels-kept-PcPW65xy.js.map} +1 -1
  38. package/dist/history/HistoryService.d.ts +7 -0
  39. package/dist/index.d.ts +20 -0
  40. package/dist/index.es.js +226 -8
  41. package/dist/index.es.js.map +1 -1
  42. package/dist/{db-subcommands-BPoHCxJR.js → migrate-baseline-B-2Ef4Mv.js} +266 -41
  43. package/dist/migrate-baseline-B-2Ef4Mv.js.map +1 -0
  44. package/dist/{migrate-policy-stamps-3eqfhes6.js → migrate-policy-stamps-BQtFHApx.js} +5 -5
  45. package/dist/{migrate-policy-stamps-3eqfhes6.js.map → migrate-policy-stamps-BQtFHApx.js.map} +1 -1
  46. package/dist/{open-rebase-EmqVmS14.js → open-rebase-C8jQ6s5a.js} +292 -46
  47. package/dist/open-rebase-C8jQ6s5a.js.map +1 -0
  48. package/dist/open-rebase.d.ts +9 -1
  49. package/dist/{pg-error-utils-CFEnm2m9.js → pg-error-utils-DhiUTlVx.js} +2 -2
  50. package/dist/{pg-error-utils-CFEnm2m9.js.map → pg-error-utils-DhiUTlVx.js.map} +1 -1
  51. package/dist/{push-index-prebuild-COd7i6wm.js → push-index-prebuild-UTvB5pqH.js} +3 -3
  52. package/dist/{push-index-prebuild-COd7i6wm.js.map → push-index-prebuild-UTvB5pqH.js.map} +1 -1
  53. package/dist/push-kept-defaults-BGOlOqRQ.js +137 -0
  54. package/dist/push-kept-defaults-BGOlOqRQ.js.map +1 -0
  55. package/dist/{push-legacy-renames-CLgYYwPE.js → push-legacy-renames-BwaKi8xF.js} +3 -3
  56. package/dist/{push-legacy-renames-CLgYYwPE.js.map → push-legacy-renames-BwaKi8xF.js.map} +1 -1
  57. package/dist/{push-policy-plan-D2gpQsdv.js → push-policy-plan-DQ2tWceD.js} +2 -2
  58. package/dist/{push-policy-plan-D2gpQsdv.js.map → push-policy-plan-DQ2tWceD.js.map} +1 -1
  59. package/dist/{rls-enforcement-DDyNoOSy.js → rls-enforcement-BOHuHVlc.js} +42 -3
  60. package/dist/rls-enforcement-BOHuHVlc.js.map +1 -0
  61. package/dist/schema/destructive-sql.d.ts +16 -0
  62. package/dist/schema/doctor-cli.js +3 -3
  63. package/dist/schema/introspect-db.js +1 -1
  64. package/dist/schema/migration-freshness.d.ts +69 -0
  65. package/dist/schema/push-kept-defaults.d.ts +57 -0
  66. package/dist/schema/rls-bootstrap-sql.d.ts +47 -10
  67. package/dist/security/rls-enforcement.d.ts +18 -0
  68. package/dist/services/FetchService.d.ts +12 -0
  69. package/dist/services/channel-presence.d.ts +3 -1
  70. package/dist/services/nested-path.d.ts +16 -0
  71. package/dist/services/realtimeService.d.ts +18 -1
  72. package/dist/{websocket-BKZwsjmf.js → websocket-CKYc3V4N.js} +41 -12
  73. package/dist/websocket-CKYc3V4N.js.map +1 -0
  74. package/package.json +7 -7
  75. package/dist/BranchService-ClklxyAQ.js.map +0 -1
  76. package/dist/cli-console-CQrQ0lAY.js.map +0 -1
  77. package/dist/cli-errors-DiA0XFIS.js.map +0 -1
  78. package/dist/collections-schema-version-T6cGvrDi.js.map +0 -1
  79. package/dist/db-subcommands-BPoHCxJR.js.map +0 -1
  80. package/dist/doctor-BT--1kq3.js.map +0 -1
  81. package/dist/open-rebase-EmqVmS14.js.map +0 -1
  82. package/dist/rls-enforcement-DDyNoOSy.js.map +0 -1
  83. package/dist/websocket-BKZwsjmf.js.map +0 -1
@@ -2,26 +2,28 @@ import { createRequire as __createRequire } from "module";
2
2
  __createRequire(import.meta.url);
3
3
  import { i as createPostgresDatabaseConnection } from "./connection-czvI7TZL.js";
4
4
  import { _ as visibleColumnProjection, a as buildSearchColumnSpec, r as SEARCH_UNACCENT_FN, s as hiddenColumnsOption } from "./search-column-BM-GV6vH.js";
5
- import { a as diagnoseDbError, c as isEconnrefused, n as closedDatabaseName, nt as extractCauseMessage, r as deepestErrorMessage, u as parseHostInfo } from "./cli-errors-DiA0XFIS.js";
6
- import { a as pgErrorToFriendlyMessage, i as isRowLevelSecurityDenial, n as extractPgError, o as reachedDatabase, r as isRoleSwitchingPermissionError, s as sanitizeErrorForClient, t as classifyConnectFailure } from "./pg-error-utils-CFEnm2m9.js";
7
- import { t as BranchService } from "./BranchService-ClklxyAQ.js";
8
- import { a as ensureAppRoleGrants, d as warnOnLegacyRlsFunctions, f as warnOnRoleSchemaCollision, i as ensureAppRole, l as validatePolicyPgRoles, n as applyAuthContext, r as detectConnectionPosture, s as revokeUserRoleSql, t as REBASE_USER_ROLE$1, u as warnOnAnonymousGrants } from "./rls-enforcement-DDyNoOSy.js";
5
+ import { a as diagnoseDbError, c as isEconnrefused, n as closedDatabaseName, r as deepestErrorMessage, rt as extractCauseMessage, u as parseHostInfo } from "./cli-errors-DsYyNYzd.js";
6
+ import { a as pgErrorToFriendlyMessage, i as isRowLevelSecurityDenial, n as extractPgError, o as reachedDatabase, r as isRoleSwitchingPermissionError, s as sanitizeErrorForClient, t as classifyConnectFailure } from "./pg-error-utils-DhiUTlVx.js";
7
+ import { t as BranchService } from "./BranchService-CWBnDhcv.js";
8
+ import { a as ensureAppRoleGrants, c as revokeUserRoleSql, d as warnOnAnonymousGrants, f as warnOnLegacyRlsFunctions, i as ensureAppRole, n as applyAuthContext, o as findExposedInternalTables, p as warnOnRoleSchemaCollision, r as detectConnectionPosture, t as REBASE_USER_ROLE$1, u as validatePolicyPgRoles } from "./rls-enforcement-BOHuHVlc.js";
9
9
  import { u as resolveColumnName } from "./column-plan-helpers-IG6uFb1s.js";
10
+ import { t as AUTH_USERS_COLUMNS } from "./auth-users-columns-C72EMoDJ.js";
10
11
  import { n as planSchema } from "./plan-schema-Czf3q4-Q.js";
11
12
  import { t as generateSchema } from "./generate-drizzle-schema-logic-CPzCRD-8.js";
12
13
  import { r as renderPgType } from "./render-ddl-4v88y-Gv.js";
13
- import { u as typesAgree } from "./deferred-index-build-BE0I6sYU.js";
14
- import { a as isLockTimeout, c as resolveBootLockOptions, l as sleep, n as BOOT_LOCK_TIMEOUT_MS } from "./boot-ddl-DYH95Qeg.js";
15
- import { a as readSchemaFactsFor, r as planCollectionSchemaEnsure } from "./ensure-collection-tables-Cet4650m.js";
14
+ import { u as typesAgree } from "./deferred-index-build-CqaOv1Xx.js";
15
+ import { a as isLockTimeout, c as resolveBootLockOptions, l as sleep, n as BOOT_LOCK_TIMEOUT_MS } from "./boot-ddl-EsgqDkx0.js";
16
+ import { a as readSchemaFactsFor, r as planCollectionSchemaEnsure } from "./ensure-collection-tables-CFIRq8ni.js";
17
+ import { a as mergeDatabaseBranch, i as diffDatabaseBranch } from "./branch-diff-CxE4VlxT.js";
16
18
  import { t as DatabasePoolManager } from "./databasePoolManager-3stMNFL-.js";
17
- import { a as resolveAuthSchema, i as probeAuthSchema, r as assertAuthSchemaCompatible, t as ensureAuthTablesExist } from "./ensure-tables-l_OgFQjN.js";
19
+ import { a as resolveAuthSchema, i as probeAuthSchema, r as assertAuthSchemaCompatible, t as ensureAuthTablesExist } from "./ensure-tables-4nA-tfwi.js";
18
20
  import { a as buildBaasSchema, c as introspectSchema, i as warnOnGeneratedSchemaDrift, l as readRlsStatus, n as diffGeneratedSchemaAgainstCatalogue, o as readCatalogueSchema, s as buildCollectionsFromSchema } from "./generated-schema-diff-eZCklRZQ.js";
19
21
  import { Client, DatabaseError, Pool } from "pg";
20
22
  import { drizzle } from "drizzle-orm/node-postgres";
21
23
  import { ApiError, assertQueryFieldsReadable, createDdlBootstrapper, createEmailService, logger, rawQueryLoggingEnabled, resolveBatchRefs, rowETag, splitFieldOps } from "@rebasepro/server";
22
24
  import { and, asc, count, desc, eq, getTableColumns, getTableName, gt, ilike, inArray, is, isNotNull, isNull, isTable, lt, lte, not, notInArray, or, relations, sql } from "drizzle-orm";
23
25
  import { PgArray, PgNumeric, PgTable, PgTimestamp, PgTimestampString, PgTransaction, bigint, boolean, getTableConfig, index, integer, jsonb, pgSchema, pgTable, text, timestamp, unique, uuid } from "drizzle-orm/pg-core";
24
- import { ALL_WHERE_FILTER_OPS, ANONYMOUS_USER_ID, DEFAULT_COMMIT_PATHS, DEFAULT_DATA_SOURCE_KEY, INCLUDE_COUNT_KEY, JUNCTION_PIVOT_KEY, ListLimitError, MAX_INCLUDE_DEPTH, Vector, encodeRelationAggregateSort, hasForeignKeyOnTarget, isChannelBusInstance, isChannelBusInstance as isChannelBusInstance$1, isManyToMany, isRelationalCollectionConfig, isSQLAdmin, parseEnvBoolean, parseRelationAggregateSort, resolveClientListLimit, resourceEnvSuffix, resourceKeyOf } from "@rebasepro/types";
26
+ import { ALL_WHERE_FILTER_OPS, ANONYMOUS_USER_ID, DEFAULT_COMMIT_PATHS, DEFAULT_DATA_SOURCE_KEY, INCLUDE_COUNT_KEY, JUNCTION_PIVOT_KEY, ListLimitError, MAX_INCLUDE_DEPTH, Vector, encodeRelationAggregateSort, hasForeignKeyOnTarget, isChannelBusInstance, isChannelBusInstance as isChannelBusInstance$1, isManyToMany, isRelationalCollectionConfig, isSQLAdmin, parseEnvBoolean, parseRelationAggregateSort, resolveClientListLimit, resourceEnvSuffix, resourceKeyOf, roleNameProblem } from "@rebasepro/types";
25
27
  import { COMPOSITE_ID_SEPARATOR, COMPOSITE_ID_SEPARATOR as COMPOSITE_ID_SEPARATOR$1, CollectionRegistry, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, OrderBySpecError, REBASE_USER_ROLE, UnknownFilterOperatorError, apiHiddenFieldNames, applyDefaultValuesOnCreate, buildCompositeId, buildCompositeId as buildCompositeId$1, buildPropertyCallbacks, buildSdkData, callbackRefusal, canReadField, classifyTable, createAccessorSlugResolver, createRelationRef, createRelationRefWithData, deserializeFilter, detectJunctionTables, encodeCursor, fieldKeyForColumn, findRelation, firstSqlRow, getColumnName, getDeclaredPrimaryKeys, getJunctionConfigForRelation, getTableName as getTableName$1, getTenantConfig, hasFieldAccessRules, includableRelationNames, isAddressableId, normalizeDriverOrderBy, normalizeEmail, normalizeInclude, normalizeToEntityRelation, parseIdValues, parseIdValues as parseIdValues$1, parseOrderBySpecStrict, queryableFieldNames, requireCallbackClient, requireCallbackCollection, resolveCollectionRelations, resolveTenantWrite, restoresSoftDeletedRow, revokeInternalTableSql, softDeleteFieldOf, sqlRows, tenantBypassRoles, toCallbackError, toFilterTuples, unknownAggregateField, unknownFilterField, unknownIncludeRelation, unknownOrderByField, unknownProjectionField, unknownRelationTargetField, unknownVectorProperty, updateDateAutoValues, updateUserAutoValues, withRelationReferencesOnly } from "@rebasepro/common";
26
28
  import { camelCase, generateForeignKeyName, isPrototypePollutingKey, legacyForeignKeyName, splitSearchTerms, toSnakeCase, toWireKey, unref } from "@rebasepro/utils";
27
29
  import { AsyncLocalStorage } from "node:async_hooks";
@@ -4403,6 +4405,38 @@ function isJunctionBackedRelation(relation) {
4403
4405
  return relation.shared;
4404
4406
  }
4405
4407
  /**
4408
+ * Reject a create or update whose body names a relation Rebase does not write
4409
+ * — a multi-hop `via` (`writable: false`) — before anything is read or written.
4410
+ *
4411
+ * A single-hop `via` is written through this door as it shipped (to-one: the
4412
+ * target's key; to-many: one junction's rows). A longer chain used to get as
4413
+ * far as the writer: a to-many one failed inside the transaction as a 500
4414
+ * RELATION_MISCONFIGURED telling the caller to regenerate a schema that was
4415
+ * fine, and a to-one one re-pointed the target at the first step's column.
4416
+ *
4417
+ * Matched the way the write path matches: a key that is a relation's own name
4418
+ * (the to-many split), or a `relation` property that resolves to one (the
4419
+ * to-one transformer). An `undefined` value is no value — the transformer skips
4420
+ * it — so it is not a write; `null` is, since it would unlink.
4421
+ */
4422
+ function assertNoReadOnlyRelationValues(values, collection) {
4423
+ const properties = collection.properties ?? {};
4424
+ let relations;
4425
+ const named = [];
4426
+ for (const [key, value] of Object.entries(values)) {
4427
+ if (value === void 0) continue;
4428
+ relations ??= resolveCollectionRelations(collection);
4429
+ const relation = relations[key] ?? (properties[key]?.type === "relation" ? findRelation(relations, key) : void 0);
4430
+ if (relation && !relation.writable) named.push(key);
4431
+ }
4432
+ if (named.length === 0) return;
4433
+ const list = named.map((key) => `'${key}'`).join(", ");
4434
+ throw ApiError.badRequest(`${list} on '${collection.slug}' ${named.length > 1 ? "are multi-hop `via` relations" : "is a multi-hop `via` relation"}, which is read-only: only single-hop via relations are writable — a to-one chain of one step, or a to-many chain through one junction table. Write the rows the chain joins directly.`, "RELATION_NOT_WRITABLE", {
4435
+ collection: collection.slug,
4436
+ fields: named
4437
+ });
4438
+ }
4439
+ /**
4406
4440
  * Reject a nested write whose final segment is a to-one relation.
4407
4441
  *
4408
4442
  * There is no column on the target row that records a to-one parent — the
@@ -4414,8 +4448,8 @@ function isJunctionBackedRelation(relation) {
4414
4448
  */
4415
4449
  function assertWritableThrough(hop, path) {
4416
4450
  const { relation } = hop;
4417
- if (relation.writable && relation.cardinality === "many") return;
4418
- if (!relation.writable) throw ApiError.badRequest(`"${path}" ends in '${hop.relationKey}', a \`via\` relation. Rebase will not infer how to write through an arbitrary join chain — write the row at "${hop.targetCollection.slug}" directly.`, "RELATION_NOT_WRITABLE");
4451
+ if (relation.kind !== "via" && relation.writable && relation.cardinality === "many") return;
4452
+ if (relation.kind === "via") throw ApiError.badRequest(`"${path}" ends in '${hop.relationKey}', a \`via\` relation, which a nested path does not write through — write the row at "${hop.targetCollection.slug}" directly` + (relation.writable ? `, and link it by setting '${hop.relationKey}' in an update of the '${hop.parentCollection.slug}' row.` : "."), "RELATION_NOT_WRITABLE");
4419
4453
  throw ApiError.badRequest(`"${path}" ends in the to-one relation '${hop.relationKey}', which cannot be written through: the foreign key for a to-one relation lives on '${hop.parentCollection.slug}', not on '${hop.targetCollection.slug}'. Write the target row at "${hop.targetCollection.slug}" and set '${hop.relationKey}' on the parent instead.`, "RELATION_NOT_WRITABLE");
4420
4454
  }
4421
4455
  //#endregion
@@ -5338,6 +5372,38 @@ var FetchService = class FetchService {
5338
5372
  return values;
5339
5373
  }
5340
5374
  /**
5375
+ * Every row of a collection's table, as stored, a page at a time, in key
5376
+ * order — the driver's `scanStoredRows`.
5377
+ *
5378
+ * Deliberately none of the read pipeline: no `beforeQuery` (a sweep has no
5379
+ * caller to scope to, and the documented hook narrows a caller-less read to
5380
+ * the rows owned by nobody), no soft-delete filter, no `afterRead`, no
5381
+ * field stripping. The columns come back under their table names; a sweep
5382
+ * reads values, not keys. Paged by the key rather than an offset, so a row
5383
+ * deleted during the scan never shifts another past the next page.
5384
+ */
5385
+ async *scanStoredRows(collectionPath, pageSize = 500) {
5386
+ const collection = getCollectionByPath(collectionPath, this.registry);
5387
+ const table = getTableForCollection(collection, this.registry);
5388
+ const keys = primaryKeyColumns(table, requirePrimaryKeys(collection, this.registry), collectionPath);
5389
+ const keyNames = keys.map((column) => {
5390
+ const entry = Object.entries(getTableColumns(table)).find(([, candidate]) => candidate === column);
5391
+ if (!entry) throw new Error(`Key column '${column.name}' not found in table for collection '${collectionPath}'`);
5392
+ return entry[0];
5393
+ });
5394
+ const limit = Math.max(1, pageSize);
5395
+ let after;
5396
+ for (;;) {
5397
+ const past = after ? or(...keys.map((column, i) => and(...keys.slice(0, i).map((earlier, j) => eq(earlier, after[j])), gt(column, after[i])))) : void 0;
5398
+ const query = this.db.select().from(table);
5399
+ const rows = await (past ? query.where(past) : query).orderBy(...keys.map((column) => asc(column))).limit(limit);
5400
+ if (rows.length > 0) yield rows;
5401
+ if (rows.length < limit) return;
5402
+ const last = rows[rows.length - 1];
5403
+ after = keyNames.map((name) => last[name]);
5404
+ }
5405
+ }
5406
+ /**
5341
5407
  * Fetch a collection of rows
5342
5408
  */
5343
5409
  async fetchCollection(collectionPath, options = {}) {
@@ -6803,6 +6869,7 @@ var PersistService = class {
6803
6869
  }
6804
6870
  }
6805
6871
  const collection = getCollectionByPath(effectiveCollectionPath, this.registry);
6872
+ assertNoReadOnlyRelationValues(effectiveValues, collection);
6806
6873
  const table = getTableForCollection(collection, this.registry);
6807
6874
  const idInfoArray = getPrimaryKeys(collection, this.registry);
6808
6875
  idInfoArray.map((info) => info.fieldName);
@@ -9476,10 +9543,29 @@ var PostgresBackendDriver = class PostgresBackendDriver {
9476
9543
  createBranch: this.branchService.createBranch.bind(this.branchService),
9477
9544
  deleteBranch: this.branchService.deleteBranch.bind(this.branchService),
9478
9545
  listBranches: this.branchService.listBranches.bind(this.branchService),
9479
- getBranchInfo: this.branchService.getBranchInfo.bind(this.branchService)
9546
+ getBranchInfo: this.branchService.getBranchInfo.bind(this.branchService),
9547
+ diffBranch: async (name, options) => this.nameDiffCollections(await diffDatabaseBranch(this.admin, name, options)),
9548
+ mergeBranch: (name, options) => mergeDatabaseBranch(this.admin, name, options)
9480
9549
  } : {}
9481
9550
  };
9482
9551
  }
9552
+ /** The diff with each table named by the collection it backs, where one does. */
9553
+ nameDiffCollections(diff) {
9554
+ const collections = this.registry?.getCollections() ?? [];
9555
+ const slugOf = (schema, table) => collections.find((collection) => {
9556
+ return ("schema" in collection && typeof collection.schema === "string" ? collection.schema : "public") === schema && getTableName$1(collection) === table;
9557
+ })?.slug;
9558
+ return {
9559
+ ...diff,
9560
+ data: diff.data.map((entry) => {
9561
+ const collection = slugOf(entry.schema, entry.table);
9562
+ return collection ? {
9563
+ ...entry,
9564
+ collection
9565
+ } : entry;
9566
+ })
9567
+ };
9568
+ }
9483
9569
  /**
9484
9570
  * `SQLAdmin.listen`: a `LISTEN` on a connection of its own, through the
9485
9571
  * listener CDC and the channel bus use, which reconnects and heartbeats.
@@ -9574,6 +9660,7 @@ var PostgresBackendDriver = class PostgresBackendDriver {
9574
9660
  const client = this.client;
9575
9661
  return {
9576
9662
  user: this.user,
9663
+ ...this.user?.credential ? { credential: this.user.credential } : {},
9577
9664
  driver: this,
9578
9665
  data: this.data,
9579
9666
  get client() {
@@ -9743,6 +9830,15 @@ var PostgresBackendDriver = class PostgresBackendDriver {
9743
9830
  return rows;
9744
9831
  }
9745
9832
  /**
9833
+ * Every stored row of a collection, past every read hook, for a whole-store
9834
+ * sweep — see `DataDriver.scanStoredRows`. Only this unscoped driver has
9835
+ * it: {@link AuthenticatedPostgresBackendDriver} reads as a caller, and a
9836
+ * caller's reads are scoped.
9837
+ */
9838
+ scanStoredRows(props) {
9839
+ return this.dataService.getFetchService().scanStoredRows(props.path, props.pageSize);
9840
+ }
9841
+ /**
9746
9842
  * Listen to a collection: the rows now, then again after every change.
9747
9843
  *
9748
9844
  * The realtime service runs every read, the first one included, as
@@ -11082,6 +11178,7 @@ var AuthenticatedPostgresBackendDriver = class {
11082
11178
  const client = this.delegate.client;
11083
11179
  writeScope?.setSettledContext({
11084
11180
  user: this.user,
11181
+ ...this.user?.credential ? { credential: this.user.credential } : {},
11085
11182
  driver: this,
11086
11183
  data: this.data,
11087
11184
  get client() {
@@ -13042,9 +13139,13 @@ var ChannelPresenceStore = class {
13042
13139
  * `tablesReady` is now set from a probe of what exists, not from having been
13043
13140
  * the instance that created it.
13044
13141
  */
13045
- async ensureTables() {
13046
- if (this.tablesReady) return;
13142
+ async ensureTables(options = {}) {
13143
+ if (this.tablesReady) return true;
13047
13144
  const ddl = drizzleDdlBootstrapper(this.db, "channel-presence");
13145
+ if (options.provision === false) {
13146
+ this.tablesReady = await ddl.isReadable("rebase.channel_presence");
13147
+ return this.tablesReady;
13148
+ }
13048
13149
  await ddl.ensureObject("rebase schema", "CREATE SCHEMA IF NOT EXISTS rebase");
13049
13150
  await ddl.ensureObject("channel_presence table", `
13050
13151
  CREATE TABLE IF NOT EXISTS rebase.channel_presence (
@@ -13068,6 +13169,7 @@ var ChannelPresenceStore = class {
13068
13169
  await ddl.step("channel_presence revoke", () => this.db.execute(sql.raw(revokeInternalTableSql("rebase", "channel_presence"))));
13069
13170
  this.tablesReady = true;
13070
13171
  }
13172
+ return this.tablesReady;
13071
13173
  }
13072
13174
  /** Record (or refresh) a client's presence. */
13073
13175
  async track(channel, clientId, state) {
@@ -13750,6 +13852,14 @@ var RealtimeService = class RealtimeService extends EventEmitter {
13750
13852
  refetchQueue = /* @__PURE__ */ new Set();
13751
13853
  /** Group refetches running now — at most {@link maxConcurrentRefetches}. */
13752
13854
  refetchesRunning = 0;
13855
+ /**
13856
+ * What a group refetch runs inside — see {@link setScopeRunner}. A refetch
13857
+ * runs on a timer armed by whatever announced the change; one armed by a
13858
+ * change-capture notification inherits the LISTEN connection's context,
13859
+ * which is no backend's, so a hook the read reached saw the process
13860
+ * default as `rebase`.
13861
+ */
13862
+ runInScope = (fn) => fn();
13753
13863
  /** See {@link refetchConcurrencyFor}. */
13754
13864
  maxConcurrentRefetches;
13755
13865
  /**
@@ -14018,6 +14128,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
14018
14128
  if (group.pending !== "all") group.pending.delete(key);
14019
14129
  if (group.members.size > 0) return;
14020
14130
  if (group.timer) clearTimeout(group.timer);
14131
+ if (group.recountTimer) clearTimeout(group.recountTimer);
14021
14132
  this.refetchQueue.delete(group);
14022
14133
  this.groups.delete(group.key);
14023
14134
  const [index, address] = group.type === "collection" ? [this.collectionGroupsByPath, group.path] : [this.singleGroupsByAddress, `${group.path}\u0000${group.id}`];
@@ -14091,7 +14202,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
14091
14202
  this.refetchQueue.delete(group);
14092
14203
  if (this.groups.get(group.key) !== group) continue;
14093
14204
  this.refetchesRunning++;
14094
- this.refetchGroup(group).finally(() => {
14205
+ this.runInScope(() => this.refetchGroup(group)).finally(() => {
14095
14206
  this.refetchesRunning--;
14096
14207
  this.drainRefetchQueue();
14097
14208
  });
@@ -14865,6 +14976,17 @@ var RealtimeService = class RealtimeService extends EventEmitter {
14865
14976
  });
14866
14977
  }
14867
14978
  /**
14979
+ * Refetch `group` — and so count it again — at `at`, when the total its
14980
+ * last frame reused expires. One pending at a time per group.
14981
+ */
14982
+ scheduleRecount(group, at) {
14983
+ if (group.recountTimer) return;
14984
+ group.recountTimer = setTimeout(() => {
14985
+ group.recountTimer = void 0;
14986
+ if (this.groups.get(group.key) === group) this.scheduleGroupRefetch(group);
14987
+ }, Math.max(0, at - Date.now()) + 1);
14988
+ }
14989
+ /**
14868
14990
  * The `meta` a `collection_update` frame carries beside its rows, counted
14869
14991
  * through `service` — the subscriber's own transaction, read only, with
14870
14992
  * its auth context and `beforeQuery` narrowing already bound.
@@ -14893,6 +15015,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
14893
15015
  else if (reusable && this.recountIntervalMs !== void 0 && reusable.total >= this.recountMinRows && Date.now() - reusable.at < this.recountIntervalMs) {
14894
15016
  total = reusable.total;
14895
15017
  totalAsOf = new Date(reusable.at).toISOString();
15018
+ if (group) this.scheduleRecount(group, reusable.at + this.recountIntervalMs);
14896
15019
  } else {
14897
15020
  try {
14898
15021
  total = await service.count(notifyPath, {
@@ -15471,7 +15594,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
15471
15594
  * what it was before this existed, whereas refusing to boot takes the whole
15472
15595
  * backend down for it.
15473
15596
  */
15474
- async configureChannelBus(bus) {
15597
+ async configureChannelBus(bus, options = {}) {
15475
15598
  if (bus.kind === "memory") {
15476
15599
  this.bus = bus;
15477
15600
  return;
@@ -15487,7 +15610,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
15487
15610
  this.bus = bus;
15488
15611
  try {
15489
15612
  const store = new ChannelPresenceStore(this.db, this.instanceId);
15490
- await store.ensureTables();
15613
+ if (!await store.ensureTables({ provision: options.provision }) && options.provision === false) throw new Error("rebase.channel_presence does not exist, and this backend changes nothing in its database. A provisioning run with the same channel bus configured creates it.");
15491
15614
  this.presenceStore = store;
15492
15615
  this.ensurePresenceSweep();
15493
15616
  } catch (error) {
@@ -15818,6 +15941,8 @@ var RealtimeService = class RealtimeService extends EventEmitter {
15818
15941
  for (const group of this.groups.values()) {
15819
15942
  if (group.timer) clearTimeout(group.timer);
15820
15943
  group.timer = void 0;
15944
+ if (group.recountTimer) clearTimeout(group.recountTimer);
15945
+ group.recountTimer = void 0;
15821
15946
  }
15822
15947
  this.unscheduledListGroups.clear();
15823
15948
  this.listPathChangedAt.clear();
@@ -15877,6 +16002,10 @@ var RealtimeService = class RealtimeService extends EventEmitter {
15877
16002
  isCdcActive() {
15878
16003
  return this.cdcActive && this.cdcListener?.connected === true;
15879
16004
  }
16005
+ /** See `RealtimeProvider.setScopeRunner`. */
16006
+ setScopeRunner(run) {
16007
+ this.runInScope = run;
16008
+ }
15880
16009
  /** The LISTEN connections this service depends on — see `RealtimeProvider.health`. */
15881
16010
  health() {
15882
16011
  const listeners = [];
@@ -16411,7 +16540,7 @@ function columnNames(table) {
16411
16540
  }
16412
16541
  return names;
16413
16542
  }
16414
- var quote = (xs) => Array.from(xs).map((s) => `\`${s}\``).join(", ");
16543
+ var quote$1 = (xs) => Array.from(xs).map((s) => `\`${s}\``).join(", ");
16415
16544
  /** `on.from` / `on.to` accept a single column or a composite tuple. */
16416
16545
  var asColumns = (value) => Array.isArray(value) ? value : [value];
16417
16546
  /**
@@ -16433,7 +16562,7 @@ function compositeKeyDefect(relation, source, target, registry) {
16433
16562
  const keys = getPrimaryKeys(keyed, registry);
16434
16563
  if (keys.length < 2) return void 0;
16435
16564
  return {
16436
- problem: `${linkColumn} is one column, and \`${keyed.slug}\` is keyed on ${quote(keys.map((key) => key.fieldName))} together — one column cannot reference a composite key`,
16565
+ problem: `${linkColumn} is one column, and \`${keyed.slug}\` is keyed on ${quote$1(keys.map((key) => key.fieldName))} together — one column cannot reference a composite key`,
16437
16566
  fix
16438
16567
  };
16439
16568
  };
@@ -16562,7 +16691,7 @@ function findRelationDefects(collections, registry, given) {
16562
16691
  } : {
16563
16692
  ...at,
16564
16693
  problem: `\`localKey: "${relation.localKey}"\` is not a column on \`${sourceTableName}\``,
16565
- fix: `add the column, or set \`localKey\` to one of: ${quote(sourceColumns)}`
16694
+ fix: `add the column, or set \`localKey\` to one of: ${quote$1(sourceColumns)}`
16566
16695
  });
16567
16696
  }
16568
16697
  break;
@@ -16576,13 +16705,13 @@ function findRelationDefects(collections, registry, given) {
16576
16705
  } : {
16577
16706
  ...at,
16578
16707
  problem: `\`foreignKeyOnTarget: "${relation.foreignKeyOnTarget}"\` is not a column on the target table \`${targetTableName}\``,
16579
- fix: `add the column, or set \`foreignKeyOnTarget\` to one of: ${quote(targetColumns)}`
16708
+ fix: `add the column, or set \`foreignKeyOnTarget\` to one of: ${quote$1(targetColumns)}`
16580
16709
  });
16581
16710
  }
16582
16711
  if (relation.sourceKey && !sourceColumns.has(relation.sourceKey)) defects.push({
16583
16712
  ...at,
16584
16713
  problem: `\`sourceKey: "${relation.sourceKey}"\` is not a column on \`${sourceTableName}\``,
16585
- fix: targetColumns.has(relation.sourceKey) ? `it is a column on the *target* table \`${targetTableName}\` — \`sourceKey\` names the column on this collection that the target's foreign key points at, so it must be one of: ${quote(sourceColumns)}` : `add the column, or set \`sourceKey\` to one of: ${quote(sourceColumns)}`
16714
+ fix: targetColumns.has(relation.sourceKey) ? `it is a column on the *target* table \`${targetTableName}\` — \`sourceKey\` names the column on this collection that the target's foreign key points at, so it must be one of: ${quote$1(sourceColumns)}` : `add the column, or set \`sourceKey\` to one of: ${quote$1(sourceColumns)}`
16586
16715
  });
16587
16716
  break;
16588
16717
  case "manyToMany": {
@@ -16609,7 +16738,7 @@ function findRelationDefects(collections, registry, given) {
16609
16738
  } : {
16610
16739
  ...at,
16611
16740
  problem: `\`through.${label}: "${column}"\` is not a column on the junction table \`${table}\``,
16612
- fix: `set \`through.${label}\` to one of: ${quote(junctionColumns)}` + (label === "sourceColumn" ? " — it is the column naming *this* collection" : "")
16741
+ fix: `set \`through.${label}\` to one of: ${quote$1(junctionColumns)}` + (label === "sourceColumn" ? " — it is the column naming *this* collection" : "")
16613
16742
  });
16614
16743
  }
16615
16744
  break;
@@ -16641,12 +16770,12 @@ function findRelationDefects(collections, registry, given) {
16641
16770
  for (const column of asColumns(step.on.from)) if (!prevColumns.has(column)) defects.push({
16642
16771
  ...at,
16643
16772
  problem: `step ${i + 1} joins \`${prevName}.${column}\` → \`${step.table}\`, but \`${column}\` is not a column on \`${prevName}\``,
16644
- fix: `\`joinPath[${i}].on.from\` names columns on ${i === 0 ? "this collection's table" : `the previous step's table (\`${prevName}\`)`}: ${quote(prevColumns)}`
16773
+ fix: `\`joinPath[${i}].on.from\` names columns on ${i === 0 ? "this collection's table" : `the previous step's table (\`${prevName}\`)`}: ${quote$1(prevColumns)}`
16645
16774
  });
16646
16775
  for (const column of asColumns(step.on.to)) if (!stepColumns.has(column)) defects.push({
16647
16776
  ...at,
16648
16777
  problem: `step ${i + 1} joins into \`${step.table}.${column}\`, but \`${column}\` is not a column on \`${step.table}\``,
16649
- fix: `\`joinPath[${i}].on.to\` names columns on \`${step.table}\`: ${quote(stepColumns)}`
16778
+ fix: `\`joinPath[${i}].on.to\` names columns on \`${step.table}\`: ${quote$1(stepColumns)}`
16650
16779
  });
16651
16780
  if (asColumns(step.on.from).length !== asColumns(step.on.to).length) defects.push({
16652
16781
  ...at,
@@ -17138,14 +17267,26 @@ var UserService = class {
17138
17267
  return result.rows[0].roles ?? [];
17139
17268
  }
17140
17269
  /**
17141
- * Set roles for a user (replaces existing roles)
17270
+ * Set roles for a user (replaces existing roles).
17271
+ *
17272
+ * Each role is its own bound parameter. The array used to be spelled as a
17273
+ * literal, `{${roles.join(",")}}`, so the database parsed what the caller
17274
+ * had only checked as strings: `"support,admin"` was stored as two roles,
17275
+ * one of them `admin`, and `"NULL"` as a SQL NULL. A name that would be read
17276
+ * back as other roles is refused (`roleNameProblem`) — RLS splits the
17277
+ * comma-joined `app.user_roles` the same way.
17142
17278
  */
17143
17279
  async setUserRoles(uid, roleIds) {
17280
+ const problems = roleIds.flatMap((role) => {
17281
+ const problem = roleNameProblem(role);
17282
+ return problem ? [`${JSON.stringify(role)} ${problem}`] : [];
17283
+ });
17284
+ if (problems.length > 0) throw ApiError.badRequest(`Not valid role names: ${problems.join("; ")}`, "INVALID_ROLE");
17144
17285
  const usersTableName = this.getQualifiedUsersTableName();
17145
- const rolesArray = `{${roleIds.join(",")}}`;
17286
+ const rolesArray = roleIds.length === 0 ? sql`'{}'::text[]` : sql`ARRAY[${sql.join(roleIds.map((role) => sql`${role}`), sql`, `)}]::text[]`;
17146
17287
  await this.withServerContext(async (db) => db.execute(sql`
17147
17288
  UPDATE ${sql.raw(usersTableName)}
17148
- SET roles = ${rolesArray}::text[], updated_at = NOW()
17289
+ SET roles = ${rolesArray}, updated_at = NOW()
17149
17290
  WHERE id = ${uid}
17150
17291
  `));
17151
17292
  }
@@ -18167,6 +18308,59 @@ var MfaService = class {
18167
18308
  }
18168
18309
  };
18169
18310
  //#endregion
18311
+ //#region src/auth/verify-auth-tables.ts
18312
+ /**
18313
+ * Whether the auth tables a backend reads are there, on a backend that may not
18314
+ * create them.
18315
+ *
18316
+ * A driver opened without provisioning skips `ensureAuthTablesExist` — the
18317
+ * tables are another process's to make — and before this nothing took its
18318
+ * place. A database that lacked `rebase.refresh_tokens`, or had a users table
18319
+ * from a release before `disabled_at`, booted green and then failed every
18320
+ * sign-in with a 500 whose cause was one missing relation. Auth cannot run on
18321
+ * part of its schema, so this is a refusal at boot, naming each table.
18322
+ *
18323
+ * Asked by reading, not by the catalogue: a `SELECT` of every column the auth
18324
+ * services name, `WHERE false`. That answers "missing", "out of date" and "this
18325
+ * role may not read it" in one statement per table, with the database's own
18326
+ * words for whichever it is.
18327
+ */
18328
+ var quote = (identifier) => `"${identifier.replace(/"/g, "\"\"")}"`;
18329
+ /** What a Drizzle table definition says auth will query. */
18330
+ function expectationOf(table) {
18331
+ const config = getTableConfig(table);
18332
+ return {
18333
+ table: config.schema ? `${quote(config.schema)}.${quote(config.name)}` : quote(config.name),
18334
+ columns: Object.values(getTableColumns(table)).map((column) => column.name)
18335
+ };
18336
+ }
18337
+ /**
18338
+ * The users table, which is the project's own when it declares an auth
18339
+ * collection: only the columns the auth bootstrap guarantees on it are
18340
+ * expected, never the collection's other fields — those are the collection
18341
+ * schema's business, checked where collections are.
18342
+ */
18343
+ function usersExpectation(qualifiedTable) {
18344
+ return {
18345
+ table: qualifiedTable,
18346
+ columns: ["id", ...AUTH_USERS_COLUMNS.map((spec) => spec.column)]
18347
+ };
18348
+ }
18349
+ /** Each table that cannot be read as expected, with the database's reason. */
18350
+ async function findUnusableAuthTables(db, expectations) {
18351
+ const problems = [];
18352
+ for (const { table, columns } of expectations) try {
18353
+ await db.execute(sql.raw(`SELECT ${columns.map(quote).join(", ")} FROM ${table} WHERE false`));
18354
+ } catch (err) {
18355
+ problems.push(`${table}: ${deepestErrorMessage(err)}`);
18356
+ }
18357
+ return problems;
18358
+ }
18359
+ /** The refusal, phrased as what to run. */
18360
+ function unusableAuthTablesError(problems) {
18361
+ return /* @__PURE__ */ new Error("Auth cannot run on this database, and this backend changes nothing in it (provisioning is off):\n" + problems.map((problem) => ` - ${problem}`).join("\n") + "\nCreate them with a provisioning run against this database (REBASE_PROVISION_ONLY=true, without REBASE_PROVISION=false), or apply the SQL such a run recorded (REBASE_PROVISION_SQL_OUT).");
18362
+ }
18363
+ //#endregion
18170
18364
  //#region src/history/ensure-history-table.ts
18171
18365
  /** How a history entry gets written from inside the write it records. */
18172
18366
  var RECORD_HISTORY_FUNCTION = "rebase.record_history";
@@ -18570,6 +18764,28 @@ var HistoryService = class {
18570
18764
  `)) ?? null;
18571
18765
  }
18572
18766
  /**
18767
+ * Every entry's `values` and `previous_values`, of every row, a page at a
18768
+ * time in id order — for a whole-store sweep. `rebase storage orphans`
18769
+ * reads it because a history revert restores the values an entry holds,
18770
+ * so a file only an entry names is still in use.
18771
+ */
18772
+ async *scanStoredValues(pageSize = 1e3) {
18773
+ const limit = Math.max(1, pageSize);
18774
+ let after;
18775
+ for (;;) {
18776
+ const rows = sqlRows(await this.db.execute(sql`
18777
+ SELECT id, "values", previous_values
18778
+ FROM rebase.entity_history
18779
+ ${after === void 0 ? sql`` : sql`WHERE id > ${after}`}
18780
+ ORDER BY id
18781
+ LIMIT ${limit}
18782
+ `));
18783
+ if (rows.length > 0) yield rows.map((row) => [row.values, row.previous_values]);
18784
+ if (rows.length < limit) return;
18785
+ after = rows[rows.length - 1].id;
18786
+ }
18787
+ }
18788
+ /**
18573
18789
  * Each entry's `format`. A table without the column holds only entries
18574
18790
  * that store the whole previous row, which is what `1` says.
18575
18791
  */
@@ -19206,6 +19422,7 @@ async function openPostgresDriver(pgConfig, config) {
19206
19422
  driver.listenConnectionString = directUrl;
19207
19423
  realtimeService.setDataDriver(driver);
19208
19424
  let rlsEnforcement;
19425
+ let exposedInternalTables;
19209
19426
  {
19210
19427
  const runSql = async (text) => {
19211
19428
  return (await schemaAwareDb.execute(sql.raw(text))).rows ?? [];
@@ -19223,6 +19440,10 @@ async function openPostgresDriver(pgConfig, config) {
19223
19440
  if (!provision) {
19224
19441
  const roleExists = (await runSql(`SELECT 1 FROM pg_roles WHERE rolname = '${REBASE_USER_ROLE$1}'`)).length > 0;
19225
19442
  switched = roleExists || bypassesNow;
19443
+ if (roleExists) {
19444
+ exposedInternalTables = await findExposedInternalTables(runSql, managedSchemas);
19445
+ if (exposedInternalTables.length > 0) logger.warn(`⚠️ RLS enforcement: the request role "${REBASE_USER_ROLE$1}" can reach framework tables no user should: ${exposedInternalTables.join(", ")}. A provisioning boot revokes this; this driver was opened without provisioning, so nothing does.`);
19446
+ }
19226
19447
  if (!roleExists && bypassesNow) logger.warn(`⚠️ RLS enforcement: connection role "${posture.role}" bypasses row-level security and the restricted role "${REBASE_USER_ROLE$1}" does not exist yet — this driver was opened without provisioning, so it was not created. Requests made as a user will fail until a server boot (or \`rebase db push\`) creates it; requests as the connection itself are unaffected.`);
19227
19448
  } else try {
19228
19449
  await ensureAppRole(runSql, managedSchemas);
@@ -19262,7 +19483,7 @@ async function openPostgresDriver(pgConfig, config) {
19262
19483
  if (!realtimeOff && (isChannelBusInstance(busSetting) || busSetting.type !== "memory")) await realtimeService.configureChannelBus(createChannelBus(busSetting, {
19263
19484
  db: schemaAwareDb,
19264
19485
  directUrl
19265
- }));
19486
+ }), { provision });
19266
19487
  } catch (err) {
19267
19488
  logger.warn("⚠️ [ChannelBus] Could not configure the channel bus — channel broadcast and presence stay per-instance", { error: err });
19268
19489
  }
@@ -19397,6 +19618,7 @@ async function openPostgresDriver(pgConfig, config) {
19397
19618
  poolManager,
19398
19619
  provisioned: provision,
19399
19620
  rlsEnforcement,
19621
+ exposedInternalTables,
19400
19622
  provisionCdcForTables
19401
19623
  };
19402
19624
  return {
@@ -19467,7 +19689,7 @@ function createPostgresBootstrapper(pgConfig) {
19467
19689
  const openIndexBuildSession = (driverResult) => async () => {
19468
19690
  const db = requireProvisioningDb(driverResult);
19469
19691
  const pool = "$client" in db ? db.$client : void 0;
19470
- const { isPgPool, poolIndexBuildSession } = await import("./deferred-index-build-BE0I6sYU.js").then((n) => n.r);
19692
+ const { isPgPool, poolIndexBuildSession } = await import("./deferred-index-build-CqaOv1Xx.js").then((n) => n.r);
19471
19693
  if (isPgPool(pool)) return poolIndexBuildSession(pool)();
19472
19694
  const queryable = provisioningQueryable(driverResult);
19473
19695
  return {
@@ -19493,7 +19715,7 @@ function createPostgresBootstrapper(pgConfig) {
19493
19715
  */
19494
19716
  const provisionRlsRuntime = async (driverResult) => {
19495
19717
  const db = requireProvisioningDb(driverResult);
19496
- const { RLS_BOOTSTRAP_STATEMENTS, rlsRuntimeIsCurrent } = await import("./db-subcommands-BPoHCxJR.js").then((n) => n.v);
19718
+ const { RLS_BOOTSTRAP_STATEMENTS, rlsRuntimeIsCurrent } = await import("./migrate-baseline-B-2Ef4Mv.js").then((n) => n.w);
19497
19719
  const run = async (text) => sqlRows(await db.execute(sql.raw(text)));
19498
19720
  if (await rlsRuntimeIsCurrent(run).catch(() => false)) return;
19499
19721
  await db.transaction(async (tx) => {
@@ -19526,6 +19748,9 @@ function createPostgresBootstrapper(pgConfig) {
19526
19748
  const init = config;
19527
19749
  const opened = await openPostgresDriver(effectiveConfig(init.dataSourceKey), init);
19528
19750
  if (opened.internals.rlsEnforcement) logger.info(opened.internals.rlsEnforcement);
19751
+ const exposed = opened.internals.exposedInternalTables ?? [];
19752
+ if (exposed.length > 0) throw new Error(`The request role "${REBASE_USER_ROLE$1}" can read or write framework tables that hold sessions, key hashes and job payloads: ${exposed.join(", ")}.\nA provisioning boot revokes this, and this backend changes nothing in its database (provisioning is off). Revoke it as part of your change process:
19753
+ ` + exposed.map((table) => ` REVOKE ALL ON ${table.split(".").map((part) => `"${part}"`).join(".")} FROM ${REBASE_USER_ROLE$1};`).join("\n"));
19529
19754
  return opened;
19530
19755
  },
19531
19756
  async initializeAuth(config, driverResult) {
@@ -19536,7 +19761,23 @@ function createPostgresBootstrapper(pgConfig) {
19536
19761
  const registry = internals.registry;
19537
19762
  const authCollection = authConfig.collection;
19538
19763
  if (internals.provisioned !== false) await ensureAuthTablesExist(db, authCollection);
19539
- else await assertAuthSchemaCompatible(db, resolveAuthSchema(authCollection));
19764
+ else {
19765
+ await assertAuthSchemaCompatible(db, resolveAuthSchema(authCollection));
19766
+ const usersSchema = authCollection ? "schema" in authCollection && typeof authCollection.schema === "string" ? authCollection.schema : "public" : "rebase";
19767
+ const usersTableName = authCollection ? "table" in authCollection && typeof authCollection.table === "string" ? authCollection.table : authCollection.slug : "users";
19768
+ const expected = createAuthSchema(usersSchema === "public" ? "rebase" : usersSchema);
19769
+ const problems = await findUnusableAuthTables(db, [usersExpectation(usersSchema === "public" ? `"${usersTableName}"` : `"${usersSchema}"."${usersTableName}"`), ...[
19770
+ expected.refreshTokens,
19771
+ expected.passwordResetTokens,
19772
+ expected.appConfig,
19773
+ expected.userIdentities,
19774
+ expected.mfaFactors,
19775
+ expected.mfaChallenges,
19776
+ expected.recoveryCodes,
19777
+ expected.magicLinkTokens
19778
+ ].map(expectationOf)]);
19779
+ if (problems.length > 0) throw unusableAuthTablesError(problems);
19780
+ }
19540
19781
  if (authCollection && internals.provisionCdcForTables) {
19541
19782
  const authSchema = "schema" in authCollection && typeof authCollection.schema === "string" ? authCollection.schema : "rebase";
19542
19783
  const authTable = "table" in authCollection && typeof authCollection.table === "string" ? authCollection.table : authCollection.slug;
@@ -19587,11 +19828,11 @@ function createPostgresBootstrapper(pgConfig) {
19587
19828
  unavailable: historyUnavailableReason(state, { requireTransaction: false })
19588
19829
  }) };
19589
19830
  }
19590
- const { BOOT_LOCK_TIMEOUT_MS } = await import("./boot-ddl-DYH95Qeg.js").then((n) => n.i);
19831
+ const { BOOT_LOCK_TIMEOUT_MS } = await import("./boot-ddl-EsgqDkx0.js").then((n) => n.i);
19591
19832
  const historyTable = await ensureHistoryTableExists(db, { lockTimeoutMs: BOOT_LOCK_TIMEOUT_MS });
19592
19833
  const historyIndexes = await ensureHistoryIndexes(db, { lockTimeoutMs: BOOT_LOCK_TIMEOUT_MS });
19593
19834
  if (historyIndexes.length > 0) {
19594
- const { buildDeferredIndexes, scheduleDeferredIndexBuild } = await import("./deferred-index-build-BE0I6sYU.js").then((n) => n.r);
19835
+ const { buildDeferredIndexes, scheduleDeferredIndexBuild } = await import("./deferred-index-build-CqaOv1Xx.js").then((n) => n.r);
19595
19836
  const indexOptions = {
19596
19837
  log: (message) => logger.info(`[history] ${message}`),
19597
19838
  lockTimeoutMs: BOOT_LOCK_TIMEOUT_MS
@@ -19647,9 +19888,9 @@ function createPostgresBootstrapper(pgConfig) {
19647
19888
  * proved it can bootstrap with.
19648
19889
  */
19649
19890
  async ensureCollectionSchema(collections, driverResult, log) {
19650
- const { ensureCollectionTables } = await import("./ensure-collection-tables-Cet4650m.js").then((n) => n.n);
19891
+ const { ensureCollectionTables } = await import("./ensure-collection-tables-CFIRq8ni.js").then((n) => n.n);
19651
19892
  const queryable = provisioningQueryable(driverResult);
19652
- const { BOOT_LOCK_TIMEOUT_MS } = await import("./boot-ddl-DYH95Qeg.js").then((n) => n.i);
19893
+ const { BOOT_LOCK_TIMEOUT_MS } = await import("./boot-ddl-EsgqDkx0.js").then((n) => n.i);
19653
19894
  const provisionOnly = ["1", "true"].includes((process.env.REBASE_PROVISION_ONLY ?? "").trim().toLowerCase());
19654
19895
  const plan = await ensureCollectionTables(queryable, collections, log, {
19655
19896
  lockTimeoutMs: BOOT_LOCK_TIMEOUT_MS,
@@ -19661,7 +19902,7 @@ function createPostgresBootstrapper(pgConfig) {
19661
19902
  pendingIndexBuild?.cancel();
19662
19903
  pendingIndexBuild = void 0;
19663
19904
  if (!provisionOnly && plan.deferredIndexes.length > 0) {
19664
- const { scheduleDeferredIndexBuild } = await import("./deferred-index-build-BE0I6sYU.js").then((n) => n.r);
19905
+ const { scheduleDeferredIndexBuild } = await import("./deferred-index-build-CqaOv1Xx.js").then((n) => n.r);
19665
19906
  logger.info(`[schema] ${plan.deferredIndexes.length} index(es) are missing, or were left INVALID, on tables that already exist. They are built in the background once the server is serving, one at a time, without blocking reads or writes.`);
19666
19907
  pendingIndexBuild = scheduleDeferredIndexBuild(openIndexBuildSession(driverResult), plan.deferredIndexes, {
19667
19908
  log: (message) => logger.info(`[schema] ${message}`),
@@ -19701,7 +19942,7 @@ function createPostgresBootstrapper(pgConfig) {
19701
19942
  await provisionRlsRuntime(driverResult);
19702
19943
  },
19703
19944
  async ensureCollectionPolicies(collections, driverResult, log) {
19704
- const { ensureCollectionPolicies, describeRulesDropped } = await import("./ensure-collection-policies-BnGOURpG.js");
19945
+ const { ensureCollectionPolicies, describeRulesDropped } = await import("./ensure-collection-policies-DLWgabQ2.js");
19705
19946
  const queryable = provisioningQueryable(driverResult);
19706
19947
  const { rows } = await queryable.query("SELECT to_regprocedure('rebase.roles()') IS NOT NULL AS present");
19707
19948
  if (!rows[0]?.present) await provisionRlsRuntime(driverResult);
@@ -19726,7 +19967,7 @@ function createPostgresBootstrapper(pgConfig) {
19726
19967
  for (const u of outcome.unsecured.filter((u) => u.grantWithdrawn)) logger.error(`🔐 [rls] Could not enable row-level security on "${u.table}": ${u.error}. Its privileges have been revoked from ${REBASE_USER_ROLE$1}, so the table is unreachable rather than unprotected. Reads and writes to that collection will fail until RLS can be enabled.`);
19727
19968
  if (unrevoked.length > 0) throw new Error("Refusing to start: row-level security could not be enabled on " + unrevoked.map((u) => `"${u.table}" (${u.error})`).join(", ") + `, and the privileges granted to ${REBASE_USER_ROLE$1} could not be revoked either. The table would be served with no row filtering. Fix the database permissions (the connection role must own these tables, or be able to ALTER them) and boot again.`);
19728
19969
  try {
19729
- const { dropLegacyAuthSchema } = await import("./db-subcommands-BPoHCxJR.js").then((n) => n.v);
19970
+ const { dropLegacyAuthSchema } = await import("./migrate-baseline-B-2Ef4Mv.js").then((n) => n.w);
19730
19971
  await dropLegacyAuthSchema(async (text) => (await queryable.query(text)).rows, {
19731
19972
  info: (m) => logger.info(m),
19732
19973
  warn: (m) => logger.warn(m)
@@ -19746,14 +19987,14 @@ function createPostgresBootstrapper(pgConfig) {
19746
19987
  async readCollectionsSchemaVersion(driverResult) {
19747
19988
  const db = provisioningDb(driverResult);
19748
19989
  if (!db) return null;
19749
- const { readCollectionsSchemaVersion } = await import("./collections-schema-version-T6cGvrDi.js");
19990
+ const { readCollectionsSchemaVersion } = await import("./collections-schema-version-D7bDI0PJ.js");
19750
19991
  return readCollectionsSchemaVersion(db, SCHEMA_META_SCHEMA);
19751
19992
  },
19752
19993
  /** Record what this process just applied. Only the provisioning process calls this. */
19753
19994
  async stampCollectionsSchemaVersion(version, driverResult) {
19754
19995
  const db = provisioningDb(driverResult);
19755
19996
  if (!db) return;
19756
- const { stampCollectionsSchemaVersion } = await import("./collections-schema-version-T6cGvrDi.js");
19997
+ const { stampCollectionsSchemaVersion } = await import("./collections-schema-version-D7bDI0PJ.js");
19757
19998
  await stampCollectionsSchemaVersion(db, SCHEMA_META_SCHEMA, version);
19758
19999
  },
19759
20000
  /**
@@ -19817,7 +20058,7 @@ function createPostgresBootstrapper(pgConfig) {
19817
20058
  return driverResult.internals.driver.admin;
19818
20059
  },
19819
20060
  async initializeWebsockets(server, realtimeService, driver, config, adapter, limits) {
19820
- const { createPostgresWebSocket, isSocketRealtimeService, socketRealtimeMissing } = await import("./websocket-BKZwsjmf.js").then((n) => n.c);
20061
+ const { createPostgresWebSocket, isSocketRealtimeService, socketRealtimeMissing } = await import("./websocket-CKYc3V4N.js").then((n) => n.c);
19821
20062
  if (!isSocketRealtimeService(realtimeService)) throw new Error(`The realtime service handed to the Postgres socket cannot serve it — it has no ${socketRealtimeMissing(realtimeService).join(", ")}. Every socket would fail on its first frame.`);
19822
20063
  const realtimeConfig = effectiveConfig().realtime;
19823
20064
  const maxSocketsPerUser = realtimeConfig?.maxSocketsPerUser !== void 0 ? parseMaxSocketsPerUser(realtimeConfig.maxSocketsPerUser, "realtime.maxSocketsPerUser") : void 0;
@@ -19891,6 +20132,7 @@ async function openRebase(options) {
19891
20132
  const hint = extractPgError(error)?.hint;
19892
20133
  throw new Error(`Cannot connect to PostgreSQL at ${parseHostInfo(connectionString ?? "")}: ${deepestErrorMessage(error) ?? "the connection failed"}.${hint ? ` ${hint}` : ""}`, { cause: error });
19893
20134
  }
20135
+ const ownKey = options.dataSourceKey ?? DEFAULT_DATA_SOURCE_KEY;
19894
20136
  let opened;
19895
20137
  try {
19896
20138
  opened = await openPostgresDriver({
@@ -19898,7 +20140,7 @@ async function openRebase(options) {
19898
20140
  connectionString
19899
20141
  }, {
19900
20142
  collections: options.collections,
19901
- dataSourceKey: options.dataSourceKey ?? DEFAULT_DATA_SOURCE_KEY,
20143
+ dataSourceKey: ownKey,
19902
20144
  introspectCollections: options.collections.length === 0,
19903
20145
  provision: false,
19904
20146
  realtime: false,
@@ -19913,6 +20155,7 @@ async function openRebase(options) {
19913
20155
  }
19914
20156
  const driver = opened.internals.driver;
19915
20157
  const registry = opened.internals.registry;
20158
+ if (options.callbacks) registry.setGlobalCallbacks(options.callbacks);
19916
20159
  if (opened.internals.rlsEnforcement) logger.debug(opened.internals.rlsEnforcement);
19917
20160
  const collections = opened.collections ?? options.collections;
19918
20161
  if (collections.some((collection) => collection.history)) try {
@@ -19937,10 +20180,13 @@ async function openRebase(options) {
19937
20180
  collections,
19938
20181
  async sql(query, sqlOptions) {
19939
20182
  if (!isSQLAdmin(admin)) throw new Error("This driver cannot run raw SQL.");
20183
+ if (sqlOptions?.database !== void 0) {
20184
+ const asked = resourceKeyOf(sqlOptions.database);
20185
+ if (asked !== ownKey) throw new Error(`openRebase opened the "${ownKey}" data source only, so it cannot run SQL on "${asked}". Open that one with its own openRebase({ databaseUrl, dataSourceKey: "${asked}" }).`);
20186
+ }
19940
20187
  return admin.executeSql(query, sqlOptions && {
19941
20188
  role: sqlOptions.role,
19942
- params: sqlOptions.params,
19943
- ...sqlOptions.database !== void 0 ? { database: resourceKeyOf(sqlOptions.database) } : {}
20189
+ params: sqlOptions.params
19944
20190
  });
19945
20191
  },
19946
20192
  async withAuth(user) {
@@ -19956,4 +20202,4 @@ async function openRebase(options) {
19956
20202
  //#endregion
19957
20203
  export { AuthenticatedPostgresBackendDriver as $, budgetSendBuffers as A, mfaFactors as B, PG_NOTIFY_MAX_PAYLOAD_BYTES as C, MemoryChannelBus as D, parseChannelBusPayload as E, createAuthSchema as F, recoveryCodesRelations as G, passwordResetTokens as H, magicLinkTokens as I, userIdentities as J, refreshTokens as K, magicLinkTokensRelations as L, terminateIfBacklogged as M, assertReadRequestReadable as N, frameByteLength as O, appConfig as P, usersSchema as Q, mfaChallenges as R, DEFAULT_BATCH_WINDOW_MS as S, parseChannelBusFrame as T, passwordResetTokensRelations as U, mfaFactorsRelations as V, recoveryCodes as W, users as X, userIdentitiesRelations as Y, usersRelations as Z, parseRealtimeTuning as _, listedColumnKeys as _t, openPostgresDriver as a, isRoleSwitchingOptedOut as at, resolveChannelBusSetting as b, resolveSourceUrls as c, SqlResultTooLargeError as ct, DEFAULT_RECOUNT_MIN_ROWS as d, DrizzleConditionBuilder as dt, CONNECTION_OWNER as et, PostgresRealtimeProvider as f, PostgresConditionBuilder as ft, parseMaxSubscriptionsPerSocket as g, joinColumn as gt, parseMaxSocketsPerUser as h, getUnknownFilterFieldsMode as ht, isScaffoldedLocalDatabase as i, effectiveSqlRole as it, reapSilentSockets as j, isChannelBusInstance$1 as k, PostgresCollectionRegistry as l, isNestedPath as lt, packEntityChanges as m, escapeLikePattern as mt, createPostgresBootstrapper as n, RoleSwitchUnavailableError as nt, postgresDriverConfigFromEnv as o, sqlRoleRanAs as ot, RealtimeService as p, configureUnknownFilterFields as pt, refreshTokensRelations as q, describeSchemaDriftCause as r, branchParentDatabase as rt, resolveDriftCheckName as s, SQL_RESULT_BYTE_BUDGET as st, openRebase as t, PostgresBackendDriver as tt, DEFAULT_MAX_SUBSCRIPTIONS_PER_SOCKET as u, resolveNestedPath as ut, refetchConcurrencyFor as v, PostgresChannelBus as w, CHANNEL_BUS_NOTIFY_CHANNEL as x, createChannelBus as y, mfaChallengesRelations as z };
19958
20204
 
19959
- //# sourceMappingURL=open-rebase-EmqVmS14.js.map
20205
+ //# sourceMappingURL=open-rebase-C8jQ6s5a.js.map