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

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 (85) hide show
  1. package/dist/PostgresBackendDriver.d.ts +48 -1
  2. package/dist/auth/services.d.ts +19 -0
  3. package/dist/{src-DlPBctw_.js → auth-users-columns-CBEOeYqa.js} +835 -63
  4. package/dist/auth-users-columns-CBEOeYqa.js.map +1 -0
  5. package/dist/{backup-service-CD8o_1Sl.js → backup-service-Bww-Lg0s.js} +2 -2
  6. package/dist/{backup-service-CD8o_1Sl.js.map → backup-service-Bww-Lg0s.js.map} +1 -1
  7. package/dist/cli-helpers.d.ts +57 -1
  8. package/dist/data-transformer.d.ts +7 -2
  9. package/dist/data_driver-ULAyJEi9.js +193 -0
  10. package/dist/data_driver-ULAyJEi9.js.map +1 -0
  11. package/dist/{ensure-collection-policies-ViG8XiPn.js → ensure-collection-policies-B_JMGa5K.js} +2 -2
  12. package/dist/{ensure-collection-policies-ViG8XiPn.js.map → ensure-collection-policies-B_JMGa5K.js.map} +1 -1
  13. package/dist/{ensure-collection-tables-CBQdOETu.js → ensure-collection-tables-DzeTEvMv.js} +170 -20
  14. package/dist/ensure-collection-tables-DzeTEvMv.js.map +1 -0
  15. package/dist/index.es.js +1365 -338
  16. package/dist/index.es.js.map +1 -1
  17. package/dist/rls-bootstrap-sql-Bpv3nUZo.js +244 -0
  18. package/dist/rls-bootstrap-sql-Bpv3nUZo.js.map +1 -0
  19. package/dist/schema/auth-schema.d.ts +102 -0
  20. package/dist/schema/auth-users-columns.d.ts +97 -0
  21. package/dist/schema/doctor-policy-checks.d.ts +28 -0
  22. package/dist/schema/doctor.d.ts +23 -25
  23. package/dist/schema/ensure-collection-tables.d.ts +61 -7
  24. package/dist/schema/generate-drizzle-schema-logic.d.ts +10 -2
  25. package/dist/schema/generate-postgres-ddl-logic.d.ts +53 -5
  26. package/dist/schema/generated-schema-staleness.d.ts +39 -0
  27. package/dist/schema/rls-bootstrap-sql.d.ts +135 -0
  28. package/dist/schema/search-column.d.ts +248 -0
  29. package/dist/security/rls-enforcement.d.ts +61 -5
  30. package/dist/services/FetchService.d.ts +34 -7
  31. package/dist/services/RelationService.d.ts +30 -0
  32. package/dist/services/collection-helpers.d.ts +26 -0
  33. package/dist/services/dataService.d.ts +5 -0
  34. package/dist/services/realtimeService.d.ts +131 -21
  35. package/dist/{src-DoU9yPqq.js → src-C_wvdMnl.js} +91 -2
  36. package/dist/src-C_wvdMnl.js.map +1 -0
  37. package/dist/utils/drizzle-conditions.d.ts +124 -2
  38. package/dist/{websocket-B2LsrINK.js → websocket-D1qbmLZ2.js} +75 -18
  39. package/dist/websocket-D1qbmLZ2.js.map +1 -0
  40. package/package.json +9 -8
  41. package/src/PostgresBackendDriver.ts +172 -6
  42. package/src/PostgresBootstrapper.ts +55 -6
  43. package/src/auth/ensure-tables.ts +207 -86
  44. package/src/auth/services.ts +69 -5
  45. package/src/cli-helpers.ts +129 -10
  46. package/src/cli.ts +232 -30
  47. package/src/collections/validate-relations.ts +124 -17
  48. package/src/data-transformer.ts +120 -17
  49. package/src/history/ensure-history-table.ts +7 -0
  50. package/src/schema/auth-schema.ts +17 -1
  51. package/src/schema/auth-users-columns.ts +131 -0
  52. package/src/schema/doctor-cli.ts +12 -63
  53. package/src/schema/doctor-policy-checks.ts +105 -0
  54. package/src/schema/doctor.ts +135 -76
  55. package/src/schema/ensure-collection-tables.ts +374 -32
  56. package/src/schema/generate-drizzle-schema-logic.ts +132 -42
  57. package/src/schema/generate-postgres-ddl-logic.ts +294 -16
  58. package/src/schema/generate-postgres-ddl.ts +25 -2
  59. package/src/schema/generated-schema-staleness.ts +169 -0
  60. package/src/schema/introspect-db-logic.ts +66 -34
  61. package/src/schema/non-sql-collections.test.ts +131 -0
  62. package/src/schema/rls-bootstrap-sql.ts +288 -0
  63. package/src/schema/search-column.ts +643 -0
  64. package/src/security/anonymous-grants.test.ts +4 -2
  65. package/src/security/rls-enforcement.ts +149 -6
  66. package/src/services/BranchService.ts +5 -0
  67. package/src/services/FetchService.ts +175 -108
  68. package/src/services/PersistService.ts +38 -2
  69. package/src/services/RelationService.ts +110 -67
  70. package/src/services/channel-history.ts +14 -0
  71. package/src/services/channel-presence.ts +13 -0
  72. package/src/services/collection-helpers.ts +54 -1
  73. package/src/services/dataService.ts +5 -0
  74. package/src/services/realtimeService.ts +344 -79
  75. package/src/utils/drizzle-conditions.ts +365 -23
  76. package/src/utils/pg-error-utils.ts +8 -3
  77. package/src/websocket.ts +113 -16
  78. package/dist/ensure-collection-tables-CBQdOETu.js.map +0 -1
  79. package/dist/policy-CeA1JcxP.js +0 -105
  80. package/dist/policy-CeA1JcxP.js.map +0 -1
  81. package/dist/schema/auth-bootstrap-sql.d.ts +0 -24
  82. package/dist/src-DlPBctw_.js.map +0 -1
  83. package/dist/src-DoU9yPqq.js.map +0 -1
  84. package/dist/websocket-B2LsrINK.js.map +0 -1
  85. package/src/schema/auth-bootstrap-sql.ts +0 -47
@@ -0,0 +1,244 @@
1
+ import { createRequire as __createRequire } from "module";
2
+ import "process";
3
+ __createRequire(import.meta.url);
4
+ import { c as __exportAll } from "./connection-BuZ97wsr.js";
5
+ import { n as REBASE_SCHEMA, t as LEGACY_RLS_SCHEMA } from "./src-C_wvdMnl.js";
6
+ //#region src/schema/rls-bootstrap-sql.ts
7
+ var rls_bootstrap_sql_exports = /* @__PURE__ */ __exportAll({
8
+ DROP_LEGACY_AUTH_SCHEMA_SQL: () => DROP_LEGACY_AUTH_SCHEMA_SQL,
9
+ LEGACY_RLS_DEPENDENTS_SQL: () => LEGACY_RLS_DEPENDENTS_SQL,
10
+ LEGACY_RLS_FUNCTION_DEPENDENTS_SQL: () => LEGACY_RLS_FUNCTION_DEPENDENTS_SQL,
11
+ RLS_BOOTSTRAP_STATEMENTS: () => RLS_BOOTSTRAP_STATEMENTS,
12
+ dropLegacyAuthSchema: () => dropLegacyAuthSchema
13
+ });
14
+ /**
15
+ * Canonical SQL bootstrap for the RLS helper functions.
16
+ *
17
+ * Generated RLS policies reference `rebase.uid()` / `rebase.roles()` /
18
+ * `rebase.jwt()`, so any SQL stream that can contain policies must be
19
+ * self-contained: it has to (re)create these helpers first. This matters for
20
+ * the migration directory in particular — Atlas replays migrations against a
21
+ * clean dev database where no out-of-band bootstrap has ever run, so a
22
+ * migration carrying policies without this preamble fails with
23
+ * "function rebase.uid() does not exist".
24
+ *
25
+ * Idempotent (`IF NOT EXISTS` / `OR REPLACE`) so it can be prepended to every
26
+ * policies block and re-applied freely. The runtime boot path
27
+ * (`auth/ensure-tables.ts`) creates the same functions under an advisory lock
28
+ * for HMR-safety; keep the definitions in sync.
29
+ *
30
+ * ## Creating the `rebase` schema here is now safe, and required
31
+ *
32
+ * It deliberately did not, once. These functions lived in a schema called
33
+ * `auth`, and the note here read: creating `rebase` would leak it into Atlas's
34
+ * replayed migration state, and — absent from the desired `schema.sql` — Atlas
35
+ * would then plan `DROP SCHEMA "rebase" CASCADE`, taking the auth tables with
36
+ * it. That reasoning still holds; what changed is the second half of it. The
37
+ * DDL generator now emits `CREATE SCHEMA IF NOT EXISTS "rebase"`
38
+ * unconditionally, so the schema is always in the desired state and the diff is
39
+ * empty. (It used to appear only when some collection happened to declare
40
+ * `schema: "rebase"` — true for the scaffold's users collection, and not a
41
+ * property anything guaranteed.) `db push` additionally excludes the whole
42
+ * schema from the declarative apply.
43
+ *
44
+ * See `@rebasepro/types`' `rls-functions` for why the functions moved out of
45
+ * `auth` at all: the short version is that the name was Supabase's, and
46
+ * `CREATE OR REPLACE FUNCTION auth.uid() RETURNS text` cannot be applied over
47
+ * Supabase's `RETURNS uuid` — Postgres refuses, and the refusal used to be
48
+ * swallowed.
49
+ */
50
+ /**
51
+ * The bootstrap as individual statements.
52
+ *
53
+ * Kept as an array because the two consumers need different shapes and only one
54
+ * of them can take a multi-command string: the migration preamble is written to
55
+ * a file and replayed by Atlas, but the boot path runs through drizzle, whose
56
+ * node-postgres handle speaks the extended query protocol and rejects more than
57
+ * one command per call. Splitting a joined string back apart on `$$;` would be
58
+ * a parser for a problem that does not need one.
59
+ */
60
+ var RLS_BOOTSTRAP_STATEMENTS = [
61
+ `CREATE SCHEMA IF NOT EXISTS ${REBASE_SCHEMA}`,
62
+ `CREATE OR REPLACE FUNCTION ${REBASE_SCHEMA}.uid() RETURNS text AS $$
63
+ SELECT COALESCE(
64
+ NULLIF(current_setting('app.uid', true), ''),
65
+ NULLIF(current_setting('app.user_id', true), '')
66
+ );
67
+ $$ LANGUAGE sql STABLE`,
68
+ `CREATE OR REPLACE FUNCTION ${REBASE_SCHEMA}.jwt() RETURNS jsonb AS $$
69
+ SELECT COALESCE(
70
+ NULLIF(current_setting('app.jwt', true), ''),
71
+ '{}'
72
+ )::jsonb;
73
+ $$ LANGUAGE sql STABLE`,
74
+ `CREATE OR REPLACE FUNCTION ${REBASE_SCHEMA}.roles() RETURNS text AS $$
75
+ SELECT COALESCE(NULLIF(current_setting('app.user_roles', true), ''), '');
76
+ $$ LANGUAGE sql STABLE`
77
+ ];
78
+ "" + RLS_BOOTSTRAP_STATEMENTS.map((s) => `${s};`).join("\n\n");
79
+ /**
80
+ * Removes the pre-1.0 `auth` schema, but only when Rebase is what put it there.
81
+ *
82
+ * ## Why this is safe against a Supabase database
83
+ *
84
+ * Two independent guards, and both have to pass:
85
+ *
86
+ * 1. **Each function is identified before it is dropped.** Ours returns `text`
87
+ * and reads the `app.uid` GUC; Supabase's returns `uuid` and reads
88
+ * `request.jwt.claims`. Nothing is dropped on a signature we did not write,
89
+ * so a Supabase database — where our `CREATE OR REPLACE` could never have
90
+ * succeeded in the first place, Postgres refusing to change a return type —
91
+ * matches nothing and this is a no-op.
92
+ * 2. **`DROP SCHEMA … RESTRICT`**, never CASCADE. If anything else at all still
93
+ * lives in `auth` (Supabase's `users` table, its other helpers), the drop
94
+ * fails and the schema stays. CASCADE here would be unrecoverable.
95
+ *
96
+ * ## Why it cannot run too early
97
+ *
98
+ * Postgres records a dependency from every RLS policy to the functions its body
99
+ * calls, so `DROP FUNCTION auth.uid()` fails for as long as a single policy
100
+ * still references it. That is the interlock, and it is load-bearing: the drop
101
+ * can only succeed once every policy has been recompiled to \`rebase.uid()\`.
102
+ * Callers therefore run this *after* applying policies, and treat a failure as
103
+ * "not yet — try again next boot" rather than as an error.
104
+ */
105
+ var DROP_LEGACY_AUTH_SCHEMA_SQL = `
106
+ DO $rebase_drop_legacy$
107
+ DECLARE
108
+ dropped_any boolean := false;
109
+ BEGIN
110
+ -- Each function is matched on its own result type and body, so a schema
111
+ -- that merely shares the name keeps everything it has.
112
+ IF EXISTS (
113
+ SELECT 1 FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
114
+ WHERE n.nspname = 'auth' AND p.proname = 'uid'
115
+ AND pg_get_function_result(p.oid) = 'text'
116
+ AND p.prosrc LIKE '%app.uid%'
117
+ ) THEN
118
+ DROP FUNCTION auth.uid();
119
+ dropped_any := true;
120
+ END IF;
121
+
122
+ IF EXISTS (
123
+ SELECT 1 FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
124
+ WHERE n.nspname = 'auth' AND p.proname = 'jwt'
125
+ AND pg_get_function_result(p.oid) = 'jsonb'
126
+ AND p.prosrc LIKE '%app.jwt%'
127
+ ) THEN
128
+ DROP FUNCTION auth.jwt();
129
+ dropped_any := true;
130
+ END IF;
131
+
132
+ IF EXISTS (
133
+ SELECT 1 FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
134
+ WHERE n.nspname = 'auth' AND p.proname = 'roles'
135
+ AND pg_get_function_result(p.oid) = 'text'
136
+ AND p.prosrc LIKE '%app.user_roles%'
137
+ ) THEN
138
+ DROP FUNCTION auth.roles();
139
+ dropped_any := true;
140
+ END IF;
141
+
142
+ -- RESTRICT: only an empty schema goes. Anything else in there — including a
143
+ -- Supabase installation left untouched above — keeps it.
144
+ IF dropped_any THEN
145
+ BEGIN
146
+ EXECUTE 'DROP SCHEMA auth RESTRICT';
147
+ EXCEPTION WHEN OTHERS THEN
148
+ NULL;
149
+ END;
150
+ END IF;
151
+ END
152
+ $rebase_drop_legacy$;
153
+ `;
154
+ /**
155
+ * Policies whose body still calls `auth.uid()` / `auth.roles()` / `auth.jwt()`.
156
+ *
157
+ * Postgres will not drop a function a policy depends on, so this is exactly the
158
+ * set standing between a database and losing the legacy schema. Rebase's own
159
+ * policies leave the list on the next push or boot, when they are recompiled —
160
+ * anything still here afterwards is hand-written, will never be recompiled by
161
+ * anybody, and is the reason the drop keeps being skipped. Silence there would
162
+ * leave an operator staring at a schema the release notes said would go.
163
+ */
164
+ var LEGACY_RLS_DEPENDENTS_SQL = `
165
+ SELECT n.nspname AS schema, c.relname AS "table", p.polname AS policy
166
+ FROM pg_policy p
167
+ JOIN pg_class c ON c.oid = p.polrelid
168
+ JOIN pg_namespace n ON n.oid = c.relnamespace
169
+ WHERE pg_get_expr(p.polqual, p.polrelid) ~* '\\m${LEGACY_RLS_SCHEMA}\\.(uid|jwt|roles)\\s*\\('
170
+ OR pg_get_expr(p.polwithcheck, p.polrelid) ~* '\\m${LEGACY_RLS_SCHEMA}\\.(uid|jwt|roles)\\s*\\('
171
+ ORDER BY 1, 2, 3
172
+ `;
173
+ /**
174
+ * Functions whose body calls `auth.uid()` / `auth.roles()` / `auth.jwt()`.
175
+ *
176
+ * This is the half `DROP FUNCTION ... RESTRICT` cannot see, and the reason it
177
+ * needs its own query. Postgres records a dependency for a *policy* that calls a
178
+ * function, which is why the drop is safe against the policies above — but a
179
+ * `LANGUAGE sql` function whose body is a **string literal** is not parsed when
180
+ * it is created, so nothing is recorded and `RESTRICT` has nothing to refuse on.
181
+ * The drop succeeds and the caller is left pointing at a function that no longer
182
+ * exists, which fails at *query* time rather than at boot.
183
+ *
184
+ * A downstream project building on these helpers is not hypothetical: the Rebase
185
+ * control plane defines `auth.is_org_member(uuid)` and `auth.is_org_admin(uuid)`
186
+ * in this very schema, each calling `auth.uid()` in a string body, and eleven of
187
+ * its row-level-security policies go through them. Every one of those would have
188
+ * started failing the first time a recompile left no policy referencing
189
+ * `auth.uid()` directly — the drop's own precondition.
190
+ *
191
+ * Matching on the body text is the only option available, and it is deliberately
192
+ * broad: a false positive costs a schema that stays one release longer and says
193
+ * why, while a false negative costs somebody their policies.
194
+ */
195
+ var LEGACY_RLS_FUNCTION_DEPENDENTS_SQL = `
196
+ SELECT n.nspname AS schema, p.proname AS function
197
+ FROM pg_proc p
198
+ JOIN pg_namespace n ON n.oid = p.pronamespace
199
+ WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
200
+ AND NOT (n.nspname = '${LEGACY_RLS_SCHEMA}' AND p.proname IN ('uid', 'jwt', 'roles'))
201
+ AND p.prosrc ~* '\\m${LEGACY_RLS_SCHEMA}\\.(uid|jwt|roles)\\s*\\('
202
+ ORDER BY 1, 2
203
+ `;
204
+ /**
205
+ * Retire the pre-1.0 `auth` schema, reporting what is holding it back.
206
+ *
207
+ * The shared implementation behind the CLI's post-push step and the runtime's
208
+ * post-policy step. Both used to just fire {@link DROP_LEGACY_AUTH_SCHEMA_SQL}
209
+ * and swallow whatever came back, which is right for the ordinary case — a
210
+ * table not recompiled *yet* — and wrong for the one that never resolves: a
211
+ * hand-written policy nothing will ever rewrite. Then the schema stays forever
212
+ * and nothing ever says why.
213
+ */
214
+ async function dropLegacyAuthSchema(run, report) {
215
+ let blockers;
216
+ try {
217
+ blockers = await run(LEGACY_RLS_DEPENDENTS_SQL);
218
+ } catch {
219
+ return;
220
+ }
221
+ if (blockers.length > 0) {
222
+ report.warn(`The pre-1.0 \`${LEGACY_RLS_SCHEMA}\` schema cannot be removed yet: ${blockers.length} ${blockers.length === 1 ? "policy still calls" : "policies still call"} \`${LEGACY_RLS_SCHEMA}.uid()\` and friends, and Postgres will not drop a function a policy depends on. Rebase's own policies are rewritten automatically — anything listed here is hand-written SQL that has to be updated to \`${REBASE_SCHEMA}.uid()\` by hand, after which the schema goes on its own:\n` + blockers.map((b) => ` • ${b.schema}.${b.table} → "${b.policy}"`).join("\n"));
223
+ return;
224
+ }
225
+ let borrowers;
226
+ try {
227
+ borrowers = await run(LEGACY_RLS_FUNCTION_DEPENDENTS_SQL);
228
+ } catch {
229
+ return;
230
+ }
231
+ if (borrowers.length > 0) {
232
+ report.warn(`The pre-1.0 \`${LEGACY_RLS_SCHEMA}\` schema cannot be removed yet: ${borrowers.length} ${borrowers.length === 1 ? "function calls" : "functions call"} \`${LEGACY_RLS_SCHEMA}.uid()\` and friends from its own body. Postgres records no dependency for that — a SQL body written as a string literal is never parsed — so dropping the helpers would leave these pointing at nothing, and they would fail when a query reached them rather than now. Repoint them at \`${REBASE_SCHEMA}.uid()\`, after which the schema goes on its own:\n` + borrowers.map((b) => ` • ${b.schema}.${b.function}()`).join("\n"));
233
+ return;
234
+ }
235
+ try {
236
+ await run(DROP_LEGACY_AUTH_SCHEMA_SQL);
237
+ } catch (err) {
238
+ report.info(`Left the \`${LEGACY_RLS_SCHEMA}\` schema in place: ` + (err instanceof Error ? err.message : String(err)));
239
+ }
240
+ }
241
+ //#endregion
242
+ export { rls_bootstrap_sql_exports as n, RLS_BOOTSTRAP_STATEMENTS as t };
243
+
244
+ //# sourceMappingURL=rls-bootstrap-sql-Bpv3nUZo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rls-bootstrap-sql-Bpv3nUZo.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"}
@@ -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";
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The one description of what an auth user table's columns must be.
3
+ *
4
+ * ## Why this exists
5
+ *
6
+ * Three different code paths create `rebase.users`, and before this module they
7
+ * disagreed about it:
8
+ *
9
+ * | column | `db push` (generator) | `ensureAuthTablesExist` | `ensureCollectionTables` |
10
+ * |------------------|-----------------------|-------------------------|--------------------------|
11
+ * | `email` | `TEXT UNIQUE NOT NULL`| `TEXT NOT NULL` + CHECK | `TEXT` (nullable) |
12
+ * | `roles` | `TEXT[]` | `NOT NULL DEFAULT '{}'` | nullable, no default |
13
+ * | `email_verified` | `BOOLEAN` | `NOT NULL DEFAULT FALSE`| nullable |
14
+ * | `created_at` | `DEFAULT now()` | `NOT NULL DEFAULT NOW()`| no default |
15
+ *
16
+ * They are all `CREATE TABLE IF NOT EXISTS` / `ADD COLUMN IF NOT EXISTS`, so
17
+ * whichever ran first decided the table and the other two silently no-op'd.
18
+ * Boot order therefore chose the constraints: a managed deploy (collection
19
+ * tables first) got a users table whose `email` was nullable and whose
20
+ * `email_verified` had no default, while the same project pushed from a
21
+ * checkout got the strict one. In the same table, `is_anonymous` came out
22
+ * `NOT NULL DEFAULT false` — because *that* column existed in only one of the
23
+ * three lists, so its owner's definition won by default.
24
+ *
25
+ * A second consequence was drift in the other direction. The scaffold's
26
+ * `users.ts` describes 12 columns; auth needs 14. `db push` is declarative and
27
+ * builds its desired state from the collection alone, so the two columns only
28
+ * auth knows about (`is_anonymous`, `tokens_valid_after`) read as unmanaged
29
+ * drift, and a push run after the server had booted once planned to DROP them.
30
+ *
31
+ * Both problems are the same problem: no single place said what this table is.
32
+ * This is that place. Every creator asks here first and only then falls back to
33
+ * the collection's own property definitions, so a column auth owns has one
34
+ * definition regardless of who gets there first, and a column the developer
35
+ * added to their users collection still behaves like any other field.
36
+ *
37
+ * ## What belongs here
38
+ *
39
+ * Only columns the auth services read or write. A developer's own additions to
40
+ * their users collection (`bio`, `stripe_customer_id`) are ordinary columns and
41
+ * must NOT be listed — they are generated from the collection like every other
42
+ * field, and listing them here would freeze a user's schema.
43
+ *
44
+ * `id` is deliberately absent: its type comes from the collection's id property
45
+ * (uuid / increment / text) and each creator already derives it.
46
+ *
47
+ * Keep in step with {@link AUTH_SCHEMA_VERSION} when a change here makes an
48
+ * older runtime unable to work against a migrated table.
49
+ */
50
+ /**
51
+ * A column auth owns.
52
+ *
53
+ * Structured rather than one SQL string because the same facts are needed in
54
+ * three grammars — a `CREATE TABLE` column list, an `ADD COLUMN IF NOT EXISTS`,
55
+ * and a pair of `ALTER COLUMN … SET DEFAULT` / `SET NOT NULL` reconciles for a
56
+ * table that already exists with the wrong shape. Parsing a string back apart
57
+ * for the third of those is how the copies drifted in the first place.
58
+ */
59
+ export interface AuthUsersColumn {
60
+ /** Physical column name. */
61
+ column: string;
62
+ /** Postgres type. */
63
+ type: string;
64
+ /** Default expression, verbatim, or absent for no default. */
65
+ default?: string;
66
+ /** Whether the column is NOT NULL. */
67
+ notNull?: boolean;
68
+ }
69
+ /**
70
+ * `email` is NOT NULL on purpose, and the anonymous sign-in route depends on it
71
+ * — it synthesizes `anon_<32 hex>@anonymous.local` rather than inserting NULL.
72
+ * The 320-char bound (RFC 5321) is a CHECK rather than a `VARCHAR(n)`, added
73
+ * separately by `ensureAuthTablesExist` so it can be `NOT VALID` on an adopted
74
+ * table that already holds a longer row.
75
+ */
76
+ export declare const AUTH_USERS_COLUMNS: readonly AuthUsersColumn[];
77
+ /** Type + inline constraints, as they appear after the column name. */
78
+ export declare function authUsersColumnSql(spec: AuthUsersColumn): string;
79
+ /**
80
+ * The auth-owned definition for a physical column name, or `undefined` when
81
+ * auth does not own it.
82
+ *
83
+ * Callers pass the RESOLVED column name (after `columnName` mapping), because
84
+ * that is the only name the three creators agree on: the scaffold's users
85
+ * collection spells the property `displayName` and the column `display_name`.
86
+ */
87
+ export declare function authUsersColumnDefinition(column: string): string | undefined;
88
+ /**
89
+ * Whether a collection is an auth collection, i.e. whether the definitions in
90
+ * this module apply to its table at all.
91
+ *
92
+ * Duplicated in shape from `@rebasepro/common`'s policy defaults on purpose:
93
+ * that one takes a `CollectionConfig`, this one is called from DDL code paths
94
+ * that hold looser objects, and both spellings must accept `auth: true` as well
95
+ * as `auth: { enabled: true }`.
96
+ */
97
+ export declare function isAuthCollection(collection: unknown): boolean;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * What the RLS checks concluded.
3
+ *
4
+ * `unchecked` exists because "we could not look" and "we looked and it is fine"
5
+ * used to be reported identically: a collections path that did not resolve made
6
+ * the loader return `[]` (it warns, it does not throw), `checkPolicyDrift`
7
+ * early-returned an empty diff, and the gate printed
8
+ * `✓ RLS policies match your collections` having compared zero policies against
9
+ * zero collections. Any exception at all — a collection file that throws on
10
+ * import, a `pg_policies` read the CI role is not granted, a connection reset —
11
+ * did the same thing through a `warn`, and exited 0.
12
+ */
13
+ export type PolicyCheckStatus = "ok" | "problems" | "unchecked";
14
+ /**
15
+ * The exit code for `rebase doctor --policies`.
16
+ *
17
+ * A gate that could not run has not passed. Only a completed, clean check
18
+ * exits 0 — anything else, including "we never opened a connection", is a
19
+ * failure, or the flag certifies a database nobody looked at.
20
+ */
21
+ export declare function exitCodeForPolicyGate(status: PolicyCheckStatus): 0 | 1;
22
+ /**
23
+ * Policies actually deployed vs the ones the collections describe, plus policy
24
+ * roles this server could never satisfy.
25
+ *
26
+ * Never reports `ok` for work it did not do — see {@link PolicyCheckStatus}.
27
+ */
28
+ export declare function runPolicyChecks(collectionsPath: string, databaseUrl?: string): Promise<PolicyCheckStatus>;
@@ -2,7 +2,7 @@ import { CollectionConfig, Property } from "@rebasepro/types";
2
2
  export type IssueSeverity = "error" | "warning" | "info";
3
3
  export interface DoctorIssue {
4
4
  severity: IssueSeverity;
5
- category: "missing_table" | "missing_column" | "type_mismatch" | "missing_constraint" | "schema_stale" | "missing_enum" | "enum_value_mismatch" | "missing_foreign_key" | "sdk_stale" | "sdk_not_generated";
5
+ category: "missing_table" | "missing_column" | "type_mismatch" | "missing_constraint" | "schema_stale" | "missing_enum" | "enum_value_mismatch" | "missing_foreign_key" | "sdk_stale" | "sdk_not_generated" | "sdk_ungeneratable";
6
6
  table?: string;
7
7
  column?: string;
8
8
  expected?: string;
@@ -10,21 +10,28 @@ export interface DoctorIssue {
10
10
  message: string;
11
11
  fix: string;
12
12
  }
13
+ export interface DoctorPhase {
14
+ passed: boolean;
15
+ issues: DoctorIssue[];
16
+ /**
17
+ * Why this phase never ran, when it did not.
18
+ *
19
+ * A check that did not happen is a third state, not a passing one. While a
20
+ * skipped phase initialised to `{ passed: true, issues: [] }` it rendered as
21
+ * `✅ Collections → Database: In sync` and counted towards
22
+ * `✓ All schemas are in sync!` — so a project whose connection string was
23
+ * spelled `POSTGRES_URL`, or a CI job that never exported one, got two green
24
+ * ticks and exit 0 against a database with no tables in it.
25
+ */
26
+ skipped?: string;
27
+ }
13
28
  export interface DoctorReport {
14
- collectionsToSchema: {
15
- passed: boolean;
16
- issues: DoctorIssue[];
17
- };
18
- collectionsToSdk: {
19
- passed: boolean;
20
- issues: DoctorIssue[];
21
- };
22
- schemaToDatabase: {
23
- passed: boolean;
24
- issues: DoctorIssue[];
25
- };
29
+ collectionsToSchema: DoctorPhase;
30
+ collectionsToSdk: DoctorPhase;
31
+ schemaToDatabase: DoctorPhase;
26
32
  summary: {
27
33
  passed: number;
34
+ skipped: number;
28
35
  warnings: number;
29
36
  errors: number;
30
37
  };
@@ -37,18 +44,9 @@ export declare function getExpectedColumnType(prop: Property): string | null;
37
44
  * wrong thing.
38
45
  */
39
46
  export declare function loadCollections(collectionsPath: string): Promise<CollectionConfig[]>;
40
- export declare function checkCollectionsVsSchema(collections: CollectionConfig[], schemaFilePath: string): Promise<{
41
- passed: boolean;
42
- issues: DoctorIssue[];
43
- }>;
44
- export declare function checkCollectionsVsSdk(collections: CollectionConfig[], sdkFilePath: string): Promise<{
45
- passed: boolean;
46
- issues: DoctorIssue[];
47
- }>;
48
- export declare function checkCollectionsVsDatabase(collections: CollectionConfig[], databaseUrl: string): Promise<{
49
- passed: boolean;
50
- issues: DoctorIssue[];
51
- }>;
47
+ export declare function checkCollectionsVsSchema(collections: CollectionConfig[], schemaFilePath: string): Promise<DoctorPhase>;
48
+ export declare function checkCollectionsVsSdk(collections: CollectionConfig[], sdkFilePath: string): Promise<DoctorPhase>;
49
+ export declare function checkCollectionsVsDatabase(collections: CollectionConfig[], databaseUrl: string): Promise<DoctorPhase>;
52
50
  export declare function renderReport(report: DoctorReport): void;
53
51
  export declare function runDoctor(options: {
54
52
  collectionsPath: string;