@rebasepro/server-postgres 0.13.1-canary.gef9608c → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/dist/PostgresBackendDriver.d.ts +48 -1
  2. package/dist/PostgresBootstrapper.d.ts +26 -0
  3. package/dist/auth/services.d.ts +21 -0
  4. package/dist/{src-CU6WZGYV.js → auth-users-columns-BfQHf9JE.js} +1111 -92
  5. package/dist/auth-users-columns-BfQHf9JE.js.map +1 -0
  6. package/dist/{backup-service-CD8o_1Sl.js → backup-service-BH0Dzo_h.js} +2 -3
  7. package/dist/{backup-service-CD8o_1Sl.js.map → backup-service-BH0Dzo_h.js.map} +1 -1
  8. package/dist/cli-helpers.d.ts +56 -0
  9. package/dist/cli-output.d.ts +34 -0
  10. package/dist/data-transformer.d.ts +7 -2
  11. package/dist/data_driver-ULAyJEi9.js +193 -0
  12. package/dist/data_driver-ULAyJEi9.js.map +1 -0
  13. package/dist/ensure-collection-policies-8vuu-n4r.js +124 -0
  14. package/dist/ensure-collection-policies-8vuu-n4r.js.map +1 -0
  15. package/dist/{ensure-collection-tables-BLIIACla.js → ensure-collection-tables-CbvaGuVn.js} +162 -16
  16. package/dist/ensure-collection-tables-CbvaGuVn.js.map +1 -0
  17. package/dist/index.es.js +1720 -946
  18. package/dist/index.es.js.map +1 -1
  19. package/dist/rls-bootstrap-sql-69hYT8nr.js +244 -0
  20. package/dist/rls-bootstrap-sql-69hYT8nr.js.map +1 -0
  21. package/dist/rls-enforcement-BJ_3wxwg.js +425 -0
  22. package/dist/rls-enforcement-BJ_3wxwg.js.map +1 -0
  23. package/dist/schema/auth-schema.d.ts +102 -0
  24. package/dist/schema/auth-users-columns.d.ts +97 -0
  25. package/dist/schema/doctor-policy-checks.d.ts +28 -0
  26. package/dist/schema/doctor.d.ts +41 -25
  27. package/dist/schema/ensure-collection-policies.d.ts +33 -9
  28. package/dist/schema/ensure-collection-tables.d.ts +60 -6
  29. package/dist/schema/generate-drizzle-schema-logic.d.ts +9 -1
  30. package/dist/schema/generate-postgres-ddl-logic.d.ts +48 -0
  31. package/dist/schema/introspect-db-inference.d.ts +8 -1
  32. package/dist/schema/introspect-db-logic.d.ts +49 -0
  33. package/dist/schema/introspect-db-project.d.ts +21 -0
  34. package/dist/schema/rls-bootstrap-sql.d.ts +135 -0
  35. package/dist/schema/search-column.d.ts +248 -0
  36. package/dist/security/policy-drift.d.ts +34 -0
  37. package/dist/security/rls-enforcement.d.ts +61 -5
  38. package/dist/services/FetchService.d.ts +24 -0
  39. package/dist/services/PersistService.d.ts +21 -17
  40. package/dist/services/RelationService.d.ts +9 -57
  41. package/dist/services/RelationWriteService.d.ts +82 -0
  42. package/dist/services/collection-helpers.d.ts +42 -0
  43. package/dist/services/dataService.d.ts +3 -0
  44. package/dist/services/junction-writes.d.ts +82 -0
  45. package/dist/services/realtimeService.d.ts +139 -2
  46. package/dist/services/write-denial.d.ts +36 -0
  47. package/dist/{src-DoU9yPqq.js → src-DCdn3Val.js} +124 -3
  48. package/dist/src-DCdn3Val.js.map +1 -0
  49. package/dist/utils/drizzle-conditions.d.ts +124 -2
  50. package/dist/{websocket-B2LsrINK.js → websocket-C8ZqVBiV.js} +75 -18
  51. package/dist/websocket-C8ZqVBiV.js.map +1 -0
  52. package/package.json +8 -7
  53. package/src/PostgresBackendDriver.ts +172 -6
  54. package/src/PostgresBootstrapper.ts +136 -11
  55. package/src/auth/ensure-tables.ts +212 -91
  56. package/src/auth/services.ts +82 -5
  57. package/src/backup/backup-cli.ts +59 -57
  58. package/src/cli-errors.ts +6 -6
  59. package/src/cli-helpers.ts +124 -11
  60. package/src/cli-output.ts +43 -0
  61. package/src/cli.ts +299 -168
  62. package/src/collections/buildRegistry.ts +3 -1
  63. package/src/data-transformer.ts +129 -25
  64. package/src/history/ensure-history-table.ts +9 -2
  65. package/src/schema/auth-schema.ts +17 -1
  66. package/src/schema/auth-users-columns.ts +131 -0
  67. package/src/schema/doctor-cli.ts +14 -65
  68. package/src/schema/doctor-policy-checks.ts +105 -0
  69. package/src/schema/doctor.ts +149 -72
  70. package/src/schema/ensure-collection-policies.ts +99 -6
  71. package/src/schema/ensure-collection-tables.ts +366 -30
  72. package/src/schema/generate-drizzle-schema-logic.ts +146 -66
  73. package/src/schema/generate-drizzle-schema.ts +11 -10
  74. package/src/schema/generate-postgres-ddl-logic.ts +277 -10
  75. package/src/schema/generate-postgres-ddl.ts +38 -14
  76. package/src/schema/generated-schema-staleness.ts +14 -7
  77. package/src/schema/introspect-db-inference.ts +9 -2
  78. package/src/schema/introspect-db-logic.ts +251 -75
  79. package/src/schema/introspect-db-project.ts +78 -0
  80. package/src/schema/introspect-db.ts +42 -25
  81. package/src/schema/introspect-runtime.ts +14 -2
  82. package/src/schema/rls-bootstrap-sql.ts +288 -0
  83. package/src/schema/search-column.ts +643 -0
  84. package/src/security/anonymous-grants.test.ts +4 -2
  85. package/src/security/policy-drift.test.ts +104 -3
  86. package/src/security/policy-drift.ts +129 -7
  87. package/src/security/rls-enforcement.ts +150 -7
  88. package/src/services/BranchService.ts +5 -0
  89. package/src/services/FetchService.ts +243 -22
  90. package/src/services/PersistService.ts +68 -42
  91. package/src/services/RelationService.ts +37 -696
  92. package/src/services/RelationWriteService.ts +653 -0
  93. package/src/services/cdc/trigger-cdc.ts +5 -1
  94. package/src/services/channel-history.ts +14 -0
  95. package/src/services/channel-presence.ts +13 -0
  96. package/src/services/collection-helpers.ts +89 -4
  97. package/src/services/dataService.ts +3 -0
  98. package/src/services/junction-writes.ts +295 -0
  99. package/src/services/pg-notify-listener.ts +1 -1
  100. package/src/services/realtimeService.ts +347 -86
  101. package/src/services/write-denial.ts +55 -0
  102. package/src/utils/drizzle-conditions.ts +433 -35
  103. package/src/utils/pg-error-utils.ts +8 -3
  104. package/src/websocket.ts +113 -16
  105. package/dist/ensure-collection-policies-Bck0ky4u.js +0 -57
  106. package/dist/ensure-collection-policies-Bck0ky4u.js.map +0 -1
  107. package/dist/ensure-collection-tables-BLIIACla.js.map +0 -1
  108. package/dist/policy-CeA1JcxP.js +0 -105
  109. package/dist/policy-CeA1JcxP.js.map +0 -1
  110. package/dist/schema/auth-bootstrap-sql.d.ts +0 -24
  111. package/dist/src-CU6WZGYV.js.map +0 -1
  112. package/dist/src-DoU9yPqq.js.map +0 -1
  113. package/dist/websocket-B2LsrINK.js.map +0 -1
  114. package/src/schema/auth-bootstrap-sql.ts +0 -47
@@ -0,0 +1,425 @@
1
+ import { createRequire as __createRequire } from "module";
2
+ import "process";
3
+ __createRequire(import.meta.url);
4
+ import { r as ANONYMOUS_USER_ID } from "./data_driver-ULAyJEi9.js";
5
+ import { E as findAnonymousGrants, T as securityRuleToConditions } from "./auth-users-columns-BfQHf9JE.js";
6
+ import { n as REBASE_SCHEMA, o as usesLegacyRlsFunctions } from "./src-DCdn3Val.js";
7
+ import { logger } from "@rebasepro/server";
8
+ import { sql } from "drizzle-orm";
9
+ //#region ../common/src/util/internal-tables.ts
10
+ /**
11
+ * The tables Rebase creates for its own bookkeeping, and the SQL that keeps the
12
+ * end-user role away from them.
13
+ *
14
+ * ## Why this exists
15
+ *
16
+ * Authenticated requests run as {@link REBASE_USER_ROLE}, and the boot-time role
17
+ * provisioning grants that role `SELECT, INSERT, UPDATE, DELETE` on every table
18
+ * in the schemas a project uses — including `rebase`, because a project's own
19
+ * collections are allowed to live there (the scaffold puts `users` there). It
20
+ * also sets `ALTER DEFAULT PRIVILEGES`, so a table created *later* by the
21
+ * migrating role inherits the same grant.
22
+ *
23
+ * Every framework-internal table is created later: auth's tables come up during
24
+ * `initializeAuth`, `api_keys` during route mounting, `cron_logs` when the first
25
+ * job registers, `idempotency_keys` on the first request that carries a key. So
26
+ * they all inherited full DML for the end-user role — and none of them enables
27
+ * row-level security, because none of them is a collection with
28
+ * `securityRules`. Measured on a freshly provisioned database, `SET ROLE
29
+ * rebase_user` could read `rebase.refresh_tokens` (session token hashes),
30
+ * `rebase.mfa_factors` (`secret_encrypted`), `rebase.recovery_codes`, and
31
+ * `rebase.api_keys` (including its `admin` flag), and insert into
32
+ * `rebase.app_config`.
33
+ *
34
+ * Nothing routes a user-context query at those tables today, so this was not
35
+ * reachable over the API. That is the wrong thing to depend on: the documented
36
+ * model is that RLS is the authorization boundary, and these tables sat outside
37
+ * it. The boundary is now a privilege boundary instead — the role simply cannot
38
+ * address them.
39
+ *
40
+ * ## Why REVOKE rather than ENABLE ROW LEVEL SECURITY
41
+ *
42
+ * RLS with no policy denies every row, which is the same outcome, but it is the
43
+ * *weaker* statement: it leaves the grant in place, so a later policy — or a
44
+ * `FORCE` flag cleared by some future migration — reopens the table. There is no
45
+ * row of `refresh_tokens` any end user should ever reach, so the honest encoding
46
+ * is "this role has no privilege here at all". It also keeps the owner
47
+ * connection (which auth actually runs on) completely unaffected.
48
+ *
49
+ * ## Keeping it true
50
+ *
51
+ * `packages/rls-check` scans the `rebase` schema — it used to skip it as a
52
+ * "platform" schema — and its `rls-disabled` check fires on exactly the
53
+ * condition this module removes: RLS off *and* a DML grant to a reachable role.
54
+ * So a table added here without a revoke is caught by `pnpm rls:check`, not by
55
+ * someone re-reading this file.
56
+ */
57
+ /**
58
+ * The Postgres role authenticated requests run as.
59
+ *
60
+ * Defined here rather than in the Postgres driver because both the driver (which
61
+ * provisions the role) and this module (which revokes on its behalf) need it,
62
+ * and a second spelling of a role name is a silent no-op waiting to happen.
63
+ */
64
+ var REBASE_USER_ROLE = "rebase_user";
65
+ /**
66
+ * Framework-internal table names, unqualified.
67
+ *
68
+ * Deliberately NOT including `users`: the auth user table is also a collection,
69
+ * with `securityRules`, RLS enabled and policies applied. Users read their own
70
+ * row through it — revoking there would break sign-in.
71
+ *
72
+ * `atlas_schema_revisions` is Atlas's migration ledger, which lands in `rebase`
73
+ * because `db migrate apply` passes `--revisions-schema rebase`.
74
+ */
75
+ var REBASE_INTERNAL_TABLES = [
76
+ "user_identities",
77
+ "refresh_tokens",
78
+ "password_reset_tokens",
79
+ "magic_link_tokens",
80
+ "mfa_factors",
81
+ "mfa_challenges",
82
+ "recovery_codes",
83
+ "app_config",
84
+ "schema_meta",
85
+ "api_keys",
86
+ "cron_logs",
87
+ "cron_claims",
88
+ "idempotency_keys",
89
+ "entity_history",
90
+ "branches",
91
+ "channel_messages",
92
+ "channel_cursors",
93
+ "channel_presence",
94
+ "atlas_schema_revisions"
95
+ ];
96
+ /** Postgres identifiers this module is willing to interpolate. */
97
+ var SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*$/;
98
+ /**
99
+ * A single statement that takes every privilege on `schema.table` away from the
100
+ * end-user role.
101
+ *
102
+ * Wrapped in a `DO` block guarded on `pg_roles` for two reasons, both of which
103
+ * happen in practice:
104
+ *
105
+ * - the role does not exist when the connection is unprivileged (Rebase then
106
+ * relies on native RLS rather than a role switch), and a bare `REVOKE` on a
107
+ * missing role is an error, not a no-op;
108
+ * - the table may not exist yet — `cron_logs` never appears in a project with
109
+ * no cron jobs — and `to_regclass` returning NULL has to be tolerated too.
110
+ *
111
+ * One command, so it is safe on handles that speak the extended query protocol
112
+ * and reject multi-statement strings.
113
+ */
114
+ function revokeInternalTableSql(schema, table) {
115
+ if (!SAFE_IDENTIFIER.test(schema)) throw new Error(`Refusing to build SQL with an unsafe schema name: ${JSON.stringify(schema)}`);
116
+ if (!SAFE_IDENTIFIER.test(table)) throw new Error(`Refusing to build SQL with an unsafe table name: ${JSON.stringify(table)}`);
117
+ const qualified = `"${schema}"."${table}"`;
118
+ return `
119
+ DO $rebase_revoke$
120
+ BEGIN
121
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${REBASE_USER_ROLE}')
122
+ AND to_regclass('${qualified}') IS NOT NULL THEN
123
+ EXECUTE 'REVOKE ALL ON ${qualified} FROM ${REBASE_USER_ROLE}';
124
+ END IF;
125
+ END
126
+ $rebase_revoke$;
127
+ `.trim();
128
+ }
129
+ /**
130
+ * Revoke on every internal table in `schema`, one statement at a time.
131
+ *
132
+ * Best-effort per table: a connection that does not own one of them (a
133
+ * pre-provisioned database, a platform-managed ledger) cannot revoke on it, and
134
+ * that must not take down a boot. The caller decides how loud to be — `onError`
135
+ * exists so the driver can warn without this module importing a logger.
136
+ */
137
+ async function revokeInternalTableAccess(execute, schema, options) {
138
+ for (const table of options?.tables ?? REBASE_INTERNAL_TABLES) try {
139
+ await execute(revokeInternalTableSql(schema, table));
140
+ } catch (error) {
141
+ options?.onError?.(table, error);
142
+ }
143
+ }
144
+ //#endregion
145
+ //#region src/security/rls-enforcement.ts
146
+ var quoteIdent = (name) => `"${name.replace(/"/g, "\"\"")}"`;
147
+ /** DML the user role holds on managed tables (RLS still filters per row). */
148
+ var USER_TABLE_PRIVILEGES = "SELECT, INSERT, UPDATE, DELETE";
149
+ /**
150
+ * Warn when the connection role shares its name with an existing schema.
151
+ *
152
+ * Postgres resolves unqualified names through `search_path`, which defaults to
153
+ * `"$user", public` — and `$user` is the connection ROLE. When a schema of that
154
+ * name exists it sits ahead of `public`, so every unqualified statement
155
+ * silently operates on it instead:
156
+ *
157
+ * CREATE TABLE posts (...); -- you meant public.posts; you got <role>.posts
158
+ *
159
+ * Nothing errors. You get a second table of the same name in the wrong schema,
160
+ * and reads that pin `public` cannot see it — which reads as "missing table" and
161
+ * sends people to re-run a push that creates a *third* copy. The bootstrapper
162
+ * has a whole branch dedicated to recognising the symptom after the fact.
163
+ *
164
+ * Rebase shipped straight into this: it creates a schema named `rebase` while
165
+ * every template named the database role `rebase` too. The scaffold uses
166
+ * `rebase_app` now, and every pool Rebase opens pins `search_path=public`
167
+ * (`pinSearchPath`), which covers the paths the framework controls. This covers
168
+ * the ones it does not — `psql`, `pg_dump`, drizzle-kit, a colleague's script,
169
+ * a hand-written migration — because the hazard is a property of the two NAMES,
170
+ * not of any one connection.
171
+ *
172
+ * A warning rather than a boot failure: the database works, the framework's own
173
+ * traffic is pinned, and refusing to start over a naming choice a user may have
174
+ * inherited would be worse than the risk.
175
+ */
176
+ async function warnOnRoleSchemaCollision(run) {
177
+ try {
178
+ const rows = await run(`
179
+ SELECT current_user AS role,
180
+ EXISTS (
181
+ SELECT 1 FROM pg_namespace n WHERE n.nspname = current_user
182
+ ) AS collides
183
+ `);
184
+ if (rows[0]?.collides !== true) return;
185
+ const role = String(rows[0]?.role ?? "the connection role");
186
+ logger.warn(`⚠️ The database role "${role}" has the same name as a schema. Postgres resolves unqualified names through \`search_path\`, which defaults to \`"$user", public\` — so "${role}" is searched BEFORE public, and any unqualified \`CREATE TABLE\`/\`SELECT\` from a tool that does not pin the path (psql, pg_dump, drizzle-kit, a hand-written migration) silently lands in "${role}" instead. Rebase's own connections pin \`search_path=public\`, so the server is unaffected. To remove the hazard entirely, connect as a role whose name is not also a schema — the scaffold uses "rebase_app".`);
187
+ } catch {}
188
+ }
189
+ async function detectConnectionPosture(run) {
190
+ const row = (await run(`
191
+ SELECT current_user AS role,
192
+ r.rolsuper AS superuser,
193
+ r.rolbypassrls AS bypassrls,
194
+ EXISTS (
195
+ SELECT 1 FROM pg_tables t
196
+ WHERE t.tableowner = current_user
197
+ AND t.schemaname NOT IN ('pg_catalog', 'information_schema')
198
+ ) AS owns_tables
199
+ FROM pg_roles r
200
+ WHERE r.rolname = current_user
201
+ `))[0] ?? {};
202
+ const superuser = row.superuser === true;
203
+ const bypassRLS = row.bypassrls === true;
204
+ const ownsTables = row.owns_tables === true;
205
+ return {
206
+ role: String(row.role ?? "unknown"),
207
+ superuser,
208
+ bypassRLS,
209
+ ownsTables,
210
+ privileged: superuser || bypassRLS || ownsTables
211
+ };
212
+ }
213
+ /**
214
+ * Human-actionable instructions for when the connection cannot provision the
215
+ * user role itself (no CREATEROLE and role not pre-created by the platform).
216
+ */
217
+ function appRoleSetupInstructions(connectionRole, schemas) {
218
+ const grants = schemas.map((s) => `GRANT USAGE ON SCHEMA ${quoteIdent(s)} TO ${REBASE_USER_ROLE};\nGRANT ${USER_TABLE_PRIVILEGES} ON ALL TABLES IN SCHEMA ${quoteIdent(s)} TO ${REBASE_USER_ROLE};\nGRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA ${quoteIdent(s)} TO ${REBASE_USER_ROLE};`).join("\n");
219
+ return `Rebase enforces row-level security by running authenticated requests as the restricted role "${REBASE_USER_ROLE}", but the connection role "${connectionRole}" bypasses RLS and cannot create that role itself.\nRun the following as a database administrator, then restart:\n\nCREATE ROLE ${REBASE_USER_ROLE} NOLOGIN NOSUPERUSER NOBYPASSRLS NOINHERIT;\nGRANT ${REBASE_USER_ROLE} TO ${quoteIdent(connectionRole)};\n` + grants;
220
+ }
221
+ /**
222
+ * Idempotently provision the `rebase_user` role, membership for the current
223
+ * connection role, and DML grants (+ default privileges for future tables)
224
+ * on every existing schema in `schemas`.
225
+ *
226
+ * Split into privilege tiers so it works both when the connection is a
227
+ * superuser (creates everything) and when the platform pre-created the role
228
+ * and membership (e.g. CNPG `postInitApplicationSQL`) and the connection is
229
+ * merely the table owner — owners can always run the grant tier themselves.
230
+ *
231
+ * RLS still filters every row: these grants only make the tables *reachable*
232
+ * by the role; the policies decide which rows/commands actually pass.
233
+ *
234
+ * Throws with precise setup instructions when the role is missing and the
235
+ * connection cannot create it.
236
+ */
237
+ async function ensureAppRole(run, schemas) {
238
+ const uniqueSchemas = Array.from(new Set(schemas.filter(Boolean)));
239
+ if ((await run(`SELECT 1 FROM pg_roles WHERE rolname = 'rebase_user'`)).length === 0) try {
240
+ await run(`CREATE ROLE ${REBASE_USER_ROLE} NOLOGIN NOSUPERUSER NOBYPASSRLS NOINHERIT`);
241
+ } catch (err) {
242
+ throw new Error(`Failed to create the "${REBASE_USER_ROLE}" role: ${err instanceof Error ? err.message : String(err)}\n\n` + appRoleSetupInstructions("current connection role", uniqueSchemas));
243
+ }
244
+ const memberRows = await run(`
245
+ SELECT (pg_has_role(current_user, '${REBASE_USER_ROLE}', 'MEMBER')
246
+ OR (SELECT rolsuper FROM pg_roles WHERE rolname = current_user)) AS can_set,
247
+ current_user AS role
248
+ `);
249
+ if (memberRows[0]?.can_set !== true) try {
250
+ await run(`GRANT ${REBASE_USER_ROLE} TO CURRENT_USER`);
251
+ } catch (err) {
252
+ throw new Error(`The connection role is not a member of "${REBASE_USER_ROLE}" and cannot grant itself membership: ${err instanceof Error ? err.message : String(err)}\n\n` + appRoleSetupInstructions(String(memberRows[0]?.role ?? "current connection role"), uniqueSchemas));
253
+ }
254
+ const nspRows = await run("SELECT nspname FROM pg_namespace");
255
+ const existing = new Set(nspRows.map((r) => String(r.nspname)));
256
+ for (const schema of uniqueSchemas) {
257
+ if (!existing.has(schema)) continue;
258
+ const s = quoteIdent(schema);
259
+ await run(`GRANT USAGE ON SCHEMA ${s} TO ${REBASE_USER_ROLE}`);
260
+ await run(`GRANT ${USER_TABLE_PRIVILEGES} ON ALL TABLES IN SCHEMA ${s} TO ${REBASE_USER_ROLE}`);
261
+ await run(`GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA ${s} TO ${REBASE_USER_ROLE}`);
262
+ await run(`ALTER DEFAULT PRIVILEGES IN SCHEMA ${s} GRANT ${USER_TABLE_PRIVILEGES} ON TABLES TO ${REBASE_USER_ROLE}`);
263
+ await run(`ALTER DEFAULT PRIVILEGES IN SCHEMA ${s} GRANT USAGE, SELECT ON SEQUENCES TO ${REBASE_USER_ROLE}`);
264
+ await revokeInternalTableAccess(async (text) => {
265
+ await run(text);
266
+ }, schema, { onError: (table, error) => logger.warn(`🔐 [rls] Could not revoke "${REBASE_USER_ROLE}" access to "${schema}"."${table}" — it stays reachable by authenticated requests: ` + (error instanceof Error ? error.message : String(error))) });
267
+ }
268
+ logger.debug(`🔐 [rls] User role "${REBASE_USER_ROLE}" provisioned (schemas: ${uniqueSchemas.join(", ")})`);
269
+ }
270
+ /**
271
+ * Apply the authenticated context to a transaction: the `app.*` GUCs that RLS
272
+ * policies read via `auth.uid()` / `auth.roles()` / `auth.jwt()`, and — when
273
+ * `userRole` is set — `SET LOCAL ROLE` so RLS binds every statement in this
274
+ * transaction (reads *and* writes).
275
+ *
276
+ * GUCs are set with `is_local = true` and the role switch is `LOCAL`: both
277
+ * reset at commit/rollback, so pooled connections are never polluted.
278
+ *
279
+ * Fails closed by construction: if the role switch errors, the transaction
280
+ * aborts instead of proceeding privileged.
281
+ *
282
+ * SECURITY: this function is only ever called on the **user** path (the server
283
+ * context uses the base/owner driver and never calls it). The default policies
284
+ * treat `auth.uid() IS NULL` as the trusted server context, and `auth.uid()`
285
+ * is `NULLIF(current_setting('app.uid'), '')` — so an EMPTY user id would
286
+ * be read as NULL and silently escalate a user request to server privileges.
287
+ * Coerce empty/blank ids to `ANONYMOUS_USER_ID` here, at the single chokepoint,
288
+ * rather than trusting every caller (e.g. realtime subscription auth) to do it.
289
+ * That sentinel is exported from `@rebasepro/types` because it leaks into rule
290
+ * semantics: it is why `auth.uid() IS NOT NULL` is true for anonymous requests.
291
+ */
292
+ async function applyAuthContext(tx, auth, userRole) {
293
+ const uid = typeof auth.uid === "string" && auth.uid.trim() !== "" ? auth.uid : ANONYMOUS_USER_ID;
294
+ const normalizedRoles = auth.roles.map((r) => typeof r === "string" ? r : r?.id ?? String(r));
295
+ await tx.execute(sql`
296
+ SELECT
297
+ set_config('app.uid', ${uid}, true),
298
+ set_config('app.user_id', ${uid}, true),
299
+ set_config('app.user_roles', ${normalizedRoles.join(",")}, true),
300
+ set_config('app.jwt', ${JSON.stringify({
301
+ sub: uid,
302
+ roles: auth.roles
303
+ })}, true)
304
+ `);
305
+ if (userRole) await tx.execute(sql.raw(`SET LOCAL ROLE ${quoteIdent(userRole)}`));
306
+ }
307
+ /** Role names from other BaaS platforms that people reach for out of habit. */
308
+ var FOREIGN_CONVENTION_ROLES = {
309
+ authenticated: "Supabase",
310
+ anon: "Supabase",
311
+ service_role: "Supabase"
312
+ };
313
+ /**
314
+ * Warn about rules that read as "signed-in users only" but admit anonymous
315
+ * callers — `auth.uid() IS NOT NULL`, or a comparison against another
316
+ * platform's magic user id such as `'anon'`.
317
+ *
318
+ * The sibling of {@link validatePolicyPgRoles}, for the more dangerous spelling
319
+ * of the same habit. A foreign `pgRoles` value makes a policy unreachable and
320
+ * the table reads empty — loud, and that guard throws. These do the opposite:
321
+ * the rule compiles to a grant, and nothing looks wrong until the data is
322
+ * already public.
323
+ *
324
+ * Warns rather than throws. Unlike an unreachable `pgRoles`, these rules are
325
+ * serving traffic today: refusing to boot would take an app offline to report a
326
+ * problem it already has, and on the read path it would take it offline
327
+ * *because* its data was exposed. Rewriting the author's SQL is not an option
328
+ * either — this is the escape hatch whose whole promise is that it means what it
329
+ * says. So: say so, loudly, and leave the rule alone.
330
+ */
331
+ function warnOnAnonymousGrants(collections) {
332
+ const byRisk = /* @__PURE__ */ new Map();
333
+ for (const collection of collections) for (const rule of collection.securityRules ?? []) {
334
+ const { usingExpr, withCheckExpr } = securityRuleToConditions(rule);
335
+ const risks = [usingExpr, withCheckExpr].filter((e) => e !== null).flatMap(findAnonymousGrants);
336
+ for (const risk of risks) {
337
+ const key = `${risk.pattern}:${risk.detail}`;
338
+ const site = `${collection.slug ?? "(unnamed)"} → "${rule.name ?? "(unnamed rule)"}"`;
339
+ const entry = byRisk.get(key) ?? {
340
+ risk,
341
+ sites: []
342
+ };
343
+ if (!entry.sites.includes(site)) entry.sites.push(site);
344
+ byRisk.set(key, entry);
345
+ }
346
+ }
347
+ if (byRisk.size === 0) return;
348
+ const problems = [...byRisk.values()].map(({ risk, sites }) => ` • ${risk.explanation}\n ${sites.length} rule(s): ${sites.join(", ")}`);
349
+ logger.warn(`Security rules that read as a lockdown but grant access to anonymous requests. Every caller from a client carries a user id ('${ANONYMOUS_USER_ID}' when nobody is signed in), so these clauses are true for everyone:\n\n` + problems.join("\n\n") + "\n");
350
+ }
351
+ /**
352
+ * Name the collections whose raw policy SQL still calls the pre-1.0 helpers.
353
+ *
354
+ * The compiler rewrites `auth.uid()` to `rebase.uid()` on the way into the
355
+ * database, so nothing is broken and no policy is wrong — which is exactly why
356
+ * this has to be said out loud. A silent rewrite that works forever is not a
357
+ * migration, it is a second supported spelling nobody wrote down, and the next
358
+ * person to read those rules will copy the old one.
359
+ *
360
+ * Only `raw` expressions can carry it. Structured rules (`policy.authUid()`,
361
+ * `policy.rolesOverlap(...)`) compile from the model and were never affected.
362
+ */
363
+ function warnOnLegacyRlsFunctions(collections) {
364
+ const sites = [];
365
+ for (const collection of collections) for (const rule of collection.securityRules ?? []) {
366
+ const { usingExpr, withCheckExpr } = securityRuleToConditions(rule);
367
+ if (![usingExpr, withCheckExpr].filter((e) => e !== null).some(containsLegacyRlsCall)) continue;
368
+ const site = `${collection.slug ?? "(unnamed)"} → "${rule.name ?? "(unnamed rule)"}"`;
369
+ if (!sites.includes(site)) sites.push(site);
370
+ }
371
+ if (sites.length === 0) return;
372
+ logger.warn(`These security rules call the pre-1.0 RLS helpers (\`auth.uid()\`, \`auth.roles()\`, \`auth.jwt()\`). They still work — the compiler rewrites them — but the functions now live in the \`rebase\` schema, and the \`auth\` one is Supabase's. Update the raw SQL in these rules to \`${REBASE_SCHEMA}.uid()\` and friends, or switch them to the structured helpers (\`policy.authUid()\`, \`policy.rolesOverlap()\`), which never had to be spelled by hand:\n\n` + sites.map((s) => ` • ${s}`).join("\n") + "\n");
373
+ }
374
+ /** Whether any `raw` expression in the tree calls a pre-1.0 helper. */
375
+ function containsLegacyRlsCall(expr) {
376
+ switch (expr.kind) {
377
+ case "raw": return usesLegacyRlsFunctions(expr.sql);
378
+ case "and":
379
+ case "or": return expr.operands.some(containsLegacyRlsCall);
380
+ case "not": return containsLegacyRlsCall(expr.operand);
381
+ case "existsIn": return containsLegacyRlsCall(expr.where);
382
+ default: return false;
383
+ }
384
+ }
385
+ /**
386
+ * Reject `pgRoles` that this server can never satisfy.
387
+ *
388
+ * `pgRoles` sets the `TO` clause of a generated policy, so a policy naming a
389
+ * role the request never runs as simply never applies — and RLS then filters
390
+ * every row. The table reads as empty, which is indistinguishable from having
391
+ * no data, so the mistake survives review and ships.
392
+ *
393
+ * Requests run as `rebase_user`, so a policy is only reachable if it targets
394
+ * `public` or a role `rebase_user` holds. Anything else is a configuration
395
+ * error worth failing the boot for.
396
+ */
397
+ async function validatePolicyPgRoles(run, collections, requestRole = REBASE_USER_ROLE) {
398
+ const wanted = /* @__PURE__ */ new Map();
399
+ for (const collection of collections) for (const rule of collection.securityRules ?? []) for (const role of rule.pgRoles ?? []) {
400
+ if (role === "public") continue;
401
+ wanted.set(role, [...wanted.get(role) ?? [], collection.slug ?? "(unnamed)"]);
402
+ }
403
+ if (wanted.size === 0) return;
404
+ const names = [...wanted.keys()].map((r) => `'${r.replace(/'/g, "''")}'`).join(",");
405
+ const rows = await run(`
406
+ SELECT r.rolname AS role,
407
+ COALESCE(pg_has_role(to_regrole('${requestRole.replace(/'/g, "''")}'), r.oid, 'MEMBER'), false) AS reachable
408
+ FROM pg_roles r
409
+ WHERE r.rolname IN (${names})
410
+ `);
411
+ const reachable = new Map(rows.map((row) => [String(row.role), row.reachable === true]));
412
+ const problems = [];
413
+ for (const [role, slugs] of wanted) {
414
+ if (reachable.get(role) === true) continue;
415
+ const why = reachable.has(role) ? `"${requestRole}" is not a member of it` : "no such role exists in this database";
416
+ const platform = FOREIGN_CONVENTION_ROLES[role];
417
+ const hint = platform ? `"${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 auth.roles().` : `Either grant it (GRANT ${role} TO ${requestRole}) or drop \`pgRoles\` so the policy targets \`public\`.`;
418
+ problems.push(` • pgRoles: ["${role}"] on ${slugs.join(", ")} — ${why}.\n ${hint}`);
419
+ }
420
+ if (problems.length > 0) throw new Error(`Security rules target PostgreSQL roles this server cannot use. Requests run as "${requestRole}", so these policies would never apply and every row would be filtered out — the collections would look empty rather than error.\n\n` + problems.join("\n\n") + "\n");
421
+ }
422
+ //#endregion
423
+ 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 };
424
+
425
+ //# sourceMappingURL=rls-enforcement-BJ_3wxwg.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rls-enforcement-BJ_3wxwg.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.\n *\n * `atlas_schema_revisions` is Atlas's migration ledger, which lands in `rebase`\n * because `db migrate apply` passes `--revisions-schema rebase`.\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 \"idempotency_keys\",\n \"entity_history\",\n \"branches\",\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 * 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 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 * `auth.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 (`auth.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 `auth.uid()` / `auth.roles()` / `auth.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 `auth.uid() IS NULL` as the trusted server context, and `auth.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 `auth.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 — `auth.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 auth.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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"}
@@ -427,6 +427,23 @@ export declare function createAuthSchema(usersSchemaName?: string): {
427
427
  identity: undefined;
428
428
  generated: undefined;
429
429
  }, {}, {}>;
430
+ aal: import("drizzle-orm/pg-core").PgColumn<{
431
+ name: "aal";
432
+ tableName: "refresh_tokens";
433
+ dataType: "string";
434
+ columnType: "PgText";
435
+ data: string;
436
+ driverParam: string;
437
+ notNull: false;
438
+ hasDefault: false;
439
+ isPrimaryKey: false;
440
+ isAutoincrement: false;
441
+ hasRuntimeDefault: false;
442
+ enumValues: [string, ...string[]];
443
+ baseColumn: never;
444
+ identity: undefined;
445
+ generated: undefined;
446
+ }, {}, {}>;
430
447
  userAgent: import("drizzle-orm/pg-core").PgColumn<{
431
448
  name: "user_agent";
432
449
  tableName: "refresh_tokens";
@@ -880,6 +897,23 @@ export declare function createAuthSchema(usersSchemaName?: string): {
880
897
  identity: undefined;
881
898
  generated: undefined;
882
899
  }, {}, {}>;
900
+ lastUsedCounter: import("drizzle-orm/pg-core").PgColumn<{
901
+ name: "last_used_counter";
902
+ tableName: "mfa_factors";
903
+ dataType: "number";
904
+ columnType: "PgBigInt53";
905
+ data: number;
906
+ driverParam: string | number;
907
+ notNull: false;
908
+ hasDefault: false;
909
+ isPrimaryKey: false;
910
+ isAutoincrement: false;
911
+ hasRuntimeDefault: false;
912
+ enumValues: undefined;
913
+ baseColumn: never;
914
+ identity: undefined;
915
+ generated: undefined;
916
+ }, {}, {}>;
883
917
  createdAt: import("drizzle-orm/pg-core").PgColumn<{
884
918
  name: "created_at";
885
919
  tableName: "mfa_factors";
@@ -1006,6 +1040,23 @@ export declare function createAuthSchema(usersSchemaName?: string): {
1006
1040
  identity: undefined;
1007
1041
  generated: undefined;
1008
1042
  }, {}, {}>;
1043
+ attempts: import("drizzle-orm/pg-core").PgColumn<{
1044
+ name: "attempts";
1045
+ tableName: "mfa_challenges";
1046
+ dataType: "number";
1047
+ columnType: "PgInteger";
1048
+ data: number;
1049
+ driverParam: string | number;
1050
+ notNull: true;
1051
+ hasDefault: true;
1052
+ isPrimaryKey: false;
1053
+ isAutoincrement: false;
1054
+ hasRuntimeDefault: false;
1055
+ enumValues: undefined;
1056
+ baseColumn: never;
1057
+ identity: undefined;
1058
+ generated: undefined;
1059
+ }, {}, {}>;
1009
1060
  expiresAt: import("drizzle-orm/pg-core").PgColumn<{
1010
1061
  name: "expires_at";
1011
1062
  tableName: "mfa_challenges";
@@ -1642,6 +1693,23 @@ export declare const refreshTokens: import("drizzle-orm/pg-core").PgTableWithCol
1642
1693
  identity: undefined;
1643
1694
  generated: undefined;
1644
1695
  }, {}, {}>;
1696
+ aal: import("drizzle-orm/pg-core").PgColumn<{
1697
+ name: "aal";
1698
+ tableName: "refresh_tokens";
1699
+ dataType: "string";
1700
+ columnType: "PgText";
1701
+ data: string;
1702
+ driverParam: string;
1703
+ notNull: false;
1704
+ hasDefault: false;
1705
+ isPrimaryKey: false;
1706
+ isAutoincrement: false;
1707
+ hasRuntimeDefault: false;
1708
+ enumValues: [string, ...string[]];
1709
+ baseColumn: never;
1710
+ identity: undefined;
1711
+ generated: undefined;
1712
+ }, {}, {}>;
1645
1713
  userAgent: import("drizzle-orm/pg-core").PgColumn<{
1646
1714
  name: "user_agent";
1647
1715
  tableName: "refresh_tokens";
@@ -2095,6 +2163,23 @@ export declare const mfaFactors: import("drizzle-orm/pg-core").PgTableWithColumn
2095
2163
  identity: undefined;
2096
2164
  generated: undefined;
2097
2165
  }, {}, {}>;
2166
+ lastUsedCounter: import("drizzle-orm/pg-core").PgColumn<{
2167
+ name: "last_used_counter";
2168
+ tableName: "mfa_factors";
2169
+ dataType: "number";
2170
+ columnType: "PgBigInt53";
2171
+ data: number;
2172
+ driverParam: string | number;
2173
+ notNull: false;
2174
+ hasDefault: false;
2175
+ isPrimaryKey: false;
2176
+ isAutoincrement: false;
2177
+ hasRuntimeDefault: false;
2178
+ enumValues: undefined;
2179
+ baseColumn: never;
2180
+ identity: undefined;
2181
+ generated: undefined;
2182
+ }, {}, {}>;
2098
2183
  createdAt: import("drizzle-orm/pg-core").PgColumn<{
2099
2184
  name: "created_at";
2100
2185
  tableName: "mfa_factors";
@@ -2221,6 +2306,23 @@ export declare const mfaChallenges: import("drizzle-orm/pg-core").PgTableWithCol
2221
2306
  identity: undefined;
2222
2307
  generated: undefined;
2223
2308
  }, {}, {}>;
2309
+ attempts: import("drizzle-orm/pg-core").PgColumn<{
2310
+ name: "attempts";
2311
+ tableName: "mfa_challenges";
2312
+ dataType: "number";
2313
+ columnType: "PgInteger";
2314
+ data: number;
2315
+ driverParam: string | number;
2316
+ notNull: true;
2317
+ hasDefault: true;
2318
+ isPrimaryKey: false;
2319
+ isAutoincrement: false;
2320
+ hasRuntimeDefault: false;
2321
+ enumValues: undefined;
2322
+ baseColumn: never;
2323
+ identity: undefined;
2324
+ generated: undefined;
2325
+ }, {}, {}>;
2224
2326
  expiresAt: import("drizzle-orm/pg-core").PgColumn<{
2225
2327
  name: "expires_at";
2226
2328
  tableName: "mfa_challenges";