@rebasepro/server-postgres 0.17.1 → 0.17.2-canary.g4d21a53

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 (35) hide show
  1. package/dist/{backup-service-BtgHxfFm.js → backup-service-DCk7KhhL.js} +3 -3
  2. package/dist/{backup-service-BtgHxfFm.js.map → backup-service-DCk7KhhL.js.map} +1 -1
  3. package/dist/cli-helpers.d.ts +26 -14
  4. package/dist/{collection-index-DxJBvVTH.js → collection-index-BRUg10H5.js} +2 -2
  5. package/dist/{collection-index-DxJBvVTH.js.map → collection-index-BRUg10H5.js.map} +1 -1
  6. package/dist/{ensure-collection-policies-BHk4TRuf.js → ensure-collection-policies-UCqgv_8c.js} +4 -4
  7. package/dist/{ensure-collection-policies-BHk4TRuf.js.map → ensure-collection-policies-UCqgv_8c.js.map} +1 -1
  8. package/dist/{ensure-collection-tables-DMjOkeRy.js → ensure-collection-tables-DgVixhX3.js} +256 -38
  9. package/dist/ensure-collection-tables-DgVixhX3.js.map +1 -0
  10. package/dist/index.es.js +12 -12
  11. package/dist/{rls-bootstrap-sql-DNzaWd4C.js → rls-bootstrap-sql-B5C9LoJ6.js} +2 -2
  12. package/dist/{rls-bootstrap-sql-DNzaWd4C.js.map → rls-bootstrap-sql-B5C9LoJ6.js.map} +1 -1
  13. package/dist/{rls-enforcement-C1RJ1uI2.js → rls-enforcement-DvAbL9YJ.js} +3 -3
  14. package/dist/{rls-enforcement-C1RJ1uI2.js.map → rls-enforcement-DvAbL9YJ.js.map} +1 -1
  15. package/dist/schema/atlas-argv.d.ts +58 -0
  16. package/dist/schema/carved-out-migration.d.ts +65 -0
  17. package/dist/schema/ensure-collection-tables.d.ts +13 -1
  18. package/dist/schema/generate-postgres-ddl-logic.d.ts +57 -5
  19. package/dist/schema/vector-index.d.ts +120 -0
  20. package/dist/{src-DiDgtX8P.js → src-DiB5RP2Z.js} +33 -3
  21. package/dist/{src-DiDgtX8P.js.map → src-DiB5RP2Z.js.map} +1 -1
  22. package/dist/{websocket-g1Ji7m4o.js → websocket-BZ4H5wUz.js} +19 -9
  23. package/dist/websocket-BZ4H5wUz.js.map +1 -0
  24. package/package.json +6 -6
  25. package/src/cli-helpers.ts +65 -34
  26. package/src/cli.ts +168 -71
  27. package/src/schema/atlas-argv.ts +94 -0
  28. package/src/schema/carved-out-migration.ts +404 -0
  29. package/src/schema/ensure-collection-tables.ts +59 -32
  30. package/src/schema/generate-postgres-ddl-logic.ts +147 -21
  31. package/src/schema/generate-postgres-ddl.ts +59 -6
  32. package/src/schema/generate-schema-commit.ts +31 -6
  33. package/src/schema/vector-index.ts +213 -0
  34. package/dist/ensure-collection-tables-DMjOkeRy.js.map +0 -1
  35. package/dist/websocket-g1Ji7m4o.js.map +0 -1
package/dist/index.es.js CHANGED
@@ -2,13 +2,13 @@ import { createRequire as __createRequire } from "module";
2
2
  import process from "process";
3
3
  __createRequire(import.meta.url);
4
4
  import { a as createReadReplicaConnection, c as poolMaxCeiling, i as createPostgresDatabaseConnection, o as guardPoolAgainstDirtyRelease, r as createDirectDatabaseConnection, s as pinSearchPath, t as cappedPoolMax } from "./connection-GOKU3Hu5.js";
5
- import { C as getTableName$1, F as firstFreeKey, I as generateForeignKeyName, J as resolveClientListLimit, L as legacyForeignKeyName, T as resolveCollectionRelations, U as camelCase, W as toSnakeCase, Z as isManyToMany, c as parseOrderBySpecStrict, l as CollectionRegistry, o as OrderBySpecError, q as ListLimitError, z as toWireKey } from "./collection-index-DxJBvVTH.js";
6
- import { l as isRelationalCollectionConfig } from "./src-DiDgtX8P.js";
7
- import { d as authUsersColumnSql, u as AUTH_USERS_COLUMNS } from "./ensure-collection-tables-DMjOkeRy.js";
8
- import { C as deriveRowAddress, E as buildPropertyCallbacks, S as getUnknownFilterFieldsMode, T as warnOnKeysTheAdminCannotResolve, _ as sanitizeErrorForClient, a as AuthenticatedPostgresBackendDriver, b as configureUnknownFilterFields, c as RoleSwitchUnavailableError, d as generateSchema, f as getDrizzleColumn, g as extractPgError, h as classifyConnectFailure, l as effectiveSqlRole, m as DataService, n as PUBLIC_TYPES, o as CONNECTION_OWNER, p as BranchService, r as createPostgresWebSocket, s as PostgresBackendDriver, t as ADMIN_ONLY_TYPES, u as isRoleSwitchingOptedOut, v as DrizzleConditionBuilder, w as getPrimaryKeys, x as escapeLikePattern, y as PostgresConditionBuilder } from "./websocket-g1Ji7m4o.js";
9
- import { a as warnOnAnonymousGrants, c as REBASE_USER_ROLE, i as validatePolicyPgRoles, l as revokeInternalTableAccess, n as detectConnectionPosture, o as warnOnLegacyRlsFunctions, r as ensureAppRole, s as warnOnRoleSchemaCollision, t as applyAuthContext, u as revokeInternalTableSql } from "./rls-enforcement-C1RJ1uI2.js";
10
- import { A as parseDbNameFromUrl, C as buildRowSecurityPgOptions, D as joinStorageKey, E as globalsFileForDump, F as withDatabaseName, M as resolveConnectionString, N as serverVersionNumToMajor, O as parseBackupDestination, P as splitGlobalsStatements, S as buildPgRestoreListArgs, T as diagnoseRowSecurityDumpFailure, _ as selectBackupsToPrune, a as detectToolMajor, b as buildPgDumpallGlobalsArgs, c as getServerVersionMajor, d as pruneBackups, f as resolvePgBinary, g as require_source, h as validateDump, i as createDump, j as parsePgToolMajor, k as parseBackupTimestamp, l as listBackups, m as uploadBackup, n as applyGlobals, o as discardPartialDump, p as restoreDump, s as ensureDatabaseExists, t as BackupToolError, u as preflight, v as buildBackupFilename, w as checkToolServerCompatibility, x as buildPgRestoreArgs, y as buildPgDumpArgs } from "./backup-service-BtgHxfFm.js";
11
- import { t as RLS_BOOTSTRAP_STATEMENTS } from "./rls-bootstrap-sql-DNzaWd4C.js";
5
+ import { C as getTableName$1, F as firstFreeKey, I as generateForeignKeyName, J as resolveClientListLimit, L as legacyForeignKeyName, T as resolveCollectionRelations, U as camelCase, W as toSnakeCase, Z as isManyToMany, c as parseOrderBySpecStrict, l as CollectionRegistry, o as OrderBySpecError, q as ListLimitError, z as toWireKey } from "./collection-index-BRUg10H5.js";
6
+ import { u as isRelationalCollectionConfig } from "./src-DiB5RP2Z.js";
7
+ import { d as AUTH_USERS_COLUMNS, f as authUsersColumnSql } from "./ensure-collection-tables-DgVixhX3.js";
8
+ import { C as deriveRowAddress, E as buildPropertyCallbacks, S as getUnknownFilterFieldsMode, T as warnOnKeysTheAdminCannotResolve, _ as sanitizeErrorForClient, a as AuthenticatedPostgresBackendDriver, b as configureUnknownFilterFields, c as RoleSwitchUnavailableError, d as generateSchema, f as getDrizzleColumn, g as extractPgError, h as classifyConnectFailure, l as effectiveSqlRole, m as DataService, n as PUBLIC_TYPES, o as CONNECTION_OWNER, p as BranchService, r as createPostgresWebSocket, s as PostgresBackendDriver, t as ADMIN_ONLY_TYPES, u as isRoleSwitchingOptedOut, v as DrizzleConditionBuilder, w as getPrimaryKeys, x as escapeLikePattern, y as PostgresConditionBuilder } from "./websocket-BZ4H5wUz.js";
9
+ import { a as warnOnAnonymousGrants, c as REBASE_USER_ROLE, i as validatePolicyPgRoles, l as revokeInternalTableAccess, n as detectConnectionPosture, o as warnOnLegacyRlsFunctions, r as ensureAppRole, s as warnOnRoleSchemaCollision, t as applyAuthContext, u as revokeInternalTableSql } from "./rls-enforcement-DvAbL9YJ.js";
10
+ import { A as parseDbNameFromUrl, C as buildRowSecurityPgOptions, D as joinStorageKey, E as globalsFileForDump, F as withDatabaseName, M as resolveConnectionString, N as serverVersionNumToMajor, O as parseBackupDestination, P as splitGlobalsStatements, S as buildPgRestoreListArgs, T as diagnoseRowSecurityDumpFailure, _ as selectBackupsToPrune, a as detectToolMajor, b as buildPgDumpallGlobalsArgs, c as getServerVersionMajor, d as pruneBackups, f as resolvePgBinary, g as require_source, h as validateDump, i as createDump, j as parsePgToolMajor, k as parseBackupTimestamp, l as listBackups, m as uploadBackup, n as applyGlobals, o as discardPartialDump, p as restoreDump, s as ensureDatabaseExists, t as BackupToolError, u as preflight, v as buildBackupFilename, w as checkToolServerCompatibility, x as buildPgRestoreArgs, y as buildPgDumpArgs } from "./backup-service-DCk7KhhL.js";
11
+ import { t as RLS_BOOTSTRAP_STATEMENTS } from "./rls-bootstrap-sql-B5C9LoJ6.js";
12
12
  import { Client, Pool } from "pg";
13
13
  import { drizzle } from "drizzle-orm/node-postgres";
14
14
  import { ApiError, createDdlBootstrapper, createEmailService, loadCollectionsFromDirectory, logger } from "@rebasepro/server";
@@ -3452,7 +3452,7 @@ function createBackupCron(config) {
3452
3452
  enabled: config.enabled ?? true,
3453
3453
  timeoutSeconds: 3600,
3454
3454
  async handler({ log }) {
3455
- const { createDump, pruneBackups, uploadBackup, validateDump } = await import("./backup-service-BtgHxfFm.js").then((n) => n.r);
3455
+ const { createDump, pruneBackups, uploadBackup, validateDump } = await import("./backup-service-DCk7KhhL.js").then((n) => n.r);
3456
3456
  const { destination } = config;
3457
3457
  if (destination.kind !== "local" && !config.storage) throw new Error(`Backup destination is ${destination.kind} but no storage controller was provided. Pass the backend's configured StorageController to createBackupCron({ storage }).`);
3458
3458
  log(`Starting backup of "${dbName}"…`);
@@ -6892,7 +6892,7 @@ function createPostgresBootstrapper(pgConfig) {
6892
6892
  * proved it can bootstrap with.
6893
6893
  */
6894
6894
  async ensureCollectionSchema(collections, driverResult, log) {
6895
- const { ensureCollectionTables } = await import("./ensure-collection-tables-DMjOkeRy.js").then((n) => n.t);
6895
+ const { ensureCollectionTables } = await import("./ensure-collection-tables-DgVixhX3.js").then((n) => n.t);
6896
6896
  const plan = await ensureCollectionTables(provisioningQueryable(driverResult), collections, log);
6897
6897
  for (const failure of plan.failures) logger.warn(failure.kind === "comment-column" ? `🔍 [schema] Could not record the search fingerprint on "${failure.target}" — search works, but a later change to the \`search\` block will not be detected: ${failure.error}` : `🔗 [schema] Could not add foreign key "${failure.target}" — the column exists and the collection still serves, but rows are not policed by this constraint: ${failure.error}`);
6898
6898
  return { applied: plan.actions.length - plan.failures.length };
@@ -6913,7 +6913,7 @@ function createPostgresBootstrapper(pgConfig) {
6913
6913
  * stays RLS-enabled, so it denies rather than leaks.
6914
6914
  */
6915
6915
  async ensureCollectionPolicies(collections, driverResult, log) {
6916
- const { ensureCollectionPolicies } = await import("./ensure-collection-policies-BHk4TRuf.js");
6916
+ const { ensureCollectionPolicies } = await import("./ensure-collection-policies-UCqgv_8c.js");
6917
6917
  const queryable = provisioningQueryable(driverResult);
6918
6918
  const outcome = await ensureCollectionPolicies(queryable, collections, log);
6919
6919
  for (const skip of outcome.skipped) logger.warn(`🔐 [rls] Policies not applied to "${skip.table}": ${skip.reason}`);
@@ -6922,7 +6922,7 @@ function createPostgresBootstrapper(pgConfig) {
6922
6922
  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}, so the table is unreachable rather than unprotected. Reads and writes to that collection will fail until RLS can be enabled.`);
6923
6923
  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} 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.`);
6924
6924
  try {
6925
- const { dropLegacyAuthSchema } = await import("./rls-bootstrap-sql-DNzaWd4C.js").then((n) => n.n);
6925
+ const { dropLegacyAuthSchema } = await import("./rls-bootstrap-sql-B5C9LoJ6.js").then((n) => n.n);
6926
6926
  await dropLegacyAuthSchema(async (text) => (await queryable.query(text)).rows, {
6927
6927
  info: (m) => logger.info(m),
6928
6928
  warn: (m) => logger.warn(m)
@@ -6956,7 +6956,7 @@ function createPostgresBootstrapper(pgConfig) {
6956
6956
  },
6957
6957
  mountRoutes(app, basePath, driverResult) {},
6958
6958
  async initializeWebsockets(server, realtimeService, driver, config, adapter) {
6959
- const { createPostgresWebSocket } = await import("./websocket-g1Ji7m4o.js").then((n) => n.i);
6959
+ const { createPostgresWebSocket } = await import("./websocket-BZ4H5wUz.js").then((n) => n.i);
6960
6960
  createPostgresWebSocket(server, realtimeService, driver, config, adapter);
6961
6961
  }
6962
6962
  };
@@ -2,7 +2,7 @@ import { createRequire as __createRequire } from "module";
2
2
  import "process";
3
3
  __createRequire(import.meta.url);
4
4
  import { u as __exportAll } from "./connection-GOKU3Hu5.js";
5
- import { n as REBASE_SCHEMA, t as LEGACY_RLS_SCHEMA } from "./src-DiDgtX8P.js";
5
+ import { n as LEGACY_RLS_SCHEMA, r as REBASE_SCHEMA } from "./src-DiB5RP2Z.js";
6
6
  //#region src/schema/rls-bootstrap-sql.ts
7
7
  var rls_bootstrap_sql_exports = /* @__PURE__ */ __exportAll({
8
8
  DROP_LEGACY_AUTH_SCHEMA_SQL: () => DROP_LEGACY_AUTH_SCHEMA_SQL,
@@ -241,4 +241,4 @@ async function dropLegacyAuthSchema(run, report) {
241
241
  //#endregion
242
242
  export { rls_bootstrap_sql_exports as n, RLS_BOOTSTRAP_STATEMENTS as t };
243
243
 
244
- //# sourceMappingURL=rls-bootstrap-sql-DNzaWd4C.js.map
244
+ //# sourceMappingURL=rls-bootstrap-sql-B5C9LoJ6.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"rls-bootstrap-sql-DNzaWd4C.js","names":[],"sources":["../src/schema/rls-bootstrap-sql.ts"],"sourcesContent":["import { LEGACY_RLS_SCHEMA, REBASE_SCHEMA } from \"@rebasepro/types\";\n\n/**\n * Canonical SQL bootstrap for the RLS helper functions.\n *\n * Generated RLS policies reference `rebase.uid()` / `rebase.roles()` /\n * `rebase.jwt()`, so any SQL stream that can contain policies must be\n * self-contained: it has to (re)create these helpers first. This matters for\n * the migration directory in particular — Atlas replays migrations against a\n * clean dev database where no out-of-band bootstrap has ever run, so a\n * migration carrying policies without this preamble fails with\n * \"function rebase.uid() does not exist\".\n *\n * Idempotent (`IF NOT EXISTS` / `OR REPLACE`) so it can be prepended to every\n * policies block and re-applied freely. The runtime boot path\n * (`auth/ensure-tables.ts`) creates the same functions under an advisory lock\n * for HMR-safety; keep the definitions in sync.\n *\n * ## Creating the `rebase` schema here is now safe, and required\n *\n * It deliberately did not, once. These functions lived in a schema called\n * `auth`, and the note here read: creating `rebase` would leak it into Atlas's\n * replayed migration state, and — absent from the desired `schema.sql` — Atlas\n * would then plan `DROP SCHEMA \"rebase\" CASCADE`, taking the auth tables with\n * it. That reasoning still holds; what changed is the second half of it. The\n * DDL generator now emits `CREATE SCHEMA IF NOT EXISTS \"rebase\"`\n * unconditionally, so the schema is always in the desired state and the diff is\n * empty. (It used to appear only when some collection happened to declare\n * `schema: \"rebase\"` — true for the scaffold's users collection, and not a\n * property anything guaranteed.) `db push` additionally excludes the whole\n * schema from the declarative apply.\n *\n * See `@rebasepro/types`' `rls-functions` for why the functions moved out of\n * `auth` at all: the short version is that the name was Supabase's, and\n * `CREATE OR REPLACE FUNCTION auth.uid() RETURNS text` cannot be applied over\n * Supabase's `RETURNS uuid` — Postgres refuses, and the refusal used to be\n * swallowed.\n */\n/**\n * The bootstrap as individual statements.\n *\n * Kept as an array because the two consumers need different shapes and only one\n * of them can take a multi-command string: the migration preamble is written to\n * a file and replayed by Atlas, but the boot path runs through drizzle, whose\n * node-postgres handle speaks the extended query protocol and rejects more than\n * one command per call. Splitting a joined string back apart on `$$;` would be\n * a parser for a problem that does not need one.\n */\nexport const RLS_BOOTSTRAP_STATEMENTS: readonly string[] = [\n `CREATE SCHEMA IF NOT EXISTS ${REBASE_SCHEMA}`,\n\n // Falls back to the pre-rename `app.user_id` so a database that has taken\n // the new schema but is still served by an older backend keeps resolving\n // the principal. Drop the COALESCE once no such deployment remains.\n `CREATE OR REPLACE FUNCTION ${REBASE_SCHEMA}.uid() RETURNS text AS $$\n SELECT COALESCE(\n NULLIF(current_setting('app.uid', true), ''),\n NULLIF(current_setting('app.user_id', true), '')\n );\n$$ LANGUAGE sql STABLE`,\n\n `CREATE OR REPLACE FUNCTION ${REBASE_SCHEMA}.jwt() RETURNS jsonb AS $$\n SELECT COALESCE(\n NULLIF(current_setting('app.jwt', true), ''),\n '{}'\n )::jsonb;\n$$ LANGUAGE sql STABLE`,\n\n `CREATE OR REPLACE FUNCTION ${REBASE_SCHEMA}.roles() RETURNS text AS $$\n SELECT COALESCE(NULLIF(current_setting('app.user_roles', true), ''), '');\n$$ LANGUAGE sql STABLE`\n];\n\n/** The same statements as one script, for migration files and raw clients. */\nexport const RLS_BOOTSTRAP_SQL =\n \"-- Rebase schema + RLS helper functions (required by the policies below)\\n\" +\n RLS_BOOTSTRAP_STATEMENTS.map(s => `${s};`).join(\"\\n\\n\") + \"\\n\";\n\n/**\n * Removes the pre-1.0 `auth` schema, but only when Rebase is what put it there.\n *\n * ## Why this is safe against a Supabase database\n *\n * Two independent guards, and both have to pass:\n *\n * 1. **Each function is identified before it is dropped.** Ours returns `text`\n * and reads the `app.uid` GUC; Supabase's returns `uuid` and reads\n * `request.jwt.claims`. Nothing is dropped on a signature we did not write,\n * so a Supabase database — where our `CREATE OR REPLACE` could never have\n * succeeded in the first place, Postgres refusing to change a return type —\n * matches nothing and this is a no-op.\n * 2. **`DROP SCHEMA … RESTRICT`**, never CASCADE. If anything else at all still\n * lives in `auth` (Supabase's `users` table, its other helpers), the drop\n * fails and the schema stays. CASCADE here would be unrecoverable.\n *\n * ## Why it cannot run too early\n *\n * Postgres records a dependency from every RLS policy to the functions its body\n * calls, so `DROP FUNCTION auth.uid()` fails for as long as a single policy\n * still references it. That is the interlock, and it is load-bearing: the drop\n * can only succeed once every policy has been recompiled to \\`rebase.uid()\\`.\n * Callers therefore run this *after* applying policies, and treat a failure as\n * \"not yet — try again next boot\" rather than as an error.\n */\nexport const DROP_LEGACY_AUTH_SCHEMA_SQL = `\nDO $rebase_drop_legacy$\nDECLARE\n dropped_any boolean := false;\nBEGIN\n -- Each function is matched on its own result type and body, so a schema\n -- that merely shares the name keeps everything it has.\n IF EXISTS (\n SELECT 1 FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace\n WHERE n.nspname = 'auth' AND p.proname = 'uid'\n AND pg_get_function_result(p.oid) = 'text'\n AND p.prosrc LIKE '%app.uid%'\n ) THEN\n DROP FUNCTION auth.uid();\n dropped_any := true;\n END IF;\n\n IF EXISTS (\n SELECT 1 FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace\n WHERE n.nspname = 'auth' AND p.proname = 'jwt'\n AND pg_get_function_result(p.oid) = 'jsonb'\n AND p.prosrc LIKE '%app.jwt%'\n ) THEN\n DROP FUNCTION auth.jwt();\n dropped_any := true;\n END IF;\n\n IF EXISTS (\n SELECT 1 FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace\n WHERE n.nspname = 'auth' AND p.proname = 'roles'\n AND pg_get_function_result(p.oid) = 'text'\n AND p.prosrc LIKE '%app.user_roles%'\n ) THEN\n DROP FUNCTION auth.roles();\n dropped_any := true;\n END IF;\n\n -- RESTRICT: only an empty schema goes. Anything else in there — including a\n -- Supabase installation left untouched above — keeps it.\n IF dropped_any THEN\n BEGIN\n EXECUTE 'DROP SCHEMA auth RESTRICT';\n EXCEPTION WHEN OTHERS THEN\n NULL;\n END;\n END IF;\nEND\n$rebase_drop_legacy$;\n`;\n\n/** Somebody's policy that still calls a pre-1.0 helper. */\nexport interface LegacyRlsDependent {\n schema: string;\n table: string;\n policy: string;\n}\n\n/**\n * Policies whose body still calls `auth.uid()` / `auth.roles()` / `auth.jwt()`.\n *\n * Postgres will not drop a function a policy depends on, so this is exactly the\n * set standing between a database and losing the legacy schema. Rebase's own\n * policies leave the list on the next push or boot, when they are recompiled —\n * anything still here afterwards is hand-written, will never be recompiled by\n * anybody, and is the reason the drop keeps being skipped. Silence there would\n * leave an operator staring at a schema the release notes said would go.\n */\nexport const LEGACY_RLS_DEPENDENTS_SQL = `\n SELECT n.nspname AS schema, c.relname AS \"table\", p.polname AS policy\n FROM pg_policy p\n JOIN pg_class c ON c.oid = p.polrelid\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE pg_get_expr(p.polqual, p.polrelid) ~* '\\\\m${LEGACY_RLS_SCHEMA}\\\\.(uid|jwt|roles)\\\\s*\\\\('\n OR pg_get_expr(p.polwithcheck, p.polrelid) ~* '\\\\m${LEGACY_RLS_SCHEMA}\\\\.(uid|jwt|roles)\\\\s*\\\\('\n ORDER BY 1, 2, 3\n`;\n\n/** Somebody's *function* that still calls a pre-1.0 helper from its own body. */\nexport interface LegacyRlsFunctionDependent {\n schema: string;\n function: string;\n}\n\n/**\n * Functions whose body calls `auth.uid()` / `auth.roles()` / `auth.jwt()`.\n *\n * This is the half `DROP FUNCTION ... RESTRICT` cannot see, and the reason it\n * needs its own query. Postgres records a dependency for a *policy* that calls a\n * function, which is why the drop is safe against the policies above — but a\n * `LANGUAGE sql` function whose body is a **string literal** is not parsed when\n * it is created, so nothing is recorded and `RESTRICT` has nothing to refuse on.\n * The drop succeeds and the caller is left pointing at a function that no longer\n * exists, which fails at *query* time rather than at boot.\n *\n * A downstream project building on these helpers is not hypothetical: the Rebase\n * control plane defines `auth.is_org_member(uuid)` and `auth.is_org_admin(uuid)`\n * in this very schema, each calling `auth.uid()` in a string body, and eleven of\n * its row-level-security policies go through them. Every one of those would have\n * started failing the first time a recompile left no policy referencing\n * `auth.uid()` directly — the drop's own precondition.\n *\n * Matching on the body text is the only option available, and it is deliberately\n * broad: a false positive costs a schema that stays one release longer and says\n * why, while a false negative costs somebody their policies.\n */\nexport const LEGACY_RLS_FUNCTION_DEPENDENTS_SQL = `\n SELECT n.nspname AS schema, p.proname AS function\n FROM pg_proc p\n JOIN pg_namespace n ON n.oid = p.pronamespace\n WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')\n AND NOT (n.nspname = '${LEGACY_RLS_SCHEMA}' AND p.proname IN ('uid', 'jwt', 'roles'))\n AND p.prosrc ~* '\\\\m${LEGACY_RLS_SCHEMA}\\\\.(uid|jwt|roles)\\\\s*\\\\('\n ORDER BY 1, 2\n`;\n\n/**\n * Retire the pre-1.0 `auth` schema, reporting what is holding it back.\n *\n * The shared implementation behind the CLI's post-push step and the runtime's\n * post-policy step. Both used to just fire {@link DROP_LEGACY_AUTH_SCHEMA_SQL}\n * and swallow whatever came back, which is right for the ordinary case — a\n * table not recompiled *yet* — and wrong for the one that never resolves: a\n * hand-written policy nothing will ever rewrite. Then the schema stays forever\n * and nothing ever says why.\n */\nexport async function dropLegacyAuthSchema(\n run: (sql: string) => Promise<Record<string, unknown>[]>,\n report: { info: (m: string) => void; warn: (m: string) => void }\n): Promise<void> {\n let blockers: LegacyRlsDependent[];\n try {\n blockers = (await run(LEGACY_RLS_DEPENDENTS_SQL)) as unknown as LegacyRlsDependent[];\n } catch {\n return; // No catalogue access; nothing here is worth failing a boot for.\n }\n\n if (blockers.length > 0) {\n report.warn(\n `The pre-1.0 \\`${LEGACY_RLS_SCHEMA}\\` schema cannot be removed yet: ${blockers.length} ` +\n `${blockers.length === 1 ? \"policy still calls\" : \"policies still call\"} ` +\n `\\`${LEGACY_RLS_SCHEMA}.uid()\\` and friends, and Postgres will not drop a function a policy ` +\n `depends on. Rebase's own policies are ` +\n `rewritten automatically — anything listed here is hand-written SQL that has to be updated to ` +\n `\\`${REBASE_SCHEMA}.uid()\\` by hand, after which the schema goes on its own:\\n` +\n blockers.map(b => ` • ${b.schema}.${b.table} → \"${b.policy}\"`).join(\"\\n\")\n );\n return;\n }\n\n // The half Postgres will not refuse on. See LEGACY_RLS_FUNCTION_DEPENDENTS_SQL:\n // a string-literal SQL body records no dependency, so `DROP FUNCTION` drops\n // out from under a caller that is still using it and nothing complains until\n // a query runs.\n let borrowers: LegacyRlsFunctionDependent[];\n try {\n borrowers = (await run(LEGACY_RLS_FUNCTION_DEPENDENTS_SQL)) as unknown as LegacyRlsFunctionDependent[];\n } catch {\n return;\n }\n\n if (borrowers.length > 0) {\n report.warn(\n `The pre-1.0 \\`${LEGACY_RLS_SCHEMA}\\` schema cannot be removed yet: ${borrowers.length} ` +\n `${borrowers.length === 1 ? \"function calls\" : \"functions call\"} ` +\n `\\`${LEGACY_RLS_SCHEMA}.uid()\\` and friends from its own body. Postgres records no dependency ` +\n `for that — a SQL body written as a string literal is never parsed — so dropping the helpers ` +\n `would leave these pointing at nothing, and they would fail when a query reached them rather ` +\n `than now. Repoint them at \\`${REBASE_SCHEMA}.uid()\\`, after which the schema goes on its own:\\n` +\n borrowers.map(b => ` • ${b.schema}.${b.function}()`).join(\"\\n\")\n );\n return;\n }\n\n try {\n await run(DROP_LEGACY_AUTH_SCHEMA_SQL);\n } catch (err) {\n // Something else depends on it, or the connection does not own it.\n // Not fatal: the schema is inert either way now that no policy calls it.\n report.info(\n `Left the \\`${LEGACY_RLS_SCHEMA}\\` schema in place: ` +\n (err instanceof Error ? err.message : String(err))\n );\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,IAAa,2BAA8C;CACvD,+BAA+B;CAK/B,8BAA8B,cAAc;;;;;;CAO5C,8BAA8B,cAAc;;;;;;CAO5C,8BAA8B,cAAc;;;AAGhD;AAII,KACA,yBAAyB,KAAI,MAAK,GAAG,EAAE,EAAE,CAAC,CAAC,KAAK,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4B1D,IAAa,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmE3C,IAAa,4BAA4B;;;;;sDAKa,kBAAkB;2DACb,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;AAgC7E,IAAa,qCAAqC;;;;;8BAKpB,kBAAkB;4BACpB,kBAAkB;;;;;;;;;;;;;AAc9C,eAAsB,qBAClB,KACA,QACa;CACb,IAAI;CACJ,IAAI;EACA,WAAY,MAAM,IAAI,yBAAyB;CACnD,QAAQ;EACJ;CACJ;CAEA,IAAI,SAAS,SAAS,GAAG;EACrB,OAAO,KACH,iBAAiB,kBAAkB,mCAAmC,SAAS,OAAO,GACnF,SAAS,WAAW,IAAI,uBAAuB,sBAAsB,KACnE,kBAAkB,4MAGlB,cAAc,+DACnB,SAAS,KAAI,MAAK,OAAO,EAAE,OAAO,GAAG,EAAE,MAAM,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,KAAK,IAAI,CAC7E;EACA;CACJ;CAMA,IAAI;CACJ,IAAI;EACA,YAAa,MAAM,IAAI,kCAAkC;CAC7D,QAAQ;EACJ;CACJ;CAEA,IAAI,UAAU,SAAS,GAAG;EACtB,OAAO,KACH,iBAAiB,kBAAkB,mCAAmC,UAAU,OAAO,GACpF,UAAU,WAAW,IAAI,mBAAmB,iBAAiB,KAC3D,kBAAkB,6RAGQ,cAAc,uDAC7C,UAAU,KAAI,MAAK,OAAO,EAAE,OAAO,GAAG,EAAE,SAAS,GAAG,CAAC,CAAC,KAAK,IAAI,CACnE;EACA;CACJ;CAEA,IAAI;EACA,MAAM,IAAI,2BAA2B;CACzC,SAAS,KAAK;EAGV,OAAO,KACH,cAAc,kBAAkB,yBAC/B,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EACpD;CACJ;AACJ"}
1
+ {"version":3,"file":"rls-bootstrap-sql-B5C9LoJ6.js","names":[],"sources":["../src/schema/rls-bootstrap-sql.ts"],"sourcesContent":["import { LEGACY_RLS_SCHEMA, REBASE_SCHEMA } from \"@rebasepro/types\";\n\n/**\n * Canonical SQL bootstrap for the RLS helper functions.\n *\n * Generated RLS policies reference `rebase.uid()` / `rebase.roles()` /\n * `rebase.jwt()`, so any SQL stream that can contain policies must be\n * self-contained: it has to (re)create these helpers first. This matters for\n * the migration directory in particular — Atlas replays migrations against a\n * clean dev database where no out-of-band bootstrap has ever run, so a\n * migration carrying policies without this preamble fails with\n * \"function rebase.uid() does not exist\".\n *\n * Idempotent (`IF NOT EXISTS` / `OR REPLACE`) so it can be prepended to every\n * policies block and re-applied freely. The runtime boot path\n * (`auth/ensure-tables.ts`) creates the same functions under an advisory lock\n * for HMR-safety; keep the definitions in sync.\n *\n * ## Creating the `rebase` schema here is now safe, and required\n *\n * It deliberately did not, once. These functions lived in a schema called\n * `auth`, and the note here read: creating `rebase` would leak it into Atlas's\n * replayed migration state, and — absent from the desired `schema.sql` — Atlas\n * would then plan `DROP SCHEMA \"rebase\" CASCADE`, taking the auth tables with\n * it. That reasoning still holds; what changed is the second half of it. The\n * DDL generator now emits `CREATE SCHEMA IF NOT EXISTS \"rebase\"`\n * unconditionally, so the schema is always in the desired state and the diff is\n * empty. (It used to appear only when some collection happened to declare\n * `schema: \"rebase\"` — true for the scaffold's users collection, and not a\n * property anything guaranteed.) `db push` additionally excludes the whole\n * schema from the declarative apply.\n *\n * See `@rebasepro/types`' `rls-functions` for why the functions moved out of\n * `auth` at all: the short version is that the name was Supabase's, and\n * `CREATE OR REPLACE FUNCTION auth.uid() RETURNS text` cannot be applied over\n * Supabase's `RETURNS uuid` — Postgres refuses, and the refusal used to be\n * swallowed.\n */\n/**\n * The bootstrap as individual statements.\n *\n * Kept as an array because the two consumers need different shapes and only one\n * of them can take a multi-command string: the migration preamble is written to\n * a file and replayed by Atlas, but the boot path runs through drizzle, whose\n * node-postgres handle speaks the extended query protocol and rejects more than\n * one command per call. Splitting a joined string back apart on `$$;` would be\n * a parser for a problem that does not need one.\n */\nexport const RLS_BOOTSTRAP_STATEMENTS: readonly string[] = [\n `CREATE SCHEMA IF NOT EXISTS ${REBASE_SCHEMA}`,\n\n // Falls back to the pre-rename `app.user_id` so a database that has taken\n // the new schema but is still served by an older backend keeps resolving\n // the principal. Drop the COALESCE once no such deployment remains.\n `CREATE OR REPLACE FUNCTION ${REBASE_SCHEMA}.uid() RETURNS text AS $$\n SELECT COALESCE(\n NULLIF(current_setting('app.uid', true), ''),\n NULLIF(current_setting('app.user_id', true), '')\n );\n$$ LANGUAGE sql STABLE`,\n\n `CREATE OR REPLACE FUNCTION ${REBASE_SCHEMA}.jwt() RETURNS jsonb AS $$\n SELECT COALESCE(\n NULLIF(current_setting('app.jwt', true), ''),\n '{}'\n )::jsonb;\n$$ LANGUAGE sql STABLE`,\n\n `CREATE OR REPLACE FUNCTION ${REBASE_SCHEMA}.roles() RETURNS text AS $$\n SELECT COALESCE(NULLIF(current_setting('app.user_roles', true), ''), '');\n$$ LANGUAGE sql STABLE`\n];\n\n/** The same statements as one script, for migration files and raw clients. */\nexport const RLS_BOOTSTRAP_SQL =\n \"-- Rebase schema + RLS helper functions (required by the policies below)\\n\" +\n RLS_BOOTSTRAP_STATEMENTS.map(s => `${s};`).join(\"\\n\\n\") + \"\\n\";\n\n/**\n * Removes the pre-1.0 `auth` schema, but only when Rebase is what put it there.\n *\n * ## Why this is safe against a Supabase database\n *\n * Two independent guards, and both have to pass:\n *\n * 1. **Each function is identified before it is dropped.** Ours returns `text`\n * and reads the `app.uid` GUC; Supabase's returns `uuid` and reads\n * `request.jwt.claims`. Nothing is dropped on a signature we did not write,\n * so a Supabase database — where our `CREATE OR REPLACE` could never have\n * succeeded in the first place, Postgres refusing to change a return type —\n * matches nothing and this is a no-op.\n * 2. **`DROP SCHEMA … RESTRICT`**, never CASCADE. If anything else at all still\n * lives in `auth` (Supabase's `users` table, its other helpers), the drop\n * fails and the schema stays. CASCADE here would be unrecoverable.\n *\n * ## Why it cannot run too early\n *\n * Postgres records a dependency from every RLS policy to the functions its body\n * calls, so `DROP FUNCTION auth.uid()` fails for as long as a single policy\n * still references it. That is the interlock, and it is load-bearing: the drop\n * can only succeed once every policy has been recompiled to \\`rebase.uid()\\`.\n * Callers therefore run this *after* applying policies, and treat a failure as\n * \"not yet — try again next boot\" rather than as an error.\n */\nexport const DROP_LEGACY_AUTH_SCHEMA_SQL = `\nDO $rebase_drop_legacy$\nDECLARE\n dropped_any boolean := false;\nBEGIN\n -- Each function is matched on its own result type and body, so a schema\n -- that merely shares the name keeps everything it has.\n IF EXISTS (\n SELECT 1 FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace\n WHERE n.nspname = 'auth' AND p.proname = 'uid'\n AND pg_get_function_result(p.oid) = 'text'\n AND p.prosrc LIKE '%app.uid%'\n ) THEN\n DROP FUNCTION auth.uid();\n dropped_any := true;\n END IF;\n\n IF EXISTS (\n SELECT 1 FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace\n WHERE n.nspname = 'auth' AND p.proname = 'jwt'\n AND pg_get_function_result(p.oid) = 'jsonb'\n AND p.prosrc LIKE '%app.jwt%'\n ) THEN\n DROP FUNCTION auth.jwt();\n dropped_any := true;\n END IF;\n\n IF EXISTS (\n SELECT 1 FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace\n WHERE n.nspname = 'auth' AND p.proname = 'roles'\n AND pg_get_function_result(p.oid) = 'text'\n AND p.prosrc LIKE '%app.user_roles%'\n ) THEN\n DROP FUNCTION auth.roles();\n dropped_any := true;\n END IF;\n\n -- RESTRICT: only an empty schema goes. Anything else in there — including a\n -- Supabase installation left untouched above — keeps it.\n IF dropped_any THEN\n BEGIN\n EXECUTE 'DROP SCHEMA auth RESTRICT';\n EXCEPTION WHEN OTHERS THEN\n NULL;\n END;\n END IF;\nEND\n$rebase_drop_legacy$;\n`;\n\n/** Somebody's policy that still calls a pre-1.0 helper. */\nexport interface LegacyRlsDependent {\n schema: string;\n table: string;\n policy: string;\n}\n\n/**\n * Policies whose body still calls `auth.uid()` / `auth.roles()` / `auth.jwt()`.\n *\n * Postgres will not drop a function a policy depends on, so this is exactly the\n * set standing between a database and losing the legacy schema. Rebase's own\n * policies leave the list on the next push or boot, when they are recompiled —\n * anything still here afterwards is hand-written, will never be recompiled by\n * anybody, and is the reason the drop keeps being skipped. Silence there would\n * leave an operator staring at a schema the release notes said would go.\n */\nexport const LEGACY_RLS_DEPENDENTS_SQL = `\n SELECT n.nspname AS schema, c.relname AS \"table\", p.polname AS policy\n FROM pg_policy p\n JOIN pg_class c ON c.oid = p.polrelid\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE pg_get_expr(p.polqual, p.polrelid) ~* '\\\\m${LEGACY_RLS_SCHEMA}\\\\.(uid|jwt|roles)\\\\s*\\\\('\n OR pg_get_expr(p.polwithcheck, p.polrelid) ~* '\\\\m${LEGACY_RLS_SCHEMA}\\\\.(uid|jwt|roles)\\\\s*\\\\('\n ORDER BY 1, 2, 3\n`;\n\n/** Somebody's *function* that still calls a pre-1.0 helper from its own body. */\nexport interface LegacyRlsFunctionDependent {\n schema: string;\n function: string;\n}\n\n/**\n * Functions whose body calls `auth.uid()` / `auth.roles()` / `auth.jwt()`.\n *\n * This is the half `DROP FUNCTION ... RESTRICT` cannot see, and the reason it\n * needs its own query. Postgres records a dependency for a *policy* that calls a\n * function, which is why the drop is safe against the policies above — but a\n * `LANGUAGE sql` function whose body is a **string literal** is not parsed when\n * it is created, so nothing is recorded and `RESTRICT` has nothing to refuse on.\n * The drop succeeds and the caller is left pointing at a function that no longer\n * exists, which fails at *query* time rather than at boot.\n *\n * A downstream project building on these helpers is not hypothetical: the Rebase\n * control plane defines `auth.is_org_member(uuid)` and `auth.is_org_admin(uuid)`\n * in this very schema, each calling `auth.uid()` in a string body, and eleven of\n * its row-level-security policies go through them. Every one of those would have\n * started failing the first time a recompile left no policy referencing\n * `auth.uid()` directly — the drop's own precondition.\n *\n * Matching on the body text is the only option available, and it is deliberately\n * broad: a false positive costs a schema that stays one release longer and says\n * why, while a false negative costs somebody their policies.\n */\nexport const LEGACY_RLS_FUNCTION_DEPENDENTS_SQL = `\n SELECT n.nspname AS schema, p.proname AS function\n FROM pg_proc p\n JOIN pg_namespace n ON n.oid = p.pronamespace\n WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')\n AND NOT (n.nspname = '${LEGACY_RLS_SCHEMA}' AND p.proname IN ('uid', 'jwt', 'roles'))\n AND p.prosrc ~* '\\\\m${LEGACY_RLS_SCHEMA}\\\\.(uid|jwt|roles)\\\\s*\\\\('\n ORDER BY 1, 2\n`;\n\n/**\n * Retire the pre-1.0 `auth` schema, reporting what is holding it back.\n *\n * The shared implementation behind the CLI's post-push step and the runtime's\n * post-policy step. Both used to just fire {@link DROP_LEGACY_AUTH_SCHEMA_SQL}\n * and swallow whatever came back, which is right for the ordinary case — a\n * table not recompiled *yet* — and wrong for the one that never resolves: a\n * hand-written policy nothing will ever rewrite. Then the schema stays forever\n * and nothing ever says why.\n */\nexport async function dropLegacyAuthSchema(\n run: (sql: string) => Promise<Record<string, unknown>[]>,\n report: { info: (m: string) => void; warn: (m: string) => void }\n): Promise<void> {\n let blockers: LegacyRlsDependent[];\n try {\n blockers = (await run(LEGACY_RLS_DEPENDENTS_SQL)) as unknown as LegacyRlsDependent[];\n } catch {\n return; // No catalogue access; nothing here is worth failing a boot for.\n }\n\n if (blockers.length > 0) {\n report.warn(\n `The pre-1.0 \\`${LEGACY_RLS_SCHEMA}\\` schema cannot be removed yet: ${blockers.length} ` +\n `${blockers.length === 1 ? \"policy still calls\" : \"policies still call\"} ` +\n `\\`${LEGACY_RLS_SCHEMA}.uid()\\` and friends, and Postgres will not drop a function a policy ` +\n `depends on. Rebase's own policies are ` +\n `rewritten automatically — anything listed here is hand-written SQL that has to be updated to ` +\n `\\`${REBASE_SCHEMA}.uid()\\` by hand, after which the schema goes on its own:\\n` +\n blockers.map(b => ` • ${b.schema}.${b.table} → \"${b.policy}\"`).join(\"\\n\")\n );\n return;\n }\n\n // The half Postgres will not refuse on. See LEGACY_RLS_FUNCTION_DEPENDENTS_SQL:\n // a string-literal SQL body records no dependency, so `DROP FUNCTION` drops\n // out from under a caller that is still using it and nothing complains until\n // a query runs.\n let borrowers: LegacyRlsFunctionDependent[];\n try {\n borrowers = (await run(LEGACY_RLS_FUNCTION_DEPENDENTS_SQL)) as unknown as LegacyRlsFunctionDependent[];\n } catch {\n return;\n }\n\n if (borrowers.length > 0) {\n report.warn(\n `The pre-1.0 \\`${LEGACY_RLS_SCHEMA}\\` schema cannot be removed yet: ${borrowers.length} ` +\n `${borrowers.length === 1 ? \"function calls\" : \"functions call\"} ` +\n `\\`${LEGACY_RLS_SCHEMA}.uid()\\` and friends from its own body. Postgres records no dependency ` +\n `for that — a SQL body written as a string literal is never parsed — so dropping the helpers ` +\n `would leave these pointing at nothing, and they would fail when a query reached them rather ` +\n `than now. Repoint them at \\`${REBASE_SCHEMA}.uid()\\`, after which the schema goes on its own:\\n` +\n borrowers.map(b => ` • ${b.schema}.${b.function}()`).join(\"\\n\")\n );\n return;\n }\n\n try {\n await run(DROP_LEGACY_AUTH_SCHEMA_SQL);\n } catch (err) {\n // Something else depends on it, or the connection does not own it.\n // Not fatal: the schema is inert either way now that no policy calls it.\n report.info(\n `Left the \\`${LEGACY_RLS_SCHEMA}\\` schema in place: ` +\n (err instanceof Error ? err.message : String(err))\n );\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,IAAa,2BAA8C;CACvD,+BAA+B;CAK/B,8BAA8B,cAAc;;;;;;CAO5C,8BAA8B,cAAc;;;;;;CAO5C,8BAA8B,cAAc;;;AAGhD;AAII,KACA,yBAAyB,KAAI,MAAK,GAAG,EAAE,EAAE,CAAC,CAAC,KAAK,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4B1D,IAAa,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmE3C,IAAa,4BAA4B;;;;;sDAKa,kBAAkB;2DACb,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;AAgC7E,IAAa,qCAAqC;;;;;8BAKpB,kBAAkB;4BACpB,kBAAkB;;;;;;;;;;;;;AAc9C,eAAsB,qBAClB,KACA,QACa;CACb,IAAI;CACJ,IAAI;EACA,WAAY,MAAM,IAAI,yBAAyB;CACnD,QAAQ;EACJ;CACJ;CAEA,IAAI,SAAS,SAAS,GAAG;EACrB,OAAO,KACH,iBAAiB,kBAAkB,mCAAmC,SAAS,OAAO,GACnF,SAAS,WAAW,IAAI,uBAAuB,sBAAsB,KACnE,kBAAkB,4MAGlB,cAAc,+DACnB,SAAS,KAAI,MAAK,OAAO,EAAE,OAAO,GAAG,EAAE,MAAM,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,KAAK,IAAI,CAC7E;EACA;CACJ;CAMA,IAAI;CACJ,IAAI;EACA,YAAa,MAAM,IAAI,kCAAkC;CAC7D,QAAQ;EACJ;CACJ;CAEA,IAAI,UAAU,SAAS,GAAG;EACtB,OAAO,KACH,iBAAiB,kBAAkB,mCAAmC,UAAU,OAAO,GACpF,UAAU,WAAW,IAAI,mBAAmB,iBAAiB,KAC3D,kBAAkB,6RAGQ,cAAc,uDAC7C,UAAU,KAAI,MAAK,OAAO,EAAE,OAAO,GAAG,EAAE,SAAS,GAAG,CAAC,CAAC,KAAK,IAAI,CACnE;EACA;CACJ;CAEA,IAAI;EACA,MAAM,IAAI,2BAA2B;CACzC,SAAS,KAAK;EAGV,OAAO,KACH,cAAc,kBAAkB,yBAC/B,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EACpD;CACJ;AACJ"}
@@ -1,8 +1,8 @@
1
1
  import { createRequire as __createRequire } from "module";
2
2
  import "process";
3
3
  __createRequire(import.meta.url);
4
- import { Y as ANONYMOUS_USER_ID, _ as securityRuleToConditions, v as findAnonymousGrants } from "./collection-index-DxJBvVTH.js";
5
- import { n as REBASE_SCHEMA, o as usesLegacyRlsFunctions } from "./src-DiDgtX8P.js";
4
+ import { Y as ANONYMOUS_USER_ID, _ as securityRuleToConditions, v as findAnonymousGrants } from "./collection-index-BRUg10H5.js";
5
+ import { r as REBASE_SCHEMA, s as usesLegacyRlsFunctions } from "./src-DiB5RP2Z.js";
6
6
  import { logger } from "@rebasepro/server";
7
7
  import { sql } from "drizzle-orm";
8
8
  //#region ../common/src/util/internal-tables.ts
@@ -464,4 +464,4 @@ async function validatePolicyPgRoles(run, collections, requestRole = REBASE_USER
464
464
  //#endregion
465
465
  export { warnOnAnonymousGrants as a, REBASE_USER_ROLE as c, validatePolicyPgRoles as i, revokeInternalTableAccess as l, detectConnectionPosture as n, warnOnLegacyRlsFunctions as o, ensureAppRole as r, warnOnRoleSchemaCollision as s, applyAuthContext as t, revokeInternalTableSql as u };
466
466
 
467
- //# sourceMappingURL=rls-enforcement-C1RJ1uI2.js.map
467
+ //# sourceMappingURL=rls-enforcement-DvAbL9YJ.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"rls-enforcement-C1RJ1uI2.js","names":[],"sources":["../../common/src/util/internal-tables.ts","../src/security/rls-enforcement.ts"],"sourcesContent":["/**\n * The tables Rebase creates for its own bookkeeping, and the SQL that keeps the\n * end-user role away from them.\n *\n * ## Why this exists\n *\n * Authenticated requests run as {@link REBASE_USER_ROLE}, and the boot-time role\n * provisioning grants that role `SELECT, INSERT, UPDATE, DELETE` on every table\n * in the schemas a project uses — including `rebase`, because a project's own\n * collections are allowed to live there (the scaffold puts `users` there). It\n * also sets `ALTER DEFAULT PRIVILEGES`, so a table created *later* by the\n * migrating role inherits the same grant.\n *\n * Every framework-internal table is created later: auth's tables come up during\n * `initializeAuth`, `api_keys` during route mounting, `cron_logs` when the first\n * job registers, `idempotency_keys` on the first request that carries a key. So\n * they all inherited full DML for the end-user role — and none of them enables\n * row-level security, because none of them is a collection with\n * `securityRules`. Measured on a freshly provisioned database, `SET ROLE\n * rebase_user` could read `rebase.refresh_tokens` (session token hashes),\n * `rebase.mfa_factors` (`secret_encrypted`), `rebase.recovery_codes`, and\n * `rebase.api_keys` (including its `admin` flag), and insert into\n * `rebase.app_config`.\n *\n * Nothing routes a user-context query at those tables today, so this was not\n * reachable over the API. That is the wrong thing to depend on: the documented\n * model is that RLS is the authorization boundary, and these tables sat outside\n * it. The boundary is now a privilege boundary instead — the role simply cannot\n * address them.\n *\n * ## Why REVOKE rather than ENABLE ROW LEVEL SECURITY\n *\n * RLS with no policy denies every row, which is the same outcome, but it is the\n * *weaker* statement: it leaves the grant in place, so a later policy — or a\n * `FORCE` flag cleared by some future migration — reopens the table. There is no\n * row of `refresh_tokens` any end user should ever reach, so the honest encoding\n * is \"this role has no privilege here at all\". It also keeps the owner\n * connection (which auth actually runs on) completely unaffected.\n *\n * ## Keeping it true\n *\n * `packages/rls-check` scans the `rebase` schema — it used to skip it as a\n * \"platform\" schema — and its `rls-disabled` check fires on exactly the\n * condition this module removes: RLS off *and* a DML grant to a reachable role.\n * So a table added here without a revoke is caught by `pnpm rls:check`, not by\n * someone re-reading this file.\n */\n\n/**\n * The Postgres role authenticated requests run as.\n *\n * Defined here rather than in the Postgres driver because both the driver (which\n * provisions the role) and this module (which revokes on its behalf) need it,\n * and a second spelling of a role name is a silent no-op waiting to happen.\n */\nexport const REBASE_USER_ROLE = \"rebase_user\";\n\n/**\n * Framework-internal table names, unqualified.\n *\n * Deliberately NOT including `users`: the auth user table is also a collection,\n * with `securityRules`, RLS enabled and policies applied. Users read their own\n * row through it — revoking there would break sign-in. `revokeInternalTableSql`\n * now skips any table with RLS enabled, so that exception is enforced rather\n * than merely remembered — and so is the same hazard for every other name here,\n * any of which a project may legitimately use for a collection of its own.\n *\n * `atlas_schema_revisions` is Atlas's migration ledger, which lands in `rebase`\n * because `db migrate apply` passes `--revisions-schema rebase`.\n *\n * Every entry here must also be revoked by whatever creates it, and vice versa:\n * the creation-time revoke fires once, on the boot that first makes the table,\n * so it cannot help a database provisioned before that revoke existed. This\n * list is what the boot-time sweep in `ensureAppRole` iterates, and the sweep\n * is the only thing that can repair an already-granted table. A table revoked\n * at creation but missing here is therefore permanently stranded on any\n * database that predates its revoke.\n *\n * These names are unqualified, and the boot-time sweep applies them to every\n * schema a project uses — so an entry here is a claim on that name in `public`\n * as much as in `rebase`. `jobs` is Rebase's queue at `rebase.jobs` AND a\n * perfectly ordinary collection name, and revoking `public.jobs` from a project\n * that owns it leaves every read failing 42501 with correct policies applied\n * and nothing in the RLS logs to explain it. The `relrowsecurity` guard in\n * `revokeInternalTableSql` is what makes a common noun safe here — it is not a\n * licence to claim more of them.\n */\nexport const REBASE_INTERNAL_TABLES: readonly string[] = [\n // auth\n \"user_identities\",\n \"refresh_tokens\",\n \"password_reset_tokens\",\n \"magic_link_tokens\",\n \"mfa_factors\",\n \"mfa_challenges\",\n \"recovery_codes\",\n \"app_config\",\n \"schema_meta\",\n // platform services\n \"api_keys\",\n \"cron_logs\",\n \"cron_claims\",\n \"jobs\",\n \"rate_limit_hits\",\n \"idempotency_keys\",\n \"entity_history\",\n \"branches\",\n \"metric_samples\",\n // realtime channels — authorization for these lives in the channel rules the\n // server evaluates before it reads or writes, never in a row policy\n \"channel_messages\",\n \"channel_cursors\",\n \"channel_presence\",\n // migration bookkeeping\n \"atlas_schema_revisions\"\n];\n\n/** Postgres identifiers this module is willing to interpolate. */\nconst SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*$/;\n\n/**\n * A single statement that takes every privilege on `schema.table` away from the\n * end-user role.\n *\n * Wrapped in a `DO` block guarded on `pg_roles` for two reasons, both of which\n * happen in practice:\n *\n * - the role does not exist when the connection is unprivileged (Rebase then\n * relies on native RLS rather than a role switch), and a bare `REVOKE` on a\n * missing role is an error, not a no-op;\n * - the table may not exist yet — `cron_logs` never appears in a project with\n * no cron jobs — and `to_regclass` returning NULL has to be tolerated too.\n *\n * The third guard is the one that decides whether the *right* table is being\n * revoked. The names in {@link REBASE_INTERNAL_TABLES} are unqualified, and the\n * boot-time sweep in `ensureAppRole` applies all of them to every schema a\n * project uses — including the schema its own collections live in. A project is\n * free to call a collection `jobs`, `branches` or `api_keys`, and when it does,\n * the sweep was revoking `rebase_user`'s DML on the project's table on every\n * single boot. That is not a subtle degradation: the collection's whole API\n * answers 500 `permission denied for table …` from then on, which is what\n * happened to a public job board whose vacancies live in `public.jobs`.\n *\n * `relrowsecurity` separates the two cleanly, and it is the same fact this\n * module already relies on. Framework-internal tables carry no RLS — that is the\n * premise stated at the top of this file, and the reason a revoke is needed at\n * all. Every collection table has it enabled, because that is how Rebase\n * enforces `securityRules`. So \"RLS is off\" is exactly \"this is not somebody's\n * collection\", and the guard also subsumes the hand-carved `users` exception:\n * the auth user table is a collection, has RLS, and would now be skipped on its\n * own merits rather than by being kept off a list.\n *\n * One command, so it is safe on handles that speak the extended query protocol\n * and reject multi-statement strings.\n */\nexport function revokeInternalTableSql(schema: string, table: string): string {\n if (!SAFE_IDENTIFIER.test(schema)) {\n throw new Error(`Refusing to build SQL with an unsafe schema name: ${JSON.stringify(schema)}`);\n }\n if (!SAFE_IDENTIFIER.test(table)) {\n throw new Error(`Refusing to build SQL with an unsafe table name: ${JSON.stringify(table)}`);\n }\n const qualified = `\"${schema}\".\"${table}\"`;\n return `\n DO $rebase_revoke$\n BEGIN\n IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${REBASE_USER_ROLE}')\n AND to_regclass('${qualified}') IS NOT NULL\n AND NOT (SELECT relrowsecurity FROM pg_class WHERE oid = to_regclass('${qualified}')) THEN\n EXECUTE 'REVOKE ALL ON ${qualified} FROM ${REBASE_USER_ROLE}';\n END IF;\n END\n $rebase_revoke$;\n `.trim();\n}\n\n/**\n * Revoke on every internal table in `schema`, one statement at a time.\n *\n * Best-effort per table: a connection that does not own one of them (a\n * pre-provisioned database, a platform-managed ledger) cannot revoke on it, and\n * that must not take down a boot. The caller decides how loud to be — `onError`\n * exists so the driver can warn without this module importing a logger.\n */\nexport async function revokeInternalTableAccess(\n execute: (sql: string) => Promise<unknown>,\n schema: string,\n options?: { tables?: readonly string[]; onError?: (table: string, error: unknown) => void }\n): Promise<void> {\n for (const table of options?.tables ?? REBASE_INTERNAL_TABLES) {\n try {\n await execute(revokeInternalTableSql(schema, table));\n } catch (error) {\n options?.onError?.(table, error);\n }\n }\n}\n","import { sql as drizzleSql, SQL } from \"drizzle-orm\";\nimport { ANONYMOUS_USER_ID, PolicyExpression, SecurityRule } from \"@rebasepro/types\";\nimport {\n AnonymousGrantRisk,\n findAnonymousGrants,\n REBASE_USER_ROLE,\n revokeInternalTableAccess,\n securityRuleToConditions\n} from \"@rebasepro/common\";\nimport { REBASE_SCHEMA, usesLegacyRlsFunctions } from \"@rebasepro/types\";\nimport { logger } from \"@rebasepro/server\";\n\n/**\n * Unified RLS enforcement — the \"user context vs server context\" model.\n *\n * Every operation runs in one of two contexts:\n *\n * - **User context** — a request authenticated (or anonymous) via\n * `driver.withAuth(user)`. Runs as the restricted `rebase_user` role: a\n * non-owner, NOSUPERUSER, NOBYPASSRLS role, so Postgres RLS binds *every*\n * statement (SELECT, INSERT, UPDATE, DELETE). The collection's\n * `securityRules` are the whole authorization model; app-layer callbacks\n * are validation/side-effects, not a security boundary.\n *\n * - **Server context** — the base (owner) connection: auth flows, migrations,\n * and raw `rebase.sql`. As table owner it bypasses RLS. This is the trusted\n * plane, equivalent to Supabase's `service_role`.\n *\n * `rebase.dataAsAdmin` is **not** in it, despite the name. `init.ts` scopes\n * that driver with `withAuth(SERVICE_IDENTITY)`, so it arrives as user\n * context above — `rebase_user`, `app.uid = 'service'`, policies evaluated —\n * and clears the default policies through their admin arm rather than the\n * `rebase.uid() IS NULL` one.\n *\n * This module provides the three pieces:\n *\n * 1. {@link detectConnectionPosture} — is the connection subject to RLS at\n * all? (superuser / BYPASSRLS / table owner ⇒ no)\n * 2. {@link ensureAppRole} — idempotently provision `rebase_user` with\n * SELECT/INSERT/UPDATE/DELETE grants (+ default privileges so future\n * tables stay covered).\n * 3. {@link applyAuthContext} — per-transaction: set the `app.*` GUCs the\n * policies read (`rebase.uid()` etc.) and `SET LOCAL ROLE rebase_user` so\n * RLS binds. Transaction-scoped, so it composes with poolers.\n *\n * Provisioning runs from the framework's own bootstrap/migrate (which already\n * self-creates the `auth` schema and functions) — enforcement is default-on,\n * not an operator opt-in.\n */\n\n/**\n * The restricted role every authenticated (user-context) request runs as.\n *\n * Re-exported, not re-declared: the same name is needed by\n * `@rebasepro/common`'s internal-table revokes, and two spellings of a role name\n * fail as a silent no-op rather than an error.\n */\nexport { REBASE_USER_ROLE };\n\n/** Minimal SQL runner so callers can adapt drizzle or pg.Client. */\nexport type RawSqlRunner = (sqlText: string) => Promise<Record<string, unknown>[]>;\n\n/** Minimal transaction surface needed by {@link applyAuthContext}. */\nexport interface SqlTx {\n execute(query: SQL): Promise<unknown>;\n}\n\nexport interface ConnectionPosture {\n /** The connection's `current_user`. */\n role: string;\n superuser: boolean;\n bypassRLS: boolean;\n /** Owns at least one user table — owners bypass non-FORCE RLS. */\n ownsTables: boolean;\n /** True when RLS would NOT constrain this connection. */\n privileged: boolean;\n}\n\nexport interface AuthContext {\n uid: string;\n /** Raw roles as carried on the user (strings or `{ id }` objects). */\n roles: unknown[];\n}\n\nconst quoteIdent = (name: string): string => `\"${name.replace(/\"/g, \"\\\"\\\"\")}\"`;\n\n/** DML the user role holds on managed tables (RLS still filters per row). */\nconst USER_TABLE_PRIVILEGES = \"SELECT, INSERT, UPDATE, DELETE\";\n\n/**\n * Warn when the connection role shares its name with an existing schema.\n *\n * Postgres resolves unqualified names through `search_path`, which defaults to\n * `\"$user\", public` — and `$user` is the connection ROLE. When a schema of that\n * name exists it sits ahead of `public`, so every unqualified statement\n * silently operates on it instead:\n *\n * CREATE TABLE posts (...); -- you meant public.posts; you got <role>.posts\n *\n * Nothing errors. You get a second table of the same name in the wrong schema,\n * and reads that pin `public` cannot see it — which reads as \"missing table\" and\n * sends people to re-run a push that creates a *third* copy. The bootstrapper\n * has a whole branch dedicated to recognising the symptom after the fact.\n *\n * Rebase shipped straight into this: it creates a schema named `rebase` while\n * every template named the database role `rebase` too. The scaffold uses\n * `rebase_app` now, and every pool Rebase opens pins `search_path=public`\n * (`pinSearchPath`), which covers the paths the framework controls. This covers\n * the ones it does not — `psql`, `pg_dump`, drizzle-kit, a colleague's script,\n * a hand-written migration — because the hazard is a property of the two NAMES,\n * not of any one connection.\n *\n * A warning rather than a boot failure: the database works, the framework's own\n * traffic is pinned, and refusing to start over a naming choice a user may have\n * inherited would be worse than the risk.\n */\nexport async function warnOnRoleSchemaCollision(run: RawSqlRunner): Promise<void> {\n try {\n const rows = await run(`\n SELECT current_user AS role,\n EXISTS (\n SELECT 1 FROM pg_namespace n WHERE n.nspname = current_user\n ) AS collides\n `);\n if (rows[0]?.collides !== true) return;\n const role = String(rows[0]?.role ?? \"the connection role\");\n logger.warn(\n `⚠️ The database role \"${role}\" has the same name as a schema. Postgres resolves unqualified ` +\n `names through \\`search_path\\`, which defaults to \\`\"$user\", public\\` — so \"${role}\" is searched ` +\n `BEFORE public, and any unqualified \\`CREATE TABLE\\`/\\`SELECT\\` from a tool that does not pin the ` +\n `path (psql, pg_dump, drizzle-kit, a hand-written migration) silently lands in \"${role}\" instead. ` +\n `Rebase's own connections pin \\`search_path=public\\`, so the server is unaffected. To remove the ` +\n `hazard entirely, connect as a role whose name is not also a schema — the scaffold uses ` +\n `\"rebase_app\".`\n );\n } catch {\n // A diagnostic must never be the reason a boot fails.\n }\n}\n\nexport async function detectConnectionPosture(run: RawSqlRunner): Promise<ConnectionPosture> {\n const rows = await run(`\n SELECT current_user AS role,\n r.rolsuper AS superuser,\n r.rolbypassrls AS bypassrls,\n EXISTS (\n SELECT 1 FROM pg_tables t\n WHERE t.tableowner = current_user\n AND t.schemaname NOT IN ('pg_catalog', 'information_schema')\n ) AS owns_tables\n FROM pg_roles r\n WHERE r.rolname = current_user\n `);\n const row = rows[0] ?? {};\n const superuser = row.superuser === true;\n const bypassRLS = row.bypassrls === true;\n const ownsTables = row.owns_tables === true;\n return {\n role: String(row.role ?? \"unknown\"),\n superuser,\n bypassRLS,\n ownsTables,\n privileged: superuser || bypassRLS || ownsTables\n };\n}\n\n/**\n * Human-actionable instructions for when the connection cannot provision the\n * user role itself (no CREATEROLE and role not pre-created by the platform).\n */\nexport function appRoleSetupInstructions(connectionRole: string, schemas: string[]): string {\n const grants = schemas.map((s) =>\n `GRANT USAGE ON SCHEMA ${quoteIdent(s)} TO ${REBASE_USER_ROLE};\\n` +\n `GRANT ${USER_TABLE_PRIVILEGES} ON ALL TABLES IN SCHEMA ${quoteIdent(s)} TO ${REBASE_USER_ROLE};\\n` +\n `GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA ${quoteIdent(s)} TO ${REBASE_USER_ROLE};`\n ).join(\"\\n\");\n return (\n `Rebase enforces row-level security by running authenticated requests as ` +\n `the restricted role \"${REBASE_USER_ROLE}\", but the connection role ` +\n `\"${connectionRole}\" bypasses RLS and cannot create that role itself.\\n` +\n `Run the following as a database administrator, then restart:\\n\\n` +\n `CREATE ROLE ${REBASE_USER_ROLE} NOLOGIN NOSUPERUSER NOBYPASSRLS NOINHERIT;\\n` +\n `GRANT ${REBASE_USER_ROLE} TO ${quoteIdent(connectionRole)};\\n` +\n grants\n );\n}\n\n/**\n * Idempotently provision the `rebase_user` role, membership for the current\n * connection role, and DML grants (+ default privileges for future tables)\n * on every existing schema in `schemas`.\n *\n * Split into privilege tiers so it works both when the connection is a\n * superuser (creates everything) and when the platform pre-created the role\n * and membership (e.g. CNPG `postInitApplicationSQL`) and the connection is\n * merely the table owner — owners can always run the grant tier themselves.\n *\n * RLS still filters every row: these grants only make the tables *reachable*\n * by the role; the policies decide which rows/commands actually pass.\n *\n * Throws with precise setup instructions when the role is missing and the\n * connection cannot create it.\n */\nexport async function ensureAppRole(run: RawSqlRunner, schemas: string[]): Promise<void> {\n const uniqueSchemas = Array.from(new Set(schemas.filter(Boolean)));\n\n // Tier 1 — role existence.\n const roleRows = await run(`SELECT 1 FROM pg_roles WHERE rolname = '${REBASE_USER_ROLE}'`);\n if (roleRows.length === 0) {\n try {\n await run(`CREATE ROLE ${REBASE_USER_ROLE} NOLOGIN NOSUPERUSER NOBYPASSRLS NOINHERIT`);\n } catch (err) {\n throw new Error(\n `Failed to create the \"${REBASE_USER_ROLE}\" role: ${err instanceof Error ? err.message : String(err)}\\n\\n` +\n appRoleSetupInstructions(\"current connection role\", uniqueSchemas)\n );\n }\n }\n\n // Tier 2 — membership, so a non-superuser connection may SET ROLE to it.\n const memberRows = await run(`\n SELECT (pg_has_role(current_user, '${REBASE_USER_ROLE}', 'MEMBER')\n OR (SELECT rolsuper FROM pg_roles WHERE rolname = current_user)) AS can_set,\n current_user AS role\n `);\n if (memberRows[0]?.can_set !== true) {\n try {\n await run(`GRANT ${REBASE_USER_ROLE} TO CURRENT_USER`);\n } catch (err) {\n throw new Error(\n `The connection role is not a member of \"${REBASE_USER_ROLE}\" and cannot grant itself membership: ` +\n `${err instanceof Error ? err.message : String(err)}\\n\\n` +\n appRoleSetupInstructions(String(memberRows[0]?.role ?? \"current connection role\"), uniqueSchemas)\n );\n }\n }\n\n // Tier 3 — grants. Table owners (the expected non-superuser posture) can\n // always grant on their own objects, so this tier needs no extra privilege.\n const nspRows = await run(\"SELECT nspname FROM pg_namespace\");\n const existing = new Set(nspRows.map((r) => String(r.nspname)));\n for (const schema of uniqueSchemas) {\n if (!existing.has(schema)) continue;\n const s = quoteIdent(schema);\n await run(`GRANT USAGE ON SCHEMA ${s} TO ${REBASE_USER_ROLE}`);\n await run(`GRANT ${USER_TABLE_PRIVILEGES} ON ALL TABLES IN SCHEMA ${s} TO ${REBASE_USER_ROLE}`);\n await run(`GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA ${s} TO ${REBASE_USER_ROLE}`);\n // Cover objects created later by the CURRENT role (the role that runs\n // migrations), so a migrate can never strand the user role.\n await run(`ALTER DEFAULT PRIVILEGES IN SCHEMA ${s} GRANT ${USER_TABLE_PRIVILEGES} ON TABLES TO ${REBASE_USER_ROLE}`);\n await run(`ALTER DEFAULT PRIVILEGES IN SCHEMA ${s} GRANT USAGE, SELECT ON SEQUENCES TO ${REBASE_USER_ROLE}`);\n\n // The grants above are deliberately schema-wide — a project's own\n // collections may live in `rebase`, and future tables must be reachable\n // or a migration strands the role. Rebase's OWN tables are the exception:\n // refresh tokens, MFA secrets, API keys and the rest carry no RLS and no\n // row an end user should ever address. Taking the privilege back here\n // covers every table that already exists; each creator revokes on the\n // table it just made, for the boot that creates them for the first time.\n await revokeInternalTableAccess(async (text) => { await run(text); }, schema, {\n onError: (table, error) => logger.warn(\n `🔐 [rls] Could not revoke \"${REBASE_USER_ROLE}\" access to \"${schema}\".\"${table}\" — ` +\n \"it stays reachable by authenticated requests: \" +\n (error instanceof Error ? error.message : String(error))\n )\n });\n }\n\n logger.debug(`🔐 [rls] User role \"${REBASE_USER_ROLE}\" provisioned (schemas: ${uniqueSchemas.join(\", \")})`);\n}\n\n/**\n * Apply the authenticated context to a transaction: the `app.*` GUCs that RLS\n * policies read via `rebase.uid()` / `rebase.roles()` / `rebase.jwt()`, and — when\n * `userRole` is set — `SET LOCAL ROLE` so RLS binds every statement in this\n * transaction (reads *and* writes).\n *\n * GUCs are set with `is_local = true` and the role switch is `LOCAL`: both\n * reset at commit/rollback, so pooled connections are never polluted.\n *\n * Fails closed by construction: if the role switch errors, the transaction\n * aborts instead of proceeding privileged.\n *\n * SECURITY: this function is only ever called on the **user** path (the server\n * context uses the base/owner driver and never calls it). The default policies\n * treat `rebase.uid() IS NULL` as the trusted server context, and `rebase.uid()`\n * is `NULLIF(current_setting('app.uid'), '')` — so an EMPTY user id would\n * be read as NULL and silently escalate a user request to server privileges.\n * Coerce empty/blank ids to `ANONYMOUS_USER_ID` here, at the single chokepoint,\n * rather than trusting every caller (e.g. realtime subscription auth) to do it.\n * That sentinel is exported from `@rebasepro/types` because it leaks into rule\n * semantics: it is why `rebase.uid() IS NOT NULL` is true for anonymous requests.\n */\nexport async function applyAuthContext(tx: SqlTx, auth: AuthContext, userRole?: string): Promise<void> {\n const uid = typeof auth.uid === \"string\" && auth.uid.trim() !== \"\" ? auth.uid : ANONYMOUS_USER_ID;\n const normalizedRoles = auth.roles.map((r: unknown) =>\n typeof r === \"string\" ? r : (r as Record<string, unknown>)?.id ?? String(r)\n );\n // `app.user_id` is the pre-rename spelling, still written because policies\n // are data: a database provisioned before the rename holds rules compiled\n // to `current_setting('app.user_id')`, and those predicates would evaluate\n // to NULL — failing open or locking out — if we stopped setting it. Drop\n // the alias only once no live database carries a legacy policy.\n await tx.execute(drizzleSql`\n SELECT\n set_config('app.uid', ${uid}, true),\n set_config('app.user_id', ${uid}, true),\n set_config('app.user_roles', ${normalizedRoles.join(\",\")}, true),\n set_config('app.jwt', ${JSON.stringify({ sub: uid, roles: auth.roles })}, true)\n `);\n if (userRole) {\n await tx.execute(drizzleSql.raw(`SET LOCAL ROLE ${quoteIdent(userRole)}`));\n }\n}\n\n/** Role names from other BaaS platforms that people reach for out of habit. */\nconst FOREIGN_CONVENTION_ROLES: Record<string, string> = {\n authenticated: \"Supabase\",\n anon: \"Supabase\",\n service_role: \"Supabase\"\n};\n\n/**\n * Warn about rules that read as \"signed-in users only\" but admit anonymous\n * callers — `rebase.uid() IS NOT NULL`, or a comparison against another\n * platform's magic user id such as `'anon'`.\n *\n * The sibling of {@link validatePolicyPgRoles}, for the more dangerous spelling\n * of the same habit. A foreign `pgRoles` value makes a policy unreachable and\n * the table reads empty — loud, and that guard throws. These do the opposite:\n * the rule compiles to a grant, and nothing looks wrong until the data is\n * already public.\n *\n * Warns rather than throws. Unlike an unreachable `pgRoles`, these rules are\n * serving traffic today: refusing to boot would take an app offline to report a\n * problem it already has, and on the read path it would take it offline\n * *because* its data was exposed. Rewriting the author's SQL is not an option\n * either — this is the escape hatch whose whole promise is that it means what it\n * says. So: say so, loudly, and leave the rule alone.\n */\nexport function warnOnAnonymousGrants(\n collections: { slug?: string; securityRules?: readonly SecurityRule[] }[]\n): void {\n // Grouped by the mistake, not by the rule: one habit typically repeats\n // across every collection an author wrote, and a per-rule list would repeat\n // the same paragraph dozens of times and get skimmed.\n const byRisk = new Map<string, { risk: AnonymousGrantRisk; sites: string[] }>();\n\n for (const collection of collections) {\n for (const rule of collection.securityRules ?? []) {\n const { usingExpr, withCheckExpr } = securityRuleToConditions(rule);\n const risks = [usingExpr, withCheckExpr]\n .filter((e): e is PolicyExpression => e !== null)\n .flatMap(findAnonymousGrants);\n\n for (const risk of risks) {\n const key = `${risk.pattern}:${risk.detail}`;\n const site = `${collection.slug ?? \"(unnamed)\"} → \"${rule.name ?? \"(unnamed rule)\"}\"`;\n const entry = byRisk.get(key) ?? { risk, sites: [] };\n if (!entry.sites.includes(site)) entry.sites.push(site);\n byRisk.set(key, entry);\n }\n }\n }\n\n if (byRisk.size === 0) return;\n\n const problems = [...byRisk.values()].map(({ risk, sites }) =>\n ` • ${risk.explanation}\\n ${sites.length} rule(s): ${sites.join(\", \")}`\n );\n\n logger.warn(\n `Security rules that read as a lockdown but grant access to anonymous requests. Every caller from a ` +\n `client carries a user id ('${ANONYMOUS_USER_ID}' when nobody is signed in), so these clauses are ` +\n `true for everyone:\\n\\n` +\n problems.join(\"\\n\\n\") + \"\\n\"\n );\n}\n\n/**\n * Name the collections whose raw policy SQL still calls the pre-1.0 helpers.\n *\n * The compiler rewrites `auth.uid()` to `rebase.uid()` on the way into the\n * database, so nothing is broken and no policy is wrong — which is exactly why\n * this has to be said out loud. A silent rewrite that works forever is not a\n * migration, it is a second supported spelling nobody wrote down, and the next\n * person to read those rules will copy the old one.\n *\n * Only `raw` expressions can carry it. Structured rules (`policy.authUid()`,\n * `policy.rolesOverlap(...)`) compile from the model and were never affected.\n */\nexport function warnOnLegacyRlsFunctions(\n collections: { slug?: string; securityRules?: readonly SecurityRule[] }[]\n): void {\n const sites: string[] = [];\n\n for (const collection of collections) {\n for (const rule of collection.securityRules ?? []) {\n const { usingExpr, withCheckExpr } = securityRuleToConditions(rule);\n const carriesLegacy = [usingExpr, withCheckExpr]\n .filter((e): e is PolicyExpression => e !== null)\n .some(containsLegacyRlsCall);\n if (!carriesLegacy) continue;\n\n const site = `${collection.slug ?? \"(unnamed)\"} → \"${rule.name ?? \"(unnamed rule)\"}\"`;\n if (!sites.includes(site)) sites.push(site);\n }\n }\n\n if (sites.length === 0) return;\n\n logger.warn(\n `These security rules call the pre-1.0 RLS helpers (\\`auth.uid()\\`, \\`auth.roles()\\`, \\`auth.jwt()\\`). ` +\n `They still work — the compiler rewrites them — but the functions now live in the \\`rebase\\` schema, ` +\n `and the \\`auth\\` one is Supabase's. Update the raw SQL in these rules to \\`${REBASE_SCHEMA}.uid()\\` ` +\n `and friends, or switch them to the structured helpers (\\`policy.authUid()\\`, \\`policy.rolesOverlap()\\`), ` +\n `which never had to be spelled by hand:\\n\\n` +\n sites.map(s => ` • ${s}`).join(\"\\n\") + \"\\n\"\n );\n}\n\n/** Whether any `raw` expression in the tree calls a pre-1.0 helper. */\nfunction containsLegacyRlsCall(expr: PolicyExpression): boolean {\n switch (expr.kind) {\n case \"raw\":\n return usesLegacyRlsFunctions(expr.sql);\n case \"and\":\n case \"or\":\n return expr.operands.some(containsLegacyRlsCall);\n case \"not\":\n return containsLegacyRlsCall(expr.operand);\n case \"existsIn\":\n return containsLegacyRlsCall(expr.where);\n default:\n return false;\n }\n}\n\n/**\n * Reject `pgRoles` that this server can never satisfy.\n *\n * `pgRoles` sets the `TO` clause of a generated policy, so a policy naming a\n * role the request never runs as simply never applies — and RLS then filters\n * every row. The table reads as empty, which is indistinguishable from having\n * no data, so the mistake survives review and ships.\n *\n * Requests run as `rebase_user`, so a policy is only reachable if it targets\n * `public` or a role `rebase_user` holds. Anything else is a configuration\n * error worth failing the boot for.\n */\nexport async function validatePolicyPgRoles(\n run: RawSqlRunner,\n collections: { slug?: string; securityRules?: readonly { name?: string; pgRoles?: readonly string[] }[] }[],\n /** The role requests actually run as: `rebase_user` when the connection is\n * privileged enough to switch, otherwise the connection role itself. */\n requestRole: string = REBASE_USER_ROLE\n): Promise<void> {\n const wanted = new Map<string, string[]>();\n for (const collection of collections) {\n for (const rule of collection.securityRules ?? []) {\n for (const role of rule.pgRoles ?? []) {\n if (role === \"public\") continue;\n wanted.set(role, [...(wanted.get(role) ?? []), collection.slug ?? \"(unnamed)\"]);\n }\n }\n }\n if (wanted.size === 0) return;\n\n const names = [...wanted.keys()].map((r) => `'${r.replace(/'/g, \"''\")}'`).join(\",\");\n const escapedRequestRole = requestRole.replace(/'/g, \"''\");\n const rows = await run(`\n SELECT r.rolname AS role,\n COALESCE(pg_has_role(to_regrole('${escapedRequestRole}'), r.oid, 'MEMBER'), false) AS reachable\n FROM pg_roles r\n WHERE r.rolname IN (${names})\n `);\n\n const reachable = new Map(rows.map((row) => [String(row.role), row.reachable === true]));\n const problems: string[] = [];\n\n for (const [role, slugs] of wanted) {\n if (reachable.get(role) === true) continue;\n\n const why = reachable.has(role)\n ? `\"${requestRole}\" is not a member of it`\n : \"no such role exists in this database\";\n const platform = FOREIGN_CONVENTION_ROLES[role];\n const hint = platform\n ? `\"${role}\" is a ${platform} convention, not a PostgreSQL role. Application roles belong in \\`roles: [\"${role === \"service_role\" ? \"admin\" : role}\"]\\`, which is checked inside the policy via rebase.roles().`\n : `Either grant it (GRANT ${role} TO ${requestRole}) or drop \\`pgRoles\\` so the policy targets \\`public\\`.`;\n\n problems.push(\n ` • pgRoles: [\"${role}\"] on ${slugs.join(\", \")} — ${why}.\\n ${hint}`\n );\n }\n\n if (problems.length > 0) {\n throw new Error(\n `Security rules target PostgreSQL roles this server cannot use. Requests run as ` +\n `\"${requestRole}\", so these policies would never apply and every row would be ` +\n `filtered out — the collections would look empty rather than error.\\n\\n` +\n problems.join(\"\\n\\n\") + \"\\n\"\n );\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"rls-enforcement-DvAbL9YJ.js","names":[],"sources":["../../common/src/util/internal-tables.ts","../src/security/rls-enforcement.ts"],"sourcesContent":["/**\n * The tables Rebase creates for its own bookkeeping, and the SQL that keeps the\n * end-user role away from them.\n *\n * ## Why this exists\n *\n * Authenticated requests run as {@link REBASE_USER_ROLE}, and the boot-time role\n * provisioning grants that role `SELECT, INSERT, UPDATE, DELETE` on every table\n * in the schemas a project uses — including `rebase`, because a project's own\n * collections are allowed to live there (the scaffold puts `users` there). It\n * also sets `ALTER DEFAULT PRIVILEGES`, so a table created *later* by the\n * migrating role inherits the same grant.\n *\n * Every framework-internal table is created later: auth's tables come up during\n * `initializeAuth`, `api_keys` during route mounting, `cron_logs` when the first\n * job registers, `idempotency_keys` on the first request that carries a key. So\n * they all inherited full DML for the end-user role — and none of them enables\n * row-level security, because none of them is a collection with\n * `securityRules`. Measured on a freshly provisioned database, `SET ROLE\n * rebase_user` could read `rebase.refresh_tokens` (session token hashes),\n * `rebase.mfa_factors` (`secret_encrypted`), `rebase.recovery_codes`, and\n * `rebase.api_keys` (including its `admin` flag), and insert into\n * `rebase.app_config`.\n *\n * Nothing routes a user-context query at those tables today, so this was not\n * reachable over the API. That is the wrong thing to depend on: the documented\n * model is that RLS is the authorization boundary, and these tables sat outside\n * it. The boundary is now a privilege boundary instead — the role simply cannot\n * address them.\n *\n * ## Why REVOKE rather than ENABLE ROW LEVEL SECURITY\n *\n * RLS with no policy denies every row, which is the same outcome, but it is the\n * *weaker* statement: it leaves the grant in place, so a later policy — or a\n * `FORCE` flag cleared by some future migration — reopens the table. There is no\n * row of `refresh_tokens` any end user should ever reach, so the honest encoding\n * is \"this role has no privilege here at all\". It also keeps the owner\n * connection (which auth actually runs on) completely unaffected.\n *\n * ## Keeping it true\n *\n * `packages/rls-check` scans the `rebase` schema — it used to skip it as a\n * \"platform\" schema — and its `rls-disabled` check fires on exactly the\n * condition this module removes: RLS off *and* a DML grant to a reachable role.\n * So a table added here without a revoke is caught by `pnpm rls:check`, not by\n * someone re-reading this file.\n */\n\n/**\n * The Postgres role authenticated requests run as.\n *\n * Defined here rather than in the Postgres driver because both the driver (which\n * provisions the role) and this module (which revokes on its behalf) need it,\n * and a second spelling of a role name is a silent no-op waiting to happen.\n */\nexport const REBASE_USER_ROLE = \"rebase_user\";\n\n/**\n * Framework-internal table names, unqualified.\n *\n * Deliberately NOT including `users`: the auth user table is also a collection,\n * with `securityRules`, RLS enabled and policies applied. Users read their own\n * row through it — revoking there would break sign-in. `revokeInternalTableSql`\n * now skips any table with RLS enabled, so that exception is enforced rather\n * than merely remembered — and so is the same hazard for every other name here,\n * any of which a project may legitimately use for a collection of its own.\n *\n * `atlas_schema_revisions` is Atlas's migration ledger, which lands in `rebase`\n * because `db migrate apply` passes `--revisions-schema rebase`.\n *\n * Every entry here must also be revoked by whatever creates it, and vice versa:\n * the creation-time revoke fires once, on the boot that first makes the table,\n * so it cannot help a database provisioned before that revoke existed. This\n * list is what the boot-time sweep in `ensureAppRole` iterates, and the sweep\n * is the only thing that can repair an already-granted table. A table revoked\n * at creation but missing here is therefore permanently stranded on any\n * database that predates its revoke.\n *\n * These names are unqualified, and the boot-time sweep applies them to every\n * schema a project uses — so an entry here is a claim on that name in `public`\n * as much as in `rebase`. `jobs` is Rebase's queue at `rebase.jobs` AND a\n * perfectly ordinary collection name, and revoking `public.jobs` from a project\n * that owns it leaves every read failing 42501 with correct policies applied\n * and nothing in the RLS logs to explain it. The `relrowsecurity` guard in\n * `revokeInternalTableSql` is what makes a common noun safe here — it is not a\n * licence to claim more of them.\n */\nexport const REBASE_INTERNAL_TABLES: readonly string[] = [\n // auth\n \"user_identities\",\n \"refresh_tokens\",\n \"password_reset_tokens\",\n \"magic_link_tokens\",\n \"mfa_factors\",\n \"mfa_challenges\",\n \"recovery_codes\",\n \"app_config\",\n \"schema_meta\",\n // platform services\n \"api_keys\",\n \"cron_logs\",\n \"cron_claims\",\n \"jobs\",\n \"rate_limit_hits\",\n \"idempotency_keys\",\n \"entity_history\",\n \"branches\",\n \"metric_samples\",\n // realtime channels — authorization for these lives in the channel rules the\n // server evaluates before it reads or writes, never in a row policy\n \"channel_messages\",\n \"channel_cursors\",\n \"channel_presence\",\n // migration bookkeeping\n \"atlas_schema_revisions\"\n];\n\n/** Postgres identifiers this module is willing to interpolate. */\nconst SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*$/;\n\n/**\n * A single statement that takes every privilege on `schema.table` away from the\n * end-user role.\n *\n * Wrapped in a `DO` block guarded on `pg_roles` for two reasons, both of which\n * happen in practice:\n *\n * - the role does not exist when the connection is unprivileged (Rebase then\n * relies on native RLS rather than a role switch), and a bare `REVOKE` on a\n * missing role is an error, not a no-op;\n * - the table may not exist yet — `cron_logs` never appears in a project with\n * no cron jobs — and `to_regclass` returning NULL has to be tolerated too.\n *\n * The third guard is the one that decides whether the *right* table is being\n * revoked. The names in {@link REBASE_INTERNAL_TABLES} are unqualified, and the\n * boot-time sweep in `ensureAppRole` applies all of them to every schema a\n * project uses — including the schema its own collections live in. A project is\n * free to call a collection `jobs`, `branches` or `api_keys`, and when it does,\n * the sweep was revoking `rebase_user`'s DML on the project's table on every\n * single boot. That is not a subtle degradation: the collection's whole API\n * answers 500 `permission denied for table …` from then on, which is what\n * happened to a public job board whose vacancies live in `public.jobs`.\n *\n * `relrowsecurity` separates the two cleanly, and it is the same fact this\n * module already relies on. Framework-internal tables carry no RLS — that is the\n * premise stated at the top of this file, and the reason a revoke is needed at\n * all. Every collection table has it enabled, because that is how Rebase\n * enforces `securityRules`. So \"RLS is off\" is exactly \"this is not somebody's\n * collection\", and the guard also subsumes the hand-carved `users` exception:\n * the auth user table is a collection, has RLS, and would now be skipped on its\n * own merits rather than by being kept off a list.\n *\n * One command, so it is safe on handles that speak the extended query protocol\n * and reject multi-statement strings.\n */\nexport function revokeInternalTableSql(schema: string, table: string): string {\n if (!SAFE_IDENTIFIER.test(schema)) {\n throw new Error(`Refusing to build SQL with an unsafe schema name: ${JSON.stringify(schema)}`);\n }\n if (!SAFE_IDENTIFIER.test(table)) {\n throw new Error(`Refusing to build SQL with an unsafe table name: ${JSON.stringify(table)}`);\n }\n const qualified = `\"${schema}\".\"${table}\"`;\n return `\n DO $rebase_revoke$\n BEGIN\n IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${REBASE_USER_ROLE}')\n AND to_regclass('${qualified}') IS NOT NULL\n AND NOT (SELECT relrowsecurity FROM pg_class WHERE oid = to_regclass('${qualified}')) THEN\n EXECUTE 'REVOKE ALL ON ${qualified} FROM ${REBASE_USER_ROLE}';\n END IF;\n END\n $rebase_revoke$;\n `.trim();\n}\n\n/**\n * Revoke on every internal table in `schema`, one statement at a time.\n *\n * Best-effort per table: a connection that does not own one of them (a\n * pre-provisioned database, a platform-managed ledger) cannot revoke on it, and\n * that must not take down a boot. The caller decides how loud to be — `onError`\n * exists so the driver can warn without this module importing a logger.\n */\nexport async function revokeInternalTableAccess(\n execute: (sql: string) => Promise<unknown>,\n schema: string,\n options?: { tables?: readonly string[]; onError?: (table: string, error: unknown) => void }\n): Promise<void> {\n for (const table of options?.tables ?? REBASE_INTERNAL_TABLES) {\n try {\n await execute(revokeInternalTableSql(schema, table));\n } catch (error) {\n options?.onError?.(table, error);\n }\n }\n}\n","import { sql as drizzleSql, SQL } from \"drizzle-orm\";\nimport { ANONYMOUS_USER_ID, PolicyExpression, SecurityRule } from \"@rebasepro/types\";\nimport {\n AnonymousGrantRisk,\n findAnonymousGrants,\n REBASE_USER_ROLE,\n revokeInternalTableAccess,\n securityRuleToConditions\n} from \"@rebasepro/common\";\nimport { REBASE_SCHEMA, usesLegacyRlsFunctions } from \"@rebasepro/types\";\nimport { logger } from \"@rebasepro/server\";\n\n/**\n * Unified RLS enforcement — the \"user context vs server context\" model.\n *\n * Every operation runs in one of two contexts:\n *\n * - **User context** — a request authenticated (or anonymous) via\n * `driver.withAuth(user)`. Runs as the restricted `rebase_user` role: a\n * non-owner, NOSUPERUSER, NOBYPASSRLS role, so Postgres RLS binds *every*\n * statement (SELECT, INSERT, UPDATE, DELETE). The collection's\n * `securityRules` are the whole authorization model; app-layer callbacks\n * are validation/side-effects, not a security boundary.\n *\n * - **Server context** — the base (owner) connection: auth flows, migrations,\n * and raw `rebase.sql`. As table owner it bypasses RLS. This is the trusted\n * plane, equivalent to Supabase's `service_role`.\n *\n * `rebase.dataAsAdmin` is **not** in it, despite the name. `init.ts` scopes\n * that driver with `withAuth(SERVICE_IDENTITY)`, so it arrives as user\n * context above — `rebase_user`, `app.uid = 'service'`, policies evaluated —\n * and clears the default policies through their admin arm rather than the\n * `rebase.uid() IS NULL` one.\n *\n * This module provides the three pieces:\n *\n * 1. {@link detectConnectionPosture} — is the connection subject to RLS at\n * all? (superuser / BYPASSRLS / table owner ⇒ no)\n * 2. {@link ensureAppRole} — idempotently provision `rebase_user` with\n * SELECT/INSERT/UPDATE/DELETE grants (+ default privileges so future\n * tables stay covered).\n * 3. {@link applyAuthContext} — per-transaction: set the `app.*` GUCs the\n * policies read (`rebase.uid()` etc.) and `SET LOCAL ROLE rebase_user` so\n * RLS binds. Transaction-scoped, so it composes with poolers.\n *\n * Provisioning runs from the framework's own bootstrap/migrate (which already\n * self-creates the `auth` schema and functions) — enforcement is default-on,\n * not an operator opt-in.\n */\n\n/**\n * The restricted role every authenticated (user-context) request runs as.\n *\n * Re-exported, not re-declared: the same name is needed by\n * `@rebasepro/common`'s internal-table revokes, and two spellings of a role name\n * fail as a silent no-op rather than an error.\n */\nexport { REBASE_USER_ROLE };\n\n/** Minimal SQL runner so callers can adapt drizzle or pg.Client. */\nexport type RawSqlRunner = (sqlText: string) => Promise<Record<string, unknown>[]>;\n\n/** Minimal transaction surface needed by {@link applyAuthContext}. */\nexport interface SqlTx {\n execute(query: SQL): Promise<unknown>;\n}\n\nexport interface ConnectionPosture {\n /** The connection's `current_user`. */\n role: string;\n superuser: boolean;\n bypassRLS: boolean;\n /** Owns at least one user table — owners bypass non-FORCE RLS. */\n ownsTables: boolean;\n /** True when RLS would NOT constrain this connection. */\n privileged: boolean;\n}\n\nexport interface AuthContext {\n uid: string;\n /** Raw roles as carried on the user (strings or `{ id }` objects). */\n roles: unknown[];\n}\n\nconst quoteIdent = (name: string): string => `\"${name.replace(/\"/g, \"\\\"\\\"\")}\"`;\n\n/** DML the user role holds on managed tables (RLS still filters per row). */\nconst USER_TABLE_PRIVILEGES = \"SELECT, INSERT, UPDATE, DELETE\";\n\n/**\n * Warn when the connection role shares its name with an existing schema.\n *\n * Postgres resolves unqualified names through `search_path`, which defaults to\n * `\"$user\", public` — and `$user` is the connection ROLE. When a schema of that\n * name exists it sits ahead of `public`, so every unqualified statement\n * silently operates on it instead:\n *\n * CREATE TABLE posts (...); -- you meant public.posts; you got <role>.posts\n *\n * Nothing errors. You get a second table of the same name in the wrong schema,\n * and reads that pin `public` cannot see it — which reads as \"missing table\" and\n * sends people to re-run a push that creates a *third* copy. The bootstrapper\n * has a whole branch dedicated to recognising the symptom after the fact.\n *\n * Rebase shipped straight into this: it creates a schema named `rebase` while\n * every template named the database role `rebase` too. The scaffold uses\n * `rebase_app` now, and every pool Rebase opens pins `search_path=public`\n * (`pinSearchPath`), which covers the paths the framework controls. This covers\n * the ones it does not — `psql`, `pg_dump`, drizzle-kit, a colleague's script,\n * a hand-written migration — because the hazard is a property of the two NAMES,\n * not of any one connection.\n *\n * A warning rather than a boot failure: the database works, the framework's own\n * traffic is pinned, and refusing to start over a naming choice a user may have\n * inherited would be worse than the risk.\n */\nexport async function warnOnRoleSchemaCollision(run: RawSqlRunner): Promise<void> {\n try {\n const rows = await run(`\n SELECT current_user AS role,\n EXISTS (\n SELECT 1 FROM pg_namespace n WHERE n.nspname = current_user\n ) AS collides\n `);\n if (rows[0]?.collides !== true) return;\n const role = String(rows[0]?.role ?? \"the connection role\");\n logger.warn(\n `⚠️ The database role \"${role}\" has the same name as a schema. Postgres resolves unqualified ` +\n `names through \\`search_path\\`, which defaults to \\`\"$user\", public\\` — so \"${role}\" is searched ` +\n `BEFORE public, and any unqualified \\`CREATE TABLE\\`/\\`SELECT\\` from a tool that does not pin the ` +\n `path (psql, pg_dump, drizzle-kit, a hand-written migration) silently lands in \"${role}\" instead. ` +\n `Rebase's own connections pin \\`search_path=public\\`, so the server is unaffected. To remove the ` +\n `hazard entirely, connect as a role whose name is not also a schema — the scaffold uses ` +\n `\"rebase_app\".`\n );\n } catch {\n // A diagnostic must never be the reason a boot fails.\n }\n}\n\nexport async function detectConnectionPosture(run: RawSqlRunner): Promise<ConnectionPosture> {\n const rows = await run(`\n SELECT current_user AS role,\n r.rolsuper AS superuser,\n r.rolbypassrls AS bypassrls,\n EXISTS (\n SELECT 1 FROM pg_tables t\n WHERE t.tableowner = current_user\n AND t.schemaname NOT IN ('pg_catalog', 'information_schema')\n ) AS owns_tables\n FROM pg_roles r\n WHERE r.rolname = current_user\n `);\n const row = rows[0] ?? {};\n const superuser = row.superuser === true;\n const bypassRLS = row.bypassrls === true;\n const ownsTables = row.owns_tables === true;\n return {\n role: String(row.role ?? \"unknown\"),\n superuser,\n bypassRLS,\n ownsTables,\n privileged: superuser || bypassRLS || ownsTables\n };\n}\n\n/**\n * Human-actionable instructions for when the connection cannot provision the\n * user role itself (no CREATEROLE and role not pre-created by the platform).\n */\nexport function appRoleSetupInstructions(connectionRole: string, schemas: string[]): string {\n const grants = schemas.map((s) =>\n `GRANT USAGE ON SCHEMA ${quoteIdent(s)} TO ${REBASE_USER_ROLE};\\n` +\n `GRANT ${USER_TABLE_PRIVILEGES} ON ALL TABLES IN SCHEMA ${quoteIdent(s)} TO ${REBASE_USER_ROLE};\\n` +\n `GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA ${quoteIdent(s)} TO ${REBASE_USER_ROLE};`\n ).join(\"\\n\");\n return (\n `Rebase enforces row-level security by running authenticated requests as ` +\n `the restricted role \"${REBASE_USER_ROLE}\", but the connection role ` +\n `\"${connectionRole}\" bypasses RLS and cannot create that role itself.\\n` +\n `Run the following as a database administrator, then restart:\\n\\n` +\n `CREATE ROLE ${REBASE_USER_ROLE} NOLOGIN NOSUPERUSER NOBYPASSRLS NOINHERIT;\\n` +\n `GRANT ${REBASE_USER_ROLE} TO ${quoteIdent(connectionRole)};\\n` +\n grants\n );\n}\n\n/**\n * Idempotently provision the `rebase_user` role, membership for the current\n * connection role, and DML grants (+ default privileges for future tables)\n * on every existing schema in `schemas`.\n *\n * Split into privilege tiers so it works both when the connection is a\n * superuser (creates everything) and when the platform pre-created the role\n * and membership (e.g. CNPG `postInitApplicationSQL`) and the connection is\n * merely the table owner — owners can always run the grant tier themselves.\n *\n * RLS still filters every row: these grants only make the tables *reachable*\n * by the role; the policies decide which rows/commands actually pass.\n *\n * Throws with precise setup instructions when the role is missing and the\n * connection cannot create it.\n */\nexport async function ensureAppRole(run: RawSqlRunner, schemas: string[]): Promise<void> {\n const uniqueSchemas = Array.from(new Set(schemas.filter(Boolean)));\n\n // Tier 1 — role existence.\n const roleRows = await run(`SELECT 1 FROM pg_roles WHERE rolname = '${REBASE_USER_ROLE}'`);\n if (roleRows.length === 0) {\n try {\n await run(`CREATE ROLE ${REBASE_USER_ROLE} NOLOGIN NOSUPERUSER NOBYPASSRLS NOINHERIT`);\n } catch (err) {\n throw new Error(\n `Failed to create the \"${REBASE_USER_ROLE}\" role: ${err instanceof Error ? err.message : String(err)}\\n\\n` +\n appRoleSetupInstructions(\"current connection role\", uniqueSchemas)\n );\n }\n }\n\n // Tier 2 — membership, so a non-superuser connection may SET ROLE to it.\n const memberRows = await run(`\n SELECT (pg_has_role(current_user, '${REBASE_USER_ROLE}', 'MEMBER')\n OR (SELECT rolsuper FROM pg_roles WHERE rolname = current_user)) AS can_set,\n current_user AS role\n `);\n if (memberRows[0]?.can_set !== true) {\n try {\n await run(`GRANT ${REBASE_USER_ROLE} TO CURRENT_USER`);\n } catch (err) {\n throw new Error(\n `The connection role is not a member of \"${REBASE_USER_ROLE}\" and cannot grant itself membership: ` +\n `${err instanceof Error ? err.message : String(err)}\\n\\n` +\n appRoleSetupInstructions(String(memberRows[0]?.role ?? \"current connection role\"), uniqueSchemas)\n );\n }\n }\n\n // Tier 3 — grants. Table owners (the expected non-superuser posture) can\n // always grant on their own objects, so this tier needs no extra privilege.\n const nspRows = await run(\"SELECT nspname FROM pg_namespace\");\n const existing = new Set(nspRows.map((r) => String(r.nspname)));\n for (const schema of uniqueSchemas) {\n if (!existing.has(schema)) continue;\n const s = quoteIdent(schema);\n await run(`GRANT USAGE ON SCHEMA ${s} TO ${REBASE_USER_ROLE}`);\n await run(`GRANT ${USER_TABLE_PRIVILEGES} ON ALL TABLES IN SCHEMA ${s} TO ${REBASE_USER_ROLE}`);\n await run(`GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA ${s} TO ${REBASE_USER_ROLE}`);\n // Cover objects created later by the CURRENT role (the role that runs\n // migrations), so a migrate can never strand the user role.\n await run(`ALTER DEFAULT PRIVILEGES IN SCHEMA ${s} GRANT ${USER_TABLE_PRIVILEGES} ON TABLES TO ${REBASE_USER_ROLE}`);\n await run(`ALTER DEFAULT PRIVILEGES IN SCHEMA ${s} GRANT USAGE, SELECT ON SEQUENCES TO ${REBASE_USER_ROLE}`);\n\n // The grants above are deliberately schema-wide — a project's own\n // collections may live in `rebase`, and future tables must be reachable\n // or a migration strands the role. Rebase's OWN tables are the exception:\n // refresh tokens, MFA secrets, API keys and the rest carry no RLS and no\n // row an end user should ever address. Taking the privilege back here\n // covers every table that already exists; each creator revokes on the\n // table it just made, for the boot that creates them for the first time.\n await revokeInternalTableAccess(async (text) => { await run(text); }, schema, {\n onError: (table, error) => logger.warn(\n `🔐 [rls] Could not revoke \"${REBASE_USER_ROLE}\" access to \"${schema}\".\"${table}\" — ` +\n \"it stays reachable by authenticated requests: \" +\n (error instanceof Error ? error.message : String(error))\n )\n });\n }\n\n logger.debug(`🔐 [rls] User role \"${REBASE_USER_ROLE}\" provisioned (schemas: ${uniqueSchemas.join(\", \")})`);\n}\n\n/**\n * Apply the authenticated context to a transaction: the `app.*` GUCs that RLS\n * policies read via `rebase.uid()` / `rebase.roles()` / `rebase.jwt()`, and — when\n * `userRole` is set — `SET LOCAL ROLE` so RLS binds every statement in this\n * transaction (reads *and* writes).\n *\n * GUCs are set with `is_local = true` and the role switch is `LOCAL`: both\n * reset at commit/rollback, so pooled connections are never polluted.\n *\n * Fails closed by construction: if the role switch errors, the transaction\n * aborts instead of proceeding privileged.\n *\n * SECURITY: this function is only ever called on the **user** path (the server\n * context uses the base/owner driver and never calls it). The default policies\n * treat `rebase.uid() IS NULL` as the trusted server context, and `rebase.uid()`\n * is `NULLIF(current_setting('app.uid'), '')` — so an EMPTY user id would\n * be read as NULL and silently escalate a user request to server privileges.\n * Coerce empty/blank ids to `ANONYMOUS_USER_ID` here, at the single chokepoint,\n * rather than trusting every caller (e.g. realtime subscription auth) to do it.\n * That sentinel is exported from `@rebasepro/types` because it leaks into rule\n * semantics: it is why `rebase.uid() IS NOT NULL` is true for anonymous requests.\n */\nexport async function applyAuthContext(tx: SqlTx, auth: AuthContext, userRole?: string): Promise<void> {\n const uid = typeof auth.uid === \"string\" && auth.uid.trim() !== \"\" ? auth.uid : ANONYMOUS_USER_ID;\n const normalizedRoles = auth.roles.map((r: unknown) =>\n typeof r === \"string\" ? r : (r as Record<string, unknown>)?.id ?? String(r)\n );\n // `app.user_id` is the pre-rename spelling, still written because policies\n // are data: a database provisioned before the rename holds rules compiled\n // to `current_setting('app.user_id')`, and those predicates would evaluate\n // to NULL — failing open or locking out — if we stopped setting it. Drop\n // the alias only once no live database carries a legacy policy.\n await tx.execute(drizzleSql`\n SELECT\n set_config('app.uid', ${uid}, true),\n set_config('app.user_id', ${uid}, true),\n set_config('app.user_roles', ${normalizedRoles.join(\",\")}, true),\n set_config('app.jwt', ${JSON.stringify({ sub: uid, roles: auth.roles })}, true)\n `);\n if (userRole) {\n await tx.execute(drizzleSql.raw(`SET LOCAL ROLE ${quoteIdent(userRole)}`));\n }\n}\n\n/** Role names from other BaaS platforms that people reach for out of habit. */\nconst FOREIGN_CONVENTION_ROLES: Record<string, string> = {\n authenticated: \"Supabase\",\n anon: \"Supabase\",\n service_role: \"Supabase\"\n};\n\n/**\n * Warn about rules that read as \"signed-in users only\" but admit anonymous\n * callers — `rebase.uid() IS NOT NULL`, or a comparison against another\n * platform's magic user id such as `'anon'`.\n *\n * The sibling of {@link validatePolicyPgRoles}, for the more dangerous spelling\n * of the same habit. A foreign `pgRoles` value makes a policy unreachable and\n * the table reads empty — loud, and that guard throws. These do the opposite:\n * the rule compiles to a grant, and nothing looks wrong until the data is\n * already public.\n *\n * Warns rather than throws. Unlike an unreachable `pgRoles`, these rules are\n * serving traffic today: refusing to boot would take an app offline to report a\n * problem it already has, and on the read path it would take it offline\n * *because* its data was exposed. Rewriting the author's SQL is not an option\n * either — this is the escape hatch whose whole promise is that it means what it\n * says. So: say so, loudly, and leave the rule alone.\n */\nexport function warnOnAnonymousGrants(\n collections: { slug?: string; securityRules?: readonly SecurityRule[] }[]\n): void {\n // Grouped by the mistake, not by the rule: one habit typically repeats\n // across every collection an author wrote, and a per-rule list would repeat\n // the same paragraph dozens of times and get skimmed.\n const byRisk = new Map<string, { risk: AnonymousGrantRisk; sites: string[] }>();\n\n for (const collection of collections) {\n for (const rule of collection.securityRules ?? []) {\n const { usingExpr, withCheckExpr } = securityRuleToConditions(rule);\n const risks = [usingExpr, withCheckExpr]\n .filter((e): e is PolicyExpression => e !== null)\n .flatMap(findAnonymousGrants);\n\n for (const risk of risks) {\n const key = `${risk.pattern}:${risk.detail}`;\n const site = `${collection.slug ?? \"(unnamed)\"} → \"${rule.name ?? \"(unnamed rule)\"}\"`;\n const entry = byRisk.get(key) ?? { risk, sites: [] };\n if (!entry.sites.includes(site)) entry.sites.push(site);\n byRisk.set(key, entry);\n }\n }\n }\n\n if (byRisk.size === 0) return;\n\n const problems = [...byRisk.values()].map(({ risk, sites }) =>\n ` • ${risk.explanation}\\n ${sites.length} rule(s): ${sites.join(\", \")}`\n );\n\n logger.warn(\n `Security rules that read as a lockdown but grant access to anonymous requests. Every caller from a ` +\n `client carries a user id ('${ANONYMOUS_USER_ID}' when nobody is signed in), so these clauses are ` +\n `true for everyone:\\n\\n` +\n problems.join(\"\\n\\n\") + \"\\n\"\n );\n}\n\n/**\n * Name the collections whose raw policy SQL still calls the pre-1.0 helpers.\n *\n * The compiler rewrites `auth.uid()` to `rebase.uid()` on the way into the\n * database, so nothing is broken and no policy is wrong — which is exactly why\n * this has to be said out loud. A silent rewrite that works forever is not a\n * migration, it is a second supported spelling nobody wrote down, and the next\n * person to read those rules will copy the old one.\n *\n * Only `raw` expressions can carry it. Structured rules (`policy.authUid()`,\n * `policy.rolesOverlap(...)`) compile from the model and were never affected.\n */\nexport function warnOnLegacyRlsFunctions(\n collections: { slug?: string; securityRules?: readonly SecurityRule[] }[]\n): void {\n const sites: string[] = [];\n\n for (const collection of collections) {\n for (const rule of collection.securityRules ?? []) {\n const { usingExpr, withCheckExpr } = securityRuleToConditions(rule);\n const carriesLegacy = [usingExpr, withCheckExpr]\n .filter((e): e is PolicyExpression => e !== null)\n .some(containsLegacyRlsCall);\n if (!carriesLegacy) continue;\n\n const site = `${collection.slug ?? \"(unnamed)\"} → \"${rule.name ?? \"(unnamed rule)\"}\"`;\n if (!sites.includes(site)) sites.push(site);\n }\n }\n\n if (sites.length === 0) return;\n\n logger.warn(\n `These security rules call the pre-1.0 RLS helpers (\\`auth.uid()\\`, \\`auth.roles()\\`, \\`auth.jwt()\\`). ` +\n `They still work — the compiler rewrites them — but the functions now live in the \\`rebase\\` schema, ` +\n `and the \\`auth\\` one is Supabase's. Update the raw SQL in these rules to \\`${REBASE_SCHEMA}.uid()\\` ` +\n `and friends, or switch them to the structured helpers (\\`policy.authUid()\\`, \\`policy.rolesOverlap()\\`), ` +\n `which never had to be spelled by hand:\\n\\n` +\n sites.map(s => ` • ${s}`).join(\"\\n\") + \"\\n\"\n );\n}\n\n/** Whether any `raw` expression in the tree calls a pre-1.0 helper. */\nfunction containsLegacyRlsCall(expr: PolicyExpression): boolean {\n switch (expr.kind) {\n case \"raw\":\n return usesLegacyRlsFunctions(expr.sql);\n case \"and\":\n case \"or\":\n return expr.operands.some(containsLegacyRlsCall);\n case \"not\":\n return containsLegacyRlsCall(expr.operand);\n case \"existsIn\":\n return containsLegacyRlsCall(expr.where);\n default:\n return false;\n }\n}\n\n/**\n * Reject `pgRoles` that this server can never satisfy.\n *\n * `pgRoles` sets the `TO` clause of a generated policy, so a policy naming a\n * role the request never runs as simply never applies — and RLS then filters\n * every row. The table reads as empty, which is indistinguishable from having\n * no data, so the mistake survives review and ships.\n *\n * Requests run as `rebase_user`, so a policy is only reachable if it targets\n * `public` or a role `rebase_user` holds. Anything else is a configuration\n * error worth failing the boot for.\n */\nexport async function validatePolicyPgRoles(\n run: RawSqlRunner,\n collections: { slug?: string; securityRules?: readonly { name?: string; pgRoles?: readonly string[] }[] }[],\n /** The role requests actually run as: `rebase_user` when the connection is\n * privileged enough to switch, otherwise the connection role itself. */\n requestRole: string = REBASE_USER_ROLE\n): Promise<void> {\n const wanted = new Map<string, string[]>();\n for (const collection of collections) {\n for (const rule of collection.securityRules ?? []) {\n for (const role of rule.pgRoles ?? []) {\n if (role === \"public\") continue;\n wanted.set(role, [...(wanted.get(role) ?? []), collection.slug ?? \"(unnamed)\"]);\n }\n }\n }\n if (wanted.size === 0) return;\n\n const names = [...wanted.keys()].map((r) => `'${r.replace(/'/g, \"''\")}'`).join(\",\");\n const escapedRequestRole = requestRole.replace(/'/g, \"''\");\n const rows = await run(`\n SELECT r.rolname AS role,\n COALESCE(pg_has_role(to_regrole('${escapedRequestRole}'), r.oid, 'MEMBER'), false) AS reachable\n FROM pg_roles r\n WHERE r.rolname IN (${names})\n `);\n\n const reachable = new Map(rows.map((row) => [String(row.role), row.reachable === true]));\n const problems: string[] = [];\n\n for (const [role, slugs] of wanted) {\n if (reachable.get(role) === true) continue;\n\n const why = reachable.has(role)\n ? `\"${requestRole}\" is not a member of it`\n : \"no such role exists in this database\";\n const platform = FOREIGN_CONVENTION_ROLES[role];\n const hint = platform\n ? `\"${role}\" is a ${platform} convention, not a PostgreSQL role. Application roles belong in \\`roles: [\"${role === \"service_role\" ? \"admin\" : role}\"]\\`, which is checked inside the policy via rebase.roles().`\n : `Either grant it (GRANT ${role} TO ${requestRole}) or drop \\`pgRoles\\` so the policy targets \\`public\\`.`;\n\n problems.push(\n ` • pgRoles: [\"${role}\"] on ${slugs.join(\", \")} — ${why}.\\n ${hint}`\n );\n }\n\n if (problems.length > 0) {\n throw new Error(\n `Security rules target PostgreSQL roles this server cannot use. Requests run as ` +\n `\"${requestRole}\", so these policies would never apply and every row would be ` +\n `filtered out — the collections would look empty rather than error.\\n\\n` +\n problems.join(\"\\n\\n\") + \"\\n\"\n );\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The argv `runAtlas` hands the Atlas binary, as a pure function.
3
+ *
4
+ * Split out of `cli.ts` because the flags an Atlas subcommand accepts are not
5
+ * uniform and getting one wrong is fatal rather than degraded: Atlas rejects an
6
+ * unknown flag before doing any work, so a flag on the wrong subcommand takes
7
+ * the whole command down.
8
+ *
9
+ * `--exclude` is the one that bit. Of the six invocations here, exactly one
10
+ * accepts it. The guard this replaces was `args.includes("apply") ||
11
+ * args.includes("diff")`, and reading it as a *subcommand* test is the trap:
12
+ * `migrate apply` passes `args.includes("apply")` just as `schema apply` does.
13
+ * So the flag went onto three invocations, two of which reject it, and both
14
+ * `rebase db generate` and `rebase db migrate` exited 1 with `unknown flag:
15
+ * --exclude` for every project declaring a `search` block.
16
+ *
17
+ * The domain is half the identity of an Atlas subcommand, and a guard that
18
+ * looks only at `args` cannot tell `schema apply` from `migrate apply`.
19
+ *
20
+ * Nothing here touches a database or the filesystem, so the whole matrix is
21
+ * unit-tested in `atlas-argv.test.ts`.
22
+ */
23
+ export interface AtlasInvocation {
24
+ /** `schema` or `migrate`. */
25
+ domain: string;
26
+ /** The subcommand and its own flags, e.g. `["diff", "--dir", "file://…"]`. */
27
+ args: string[];
28
+ /** The target database, already rewritten for libpq. */
29
+ url: string;
30
+ /** The database Atlas plans against, already rewritten for libpq. */
31
+ devUrl: string;
32
+ /**
33
+ * Everything the caller resolved for `--exclude`, in the order it should
34
+ * appear. Dropped entirely when the subcommand does not accept the flag —
35
+ * see {@link acceptsExcludeFlag}.
36
+ */
37
+ excludes?: string[];
38
+ }
39
+ /**
40
+ * Does this Atlas invocation accept `--exclude`?
41
+ *
42
+ * Only `atlas schema apply` does, on the pinned 1.2.3. Measured against the
43
+ * binary: `migrate diff` and `migrate apply` both answer `unknown flag:
44
+ * --exclude`, and neither lists it in `--help` (`migrate apply` offers
45
+ * `--url/--dir/--format/--revisions-schema/--dry-run/--lock-name/--lock-timeout/`
46
+ * `--skip-lock/--baseline/--to-version/--tx-mode/--exec-order/--allow-dirty`).
47
+ * Hence `domain === "schema"` and not just the subcommand.
48
+ *
49
+ * The `exclude` attribute of an `atlas.hcl` `env` block is not a way round it
50
+ * either: measured, it is accepted and then ignored on the diff path, which is
51
+ * the silent-no-match shape that costs the most time to notice.
52
+ *
53
+ * So the diff keeps the carved-out objects out of the migration afterwards
54
+ * instead, with `stripCarvedOutStatements`.
55
+ */
56
+ export declare function acceptsExcludeFlag(domain: string, args: string[]): boolean;
57
+ /** Assemble the full argv, connection flags and all. */
58
+ export declare function buildAtlasArgs(invocation: AtlasInvocation): string[];
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Keep the objects Rebase carved out of Atlas's view out of the migrations
3
+ * Atlas writes.
4
+ *
5
+ * `db push` protects them with `--exclude`. `atlas migrate diff` has no such
6
+ * flag — measured on the pinned 1.2.3, `--exclude` is rejected outright with
7
+ * `unknown flag`, and the `exclude` attribute of an `atlas.hcl` `env` block is
8
+ * accepted and then silently ignored on that path. So the diff sees the search
9
+ * column that the migration directory's own appended DDL created when it
10
+ * replayed, does not see it in `schema.sql`, and plans a drop:
11
+ *
12
+ * ALTER TABLE "public"."talents" DROP COLUMN "search_vector";
13
+ *
14
+ * That statement is removed here, after Atlas has written the file and before
15
+ * anything is appended to it — so the input is always Atlas's own plain output,
16
+ * never the dollar-quoted helper bodies that get appended afterwards.
17
+ *
18
+ * The drop does not arrive alone. Atlas folds every change to one table into a
19
+ * single statement, so a real edit in the same run produces
20
+ *
21
+ * ALTER TABLE "public"."talents"
22
+ * DROP COLUMN "search_vector", DROP COLUMN "interests", ADD COLUMN "headline" text NULL;
23
+ *
24
+ * and dropping the whole statement would silently throw away a migration the
25
+ * project asked for. The removal is therefore per *clause*.
26
+ *
27
+ * Fail-safe throughout: a statement that mentions a carved-out object but does
28
+ * not match a shape this understands is left exactly as it is and reported as
29
+ * `unhandled`, so the caller can say so out loud. A spurious drop the developer
30
+ * can see beats a migration quietly rewritten wrong.
31
+ */
32
+ /** One `schema.table.object` exclude pattern, taken apart. */
33
+ export interface CarvedOutObject {
34
+ schema: string;
35
+ table: string;
36
+ object: string;
37
+ }
38
+ export interface StripResult {
39
+ /** The migration text with the carved-out clauses gone. */
40
+ sql: string;
41
+ /** What was removed, as SQL fragments, for reporting. */
42
+ removed: string[];
43
+ /**
44
+ * Statements naming a carved-out object that this could not rewrite with
45
+ * confidence. Left untouched in `sql`.
46
+ */
47
+ unhandled: string[];
48
+ /** True when nothing but comments and whitespace survives. */
49
+ empty: boolean;
50
+ }
51
+ /**
52
+ * Split `schema.table.object` patterns. Anything not in three parts is
53
+ * ignored rather than guessed at — the two-part form is the one Atlas itself
54
+ * silently mis-reads, and repeating that mistake here would be worse than
55
+ * doing nothing.
56
+ */
57
+ export declare function parseExcludePatterns(patterns: string[]): CarvedOutObject[];
58
+ /**
59
+ * Remove every clause and statement that would drop one of `patterns`.
60
+ *
61
+ * `patterns` are the `schema.table.object` strings the CLI already builds for
62
+ * Atlas's `--exclude`, so the diff path and the apply path protect exactly the
63
+ * same set by construction.
64
+ */
65
+ export declare function stripCarvedOutStatements(migrationSql: string, patterns: string[]): StripResult;
@@ -106,6 +106,18 @@ export type ConstraintPolicy = "additive" | "converge";
106
106
  export interface EnsureOptions {
107
107
  /** Defaults to `additive`. See {@link ConstraintPolicy}. */
108
108
  constraints?: ConstraintPolicy;
109
+ /**
110
+ * Server extensions the project's databases gave Rebase leave to install —
111
+ * `declaredDatabaseExtensions()`. Absent means none, which is a refusal and
112
+ * is the right default for a planner given no configuration at all.
113
+ *
114
+ * Explicit rather than read from the resource registry in here, because
115
+ * this function is pure and several callers plan against fixtures: reaching
116
+ * for a process-wide registry would make the plan depend on whatever some
117
+ * other module happened to import. `ensureCollectionTables` is the boundary
118
+ * that reads the world.
119
+ */
120
+ databaseExtensions?: readonly string[];
109
121
  }
110
122
  export interface EnsureAction {
111
123
  kind: "create-enum" | "create-table" | "add-column" | "add-constraint" | "rename-column" | "create-extension" | "create-function" | "create-index" | "comment-column" | "add-enum-value" | "set-not-null" | "drop-not-null";
@@ -263,4 +275,4 @@ export declare function readSchemaFactsFor(client: Queryable, collections: Colle
263
275
  * cannot be added, say) should not roll back the tables that were created fine.
264
276
  * The error is surfaced with the statement that caused it.
265
277
  */
266
- export declare function ensureCollectionTables(client: Queryable, collections: CollectionConfig[], log?: (message: string) => void): Promise<EnsureOutcome>;
278
+ export declare function ensureCollectionTables(client: Queryable, collections: CollectionConfig[], log?: (message: string) => void, options?: EnsureOptions): Promise<EnsureOutcome>;
@@ -55,12 +55,15 @@ export declare const getSqlColumnType: (propName: string, prop: Property, collec
55
55
  * appended to migrations that run against databases at any stage of their
56
56
  * life. Empty when nothing opted in — the caller writes no file then.
57
57
  *
58
- * The leading half extensions and helpers, without the table-shaped
59
- * statements is available on its own as {@link searchPrerequisiteStatements},
60
- * for the dev database Atlas analyses plans against. That database has none of
61
- * the project's tables, so it wants the functions and nothing else.
58
+ * The dev database Atlas plans against needs none of this. `searchExcludePatterns`
59
+ * removes the search column from the *inspected* state, so the state Atlas
60
+ * replays there never mentions the column and never calls a helper. Seeding the
61
+ * helpers into that database was tried and removed: measured on Atlas 1.2.3, a
62
+ * `schema apply --dry-run` produces byte-identical output seeded or not, and
63
+ * Atlas wipes the dev database before it plans anyway — so a seed could not have
64
+ * helped even where the excludes are missing, which is the case that really does
65
+ * fail with `function public.rebase_search_unaccent(text) does not exist`.
62
66
  */
63
- export declare const searchPrerequisiteStatements: (allCollections: CollectionConfig[]) => string[];
64
67
  export declare const generatePostgresSearchDdl: (allCollections: CollectionConfig[]) => string;
65
68
  /**
66
69
  * Glob patterns telling Atlas to leave the search column and its index alone.
@@ -76,9 +79,58 @@ export declare const generatePostgresSearchDdl: (allCollections: CollectionConfi
76
79
  * and reports no error for the pattern that never fired.
77
80
  */
78
81
  export declare const searchExcludePatterns: (allCollections: CollectionConfig[]) => string[];
82
+ /**
83
+ * Everything a `{ type: "vector" }` property needs, as a file Rebase applies
84
+ * itself.
85
+ *
86
+ * Vector is the second part of the schema Atlas does not own, and it is out for
87
+ * a narrower reason than search: not that Atlas refuses to parse the file, but
88
+ * that it cannot *execute* it. To compute a desired state Atlas materialises
89
+ * `schema.sql` in a dev database, that database is created empty, and Atlas
90
+ * empties it again at the start of every run — so `VECTOR(384)` resolves
91
+ * against a database that structurally cannot have pgvector, and every push
92
+ * dies with `type "vector" does not exist`. Seeding the extension beforehand
93
+ * does not survive the clean (measured: present before the run, gone after),
94
+ * putting `CREATE EXTENSION` in `schema.sql` is refused as an Atlas Pro
95
+ * feature, and `--exclude` alone cannot help because Atlas still parses and
96
+ * applies the whole file.
97
+ *
98
+ * So the column, its ANN indexes and the extension are excluded from Atlas's
99
+ * view ({@link vectorExcludePatterns}) and applied from here — the same
100
+ * arrangement search and the RLS policies already use.
101
+ *
102
+ * `CREATE EXTENSION` appears only when the project's database named `vector` in
103
+ * its `extensions` — see `DatabaseOptions.extensions`. Everything else is
104
+ * emitted either way: a database that already has pgvector installed by hand
105
+ * needs no permission from anyone, and one that does not gets the column
106
+ * refused by Postgres with a message this repo makes name the opt-in.
107
+ *
108
+ * Ordered as it must run: the extension, then the drift guard, then the column
109
+ * whose type needs the extension, then the indexes over that column. Every
110
+ * statement is `IF NOT EXISTS`, because this is replayed on every push and
111
+ * appended to migrations that run against databases at any stage of their life.
112
+ * Empty when no collection declares a vector — the caller writes no file then.
113
+ */
114
+ export declare const generatePostgresVectorDdl: (allCollections: CollectionConfig[], options?: {
115
+ extensions?: readonly string[];
116
+ }) => string;
117
+ /**
118
+ * Glob patterns telling Atlas to leave the vector column and its ANN indexes
119
+ * alone.
120
+ *
121
+ * Without these, a desired state that omits the column reads to Atlas as an
122
+ * instruction to drop it, taking the embeddings and the indexes with it on the
123
+ * next push.
124
+ *
125
+ * Fully qualified, `schema.table.object`, for the reason spelled out on
126
+ * {@link searchExcludePatterns}: the two-part form matches nothing and reports
127
+ * no error, which turns a protection into a `DROP COLUMN`.
128
+ */
129
+ export declare const vectorExcludePatterns: (allCollections: CollectionConfig[]) => string[];
79
130
  export declare const generatePostgresDdl: (allCollections: CollectionConfig[], options?: {
80
131
  includePolicies?: boolean;
81
132
  includeSearch?: boolean;
133
+ includeVector?: boolean;
82
134
  }) => Promise<string>;
83
135
  /** The RLS statements one declared collection's table needs, ready to run. */
84
136
  /**