@rebasepro/server-postgres 0.13.0 → 0.13.1-canary.g1822133

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 (117) hide show
  1. package/dist/PostgresBackendDriver.d.ts +48 -1
  2. package/dist/PostgresBootstrapper.d.ts +26 -0
  3. package/dist/auth/services.d.ts +21 -0
  4. package/dist/{src-DlPBctw_.js → auth-users-columns-BfQHf9JE.js} +1222 -86
  5. package/dist/auth-users-columns-BfQHf9JE.js.map +1 -0
  6. package/dist/{backup-service-CD8o_1Sl.js → backup-service-BH0Dzo_h.js} +2 -3
  7. package/dist/{backup-service-CD8o_1Sl.js.map → backup-service-BH0Dzo_h.js.map} +1 -1
  8. package/dist/cli-helpers.d.ts +57 -1
  9. package/dist/cli-output.d.ts +34 -0
  10. package/dist/data-transformer.d.ts +7 -2
  11. package/dist/data_driver-ULAyJEi9.js +193 -0
  12. package/dist/data_driver-ULAyJEi9.js.map +1 -0
  13. package/dist/ensure-collection-policies-8vuu-n4r.js +124 -0
  14. package/dist/ensure-collection-policies-8vuu-n4r.js.map +1 -0
  15. package/dist/{ensure-collection-tables-CBQdOETu.js → ensure-collection-tables-CbvaGuVn.js} +170 -20
  16. package/dist/ensure-collection-tables-CbvaGuVn.js.map +1 -0
  17. package/dist/index.es.js +1815 -1023
  18. package/dist/index.es.js.map +1 -1
  19. package/dist/rls-bootstrap-sql-69hYT8nr.js +244 -0
  20. package/dist/rls-bootstrap-sql-69hYT8nr.js.map +1 -0
  21. package/dist/rls-enforcement-BJ_3wxwg.js +425 -0
  22. package/dist/rls-enforcement-BJ_3wxwg.js.map +1 -0
  23. package/dist/schema/auth-schema.d.ts +102 -0
  24. package/dist/schema/auth-users-columns.d.ts +97 -0
  25. package/dist/schema/doctor-policy-checks.d.ts +28 -0
  26. package/dist/schema/doctor.d.ts +41 -25
  27. package/dist/schema/ensure-collection-policies.d.ts +33 -9
  28. package/dist/schema/ensure-collection-tables.d.ts +61 -7
  29. package/dist/schema/generate-drizzle-schema-logic.d.ts +10 -2
  30. package/dist/schema/generate-postgres-ddl-logic.d.ts +53 -5
  31. package/dist/schema/generated-schema-staleness.d.ts +39 -0
  32. package/dist/schema/introspect-db-inference.d.ts +8 -1
  33. package/dist/schema/introspect-db-logic.d.ts +49 -0
  34. package/dist/schema/introspect-db-project.d.ts +21 -0
  35. package/dist/schema/rls-bootstrap-sql.d.ts +135 -0
  36. package/dist/schema/search-column.d.ts +248 -0
  37. package/dist/security/policy-drift.d.ts +34 -0
  38. package/dist/security/rls-enforcement.d.ts +61 -5
  39. package/dist/services/FetchService.d.ts +34 -7
  40. package/dist/services/PersistService.d.ts +21 -17
  41. package/dist/services/RelationService.d.ts +9 -57
  42. package/dist/services/RelationWriteService.d.ts +82 -0
  43. package/dist/services/collection-helpers.d.ts +42 -0
  44. package/dist/services/dataService.d.ts +5 -0
  45. package/dist/services/junction-writes.d.ts +82 -0
  46. package/dist/services/realtimeService.d.ts +164 -23
  47. package/dist/services/write-denial.d.ts +36 -0
  48. package/dist/{src-DoU9yPqq.js → src-DCdn3Val.js} +124 -3
  49. package/dist/src-DCdn3Val.js.map +1 -0
  50. package/dist/utils/drizzle-conditions.d.ts +124 -2
  51. package/dist/{websocket-B2LsrINK.js → websocket-C8ZqVBiV.js} +75 -18
  52. package/dist/websocket-C8ZqVBiV.js.map +1 -0
  53. package/package.json +9 -8
  54. package/src/PostgresBackendDriver.ts +172 -6
  55. package/src/PostgresBootstrapper.ts +136 -11
  56. package/src/auth/ensure-tables.ts +212 -91
  57. package/src/auth/services.ts +82 -5
  58. package/src/backup/backup-cli.ts +59 -57
  59. package/src/cli-errors.ts +6 -6
  60. package/src/cli-helpers.ts +132 -13
  61. package/src/cli-output.ts +43 -0
  62. package/src/cli.ts +371 -161
  63. package/src/collections/buildRegistry.ts +3 -1
  64. package/src/collections/validate-relations.ts +124 -17
  65. package/src/data-transformer.ts +142 -28
  66. package/src/history/ensure-history-table.ts +9 -2
  67. package/src/schema/auth-schema.ts +17 -1
  68. package/src/schema/auth-users-columns.ts +131 -0
  69. package/src/schema/doctor-cli.ts +14 -65
  70. package/src/schema/doctor-policy-checks.ts +105 -0
  71. package/src/schema/doctor.ts +156 -77
  72. package/src/schema/ensure-collection-policies.ts +99 -6
  73. package/src/schema/ensure-collection-tables.ts +374 -32
  74. package/src/schema/generate-drizzle-schema-logic.ts +152 -66
  75. package/src/schema/generate-drizzle-schema.ts +11 -10
  76. package/src/schema/generate-postgres-ddl-logic.ts +294 -16
  77. package/src/schema/generate-postgres-ddl.ts +38 -14
  78. package/src/schema/generated-schema-staleness.ts +171 -0
  79. package/src/schema/introspect-db-inference.ts +9 -2
  80. package/src/schema/introspect-db-logic.ts +251 -75
  81. package/src/schema/introspect-db-project.ts +78 -0
  82. package/src/schema/introspect-db.ts +42 -25
  83. package/src/schema/introspect-runtime.ts +14 -2
  84. package/src/schema/non-sql-collections.test.ts +131 -0
  85. package/src/schema/rls-bootstrap-sql.ts +288 -0
  86. package/src/schema/search-column.ts +643 -0
  87. package/src/security/anonymous-grants.test.ts +4 -2
  88. package/src/security/policy-drift.test.ts +104 -3
  89. package/src/security/policy-drift.ts +129 -7
  90. package/src/security/rls-enforcement.ts +150 -7
  91. package/src/services/BranchService.ts +5 -0
  92. package/src/services/FetchService.ts +253 -115
  93. package/src/services/PersistService.ts +82 -43
  94. package/src/services/RelationService.ts +37 -696
  95. package/src/services/RelationWriteService.ts +653 -0
  96. package/src/services/cdc/trigger-cdc.ts +5 -1
  97. package/src/services/channel-history.ts +14 -0
  98. package/src/services/channel-presence.ts +13 -0
  99. package/src/services/collection-helpers.ts +89 -4
  100. package/src/services/dataService.ts +5 -0
  101. package/src/services/junction-writes.ts +295 -0
  102. package/src/services/pg-notify-listener.ts +1 -1
  103. package/src/services/realtimeService.ts +382 -118
  104. package/src/services/write-denial.ts +55 -0
  105. package/src/utils/drizzle-conditions.ts +433 -35
  106. package/src/utils/pg-error-utils.ts +8 -3
  107. package/src/websocket.ts +113 -16
  108. package/dist/ensure-collection-policies-ViG8XiPn.js +0 -57
  109. package/dist/ensure-collection-policies-ViG8XiPn.js.map +0 -1
  110. package/dist/ensure-collection-tables-CBQdOETu.js.map +0 -1
  111. package/dist/policy-CeA1JcxP.js +0 -105
  112. package/dist/policy-CeA1JcxP.js.map +0 -1
  113. package/dist/schema/auth-bootstrap-sql.d.ts +0 -24
  114. package/dist/src-DlPBctw_.js.map +0 -1
  115. package/dist/src-DoU9yPqq.js.map +0 -1
  116. package/dist/websocket-B2LsrINK.js.map +0 -1
  117. package/src/schema/auth-bootstrap-sql.ts +0 -47
@@ -37,7 +37,7 @@ import { ensureHistoryTableExists } from "./history/ensure-history-table";
37
37
  import { patchPgArrayNullSafety } from "./utils/pg-array-null-patch";
38
38
  import { buildCollectionsFromSchema, introspectSchema, readRlsStatus } from "./schema/introspect-runtime";
39
39
  import { buildDrizzleTablesFromSchema, buildDrizzleRelationsFromSchema } from "./schema/dynamic-tables";
40
- import { detectConnectionPosture, ensureAppRole, validatePolicyPgRoles, warnOnAnonymousGrants, REBASE_USER_ROLE, type RawSqlRunner } from "./security/rls-enforcement";
40
+ import { detectConnectionPosture, ensureAppRole, validatePolicyPgRoles, warnOnAnonymousGrants, warnOnLegacyRlsFunctions, warnOnRoleSchemaCollision, REBASE_USER_ROLE, type RawSqlRunner } from "./security/rls-enforcement";
41
41
  import { provisionTriggerCdc, type CdcTableRef } from "./services/cdc/trigger-cdc";
42
42
  import { collectJunctionLinks } from "./services/cdc/junction-tables";
43
43
  import { createChannelBus, resolveChannelBusSetting } from "./services/channel-bus";
@@ -137,6 +137,50 @@ export function resolveDriftCheckName(
137
137
  ?? col.slug;
138
138
  }
139
139
 
140
+ /**
141
+ * Is this the local database `rebase init` scaffolds — i.e. the one case where
142
+ * "you are connected as a superuser" is not news?
143
+ *
144
+ * The scaffold's own `docker-compose.yml` sets `POSTGRES_USER: rebase_app`,
145
+ * which makes that role the cluster superuser, so the superuser advisory below
146
+ * was the only WARN a brand-new project ever saw and it was about a decision
147
+ * the tool had made for the developer.
148
+ *
149
+ * Of the two available fixes — provision a non-superuser table-owner role in
150
+ * the scaffold, or recognise the local shape and stay quiet — this is the
151
+ * second, because the first breaks the scaffold it is meant to improve: a
152
+ * non-superuser owner cannot `CREATE EXTENSION` (search collections need
153
+ * `pg_trgm`/`unaccent`, applied by `rebase db push` and again by the boot
154
+ * schema-ensure), so the very first `pnpm run db:push` on a scaffolded project
155
+ * with a search block would fail. Trading a working first run for a quieter log
156
+ * line is the wrong trade.
157
+ *
158
+ * The condition is deliberately narrow — a *non-production* process talking to
159
+ * a database on the loopback interface. A genuine production superuser
160
+ * connection still warns, and so does a non-production process pointed at a
161
+ * remote database (the usual "my dev machine writes to staging" mistake, where
162
+ * the advisory is exactly right). NODE_ENV alone would not do: the scaffold
163
+ * ships `NODE_ENV=development` and some deployments inherit it.
164
+ */
165
+ export function isScaffoldedLocalDatabase(connectionString: string | undefined): boolean {
166
+ if (process.env.NODE_ENV === "production") return false;
167
+ if (!connectionString) return false;
168
+ let host: string;
169
+ try {
170
+ host = new URL(connectionString).hostname;
171
+ } catch {
172
+ return false;
173
+ }
174
+ // `new URL` keeps IPv6 literals in brackets.
175
+ const bare = host.replace(/^\[|\]$/g, "").toLowerCase();
176
+ return bare === "localhost"
177
+ || bare === "::1"
178
+ || bare === "0.0.0.0"
179
+ || bare === ""
180
+ || /^127\./.test(bare)
181
+ || bare.endsWith(".localhost");
182
+ }
183
+
140
184
  /**
141
185
  * Default PostgreSQL bootstrapper.
142
186
  *
@@ -364,29 +408,46 @@ export function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): Back
364
408
  // bypasses RLS. Detect the posture and, when privileged, provision
365
409
  // the restricted `rebase_user` role and route authenticated
366
410
  // requests (reads AND writes) through it. The server context (base
367
- // driver / dataAsAdmin / auth flows) stays on the owner connection
368
- // and bypasses. Default-on: a privileged connection that cannot be
411
+ // driver / auth flows / `rebase.sql`) stays on the owner connection
412
+ // and bypasses; `rebase.dataAsAdmin` does NOT it is scoped with
413
+ // `withAuth({ uid: "service", roles: ["admin"] })` at boot, so it
414
+ // runs as `rebase_user` and its policies are evaluated.
415
+ // Default-on: a privileged connection that cannot be
369
416
  // isolated fails the boot rather than serving unenforced requests.
370
417
  {
371
418
  const runSql: RawSqlRunner = async (text) => {
372
419
  const res = await schemaAwareDb.execute(sql.raw(text));
373
420
  return (res.rows ?? []) as Record<string, unknown>[];
374
421
  };
422
+ // Said before anything else touches the schema: if the role and
423
+ // a schema share a name, unqualified SQL from any tool that does
424
+ // not pin `search_path` has been landing in the wrong place, and
425
+ // that is worth knowing before reading the drift report below.
426
+ await warnOnRoleSchemaCollision(runSql);
427
+
375
428
  const posture = await detectConnectionPosture(runSql);
376
429
  if (posture.privileged) {
377
430
  const collectionSchemas = registry.getCollections()
378
431
  .map((c) => (c as { schema?: string }).schema)
379
432
  .filter((s): s is string => typeof s === "string");
380
- await ensureAppRole(runSql, ["public", "rebase", "auth", ...collectionSchemas]);
433
+ // `auth` is deliberately absent: the RLS helpers moved into
434
+ // `rebase`, and granting USAGE on a schema Rebase does not
435
+ // own would, on a Supabase database, hand the end-user role
436
+ // access to theirs.
437
+ await ensureAppRole(runSql, ["public", "rebase", ...collectionSchemas]);
381
438
  driver.rlsUserRole = REBASE_USER_ROLE;
382
439
  realtimeService.rlsUserRole = REBASE_USER_ROLE;
383
440
  logger.info(`🔐 RLS enforcement active: authenticated requests run as "${REBASE_USER_ROLE}" (connection "${posture.role}" bypasses RLS: ${posture.superuser ? "superuser" : posture.bypassRLS ? "BYPASSRLS" : "table owner"})`);
384
441
  if (posture.superuser || posture.bypassRLS) {
385
- logger.warn(
386
- `⚠️ The database connection runs as ${posture.superuser ? "a superuser" : "a BYPASSRLS role"} ("${posture.role}"). ` +
442
+ const message =
443
+ `The database connection runs as ${posture.superuser ? "a superuser" : "a BYPASSRLS role"} ("${posture.role}"). ` +
387
444
  `User requests are isolated via SET LOCAL ROLE, but connect as a non-superuser ` +
388
- `table-owner role in production so the server/owner context is least-privilege.`
389
- );
445
+ `table-owner role in production so the server/owner context is least-privilege.`;
446
+ if (isScaffoldedLocalDatabase(pgConfig.connectionString)) {
447
+ logger.debug(`🔐 ${message}`);
448
+ } else {
449
+ logger.warn(`⚠️ ${message}`);
450
+ }
390
451
  }
391
452
  } else {
392
453
  logger.info(`🔐 RLS enforcement: connection role "${posture.role}" is subject to RLS natively; no role switch needed.`);
@@ -406,6 +467,11 @@ export function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): Back
406
467
  // a rule that reads as "signed in only" but is true for every
407
468
  // caller grants the data away rather than hiding it.
408
469
  warnOnAnonymousGrants(registry.getCollections() as never);
470
+
471
+ // Raw policy SQL written against the pre-1.0 helper schema. It
472
+ // is rewritten on compile, so this is the only place the project
473
+ // is ever told the spelling moved.
474
+ warnOnLegacyRlsFunctions(registry.getCollections() as never);
409
475
  }
410
476
 
411
477
  // Ensure branch metadata table exists when branching is available
@@ -820,8 +886,15 @@ schemaHealthCheck: () => probeAuthSchema(db, resolveAuthSchema(authCollection))
820
886
  );
821
887
  for (const failure of plan.failures) {
822
888
  logger.warn(
823
- `🔗 [schema] Could not add foreign key "${failure.target}" the column exists and the ` +
824
- `collection still serves, but rows are not policed by this constraint: ${failure.error}`
889
+ failure.kind === "comment-column"
890
+ // The stamp records which `search` block the generated
891
+ // column was built from. Without it the next boot cannot
892
+ // tell a changed block from an unchanged one and adopts
893
+ // the column again instead of refusing.
894
+ ? `🔍 [schema] Could not record the search fingerprint on "${failure.target}" — search works, ` +
895
+ `but a later change to the \`search\` block will not be detected: ${failure.error}`
896
+ : `🔗 [schema] Could not add foreign key "${failure.target}" — the column exists and the ` +
897
+ `collection still serves, but rows are not policed by this constraint: ${failure.error}`
825
898
  );
826
899
  }
827
900
  return { applied: plan.actions.length - plan.failures.length };
@@ -867,9 +940,61 @@ schemaHealthCheck: () => probeAuthSchema(db, resolveAuthSchema(authCollection))
867
940
  }
868
941
  for (const failure of outcome.failures) {
869
942
  logger.warn(
870
- `🔐 [rls] Could not fully apply policies to "${failure.table}" — it stays locked (denies) until this is resolved: ${failure.error}`
943
+ `🔐 [rls] Could not fully apply policies to "${failure.table}" — RLS is on, so it denies until this is resolved: ${failure.error}`
871
944
  );
872
945
  }
946
+
947
+ // RLS could not be turned on at all. The schema-wide grant to the
948
+ // user role has already run by this point, so without the revoke
949
+ // below the table is readable and writable by every authenticated
950
+ // request with no row filtering — the one state this boot path must
951
+ // never leave behind, and the one it used to describe as "locked".
952
+ const unrevoked = outcome.unsecured.filter(u => !u.grantWithdrawn);
953
+ for (const u of outcome.unsecured.filter(u => u.grantWithdrawn)) {
954
+ logger.error(
955
+ `🔐 [rls] Could not enable row-level security on "${u.table}": ${u.error}. ` +
956
+ `Its privileges have been revoked from ${REBASE_USER_ROLE}, so the table is ` +
957
+ "unreachable rather than unprotected. Reads and writes to that collection will " +
958
+ "fail until RLS can be enabled."
959
+ );
960
+ }
961
+ if (unrevoked.length > 0) {
962
+ // Neither securable nor closable. Serving here would mean
963
+ // handing every authenticated caller unfiltered access, so the
964
+ // boot fails instead — this is the one failure that is not
965
+ // survivable per-table.
966
+ throw new Error(
967
+ "Refusing to start: row-level security could not be enabled on " +
968
+ unrevoked.map(u => `"${u.table}" (${u.error})`).join(", ") +
969
+ `, and the privileges granted to ${REBASE_USER_ROLE} could not be revoked either. ` +
970
+ "The table would be served with no row filtering. Fix the database permissions " +
971
+ "(the connection role must own these tables, or be able to ALTER them) and boot again."
972
+ );
973
+ }
974
+
975
+ // Retire the pre-1.0 `auth` schema now that the policies above no
976
+ // longer call into it. Deliberately after, and deliberately quiet:
977
+ // Postgres refuses to drop a function an RLS policy still
978
+ // references, so on a database where some table has not been
979
+ // recompiled yet this is expected to fail and succeed on a later
980
+ // boot. See DROP_LEGACY_AUTH_SCHEMA_SQL for the guards that keep it
981
+ // off a Supabase `auth` schema.
982
+ try {
983
+ const { dropLegacyAuthSchema } = await import("./schema/rls-bootstrap-sql");
984
+ await dropLegacyAuthSchema(
985
+ async (text) => {
986
+ const res = await internals.db.execute(sql.raw(text));
987
+ return (res.rows ?? []) as Record<string, unknown>[];
988
+ },
989
+ { info: (m) => logger.info(m), warn: (m) => logger.warn(m) }
990
+ );
991
+ } catch (err) {
992
+ logger.info(
993
+ "Left the legacy `auth` schema in place: " +
994
+ (err instanceof Error ? err.message : String(err))
995
+ );
996
+ }
997
+
873
998
  return { applied: outcome.policiesApplied };
874
999
  },
875
1000