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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/dist/PostgresBackendDriver.d.ts +48 -1
  2. package/dist/PostgresBootstrapper.d.ts +26 -0
  3. package/dist/auth/services.d.ts +21 -0
  4. package/dist/{src-DlPBctw_.js → auth-users-columns-BfQHf9JE.js} +1222 -86
  5. package/dist/auth-users-columns-BfQHf9JE.js.map +1 -0
  6. package/dist/{backup-service-CD8o_1Sl.js → backup-service-BH0Dzo_h.js} +2 -3
  7. package/dist/{backup-service-CD8o_1Sl.js.map → backup-service-BH0Dzo_h.js.map} +1 -1
  8. package/dist/cli-helpers.d.ts +57 -1
  9. package/dist/cli-output.d.ts +34 -0
  10. package/dist/data-transformer.d.ts +7 -2
  11. package/dist/data_driver-ULAyJEi9.js +193 -0
  12. package/dist/data_driver-ULAyJEi9.js.map +1 -0
  13. package/dist/ensure-collection-policies-8vuu-n4r.js +124 -0
  14. package/dist/ensure-collection-policies-8vuu-n4r.js.map +1 -0
  15. package/dist/{ensure-collection-tables-CBQdOETu.js → ensure-collection-tables-CbvaGuVn.js} +170 -20
  16. package/dist/ensure-collection-tables-CbvaGuVn.js.map +1 -0
  17. package/dist/index.es.js +1815 -1023
  18. package/dist/index.es.js.map +1 -1
  19. package/dist/rls-bootstrap-sql-69hYT8nr.js +244 -0
  20. package/dist/rls-bootstrap-sql-69hYT8nr.js.map +1 -0
  21. package/dist/rls-enforcement-BJ_3wxwg.js +425 -0
  22. package/dist/rls-enforcement-BJ_3wxwg.js.map +1 -0
  23. package/dist/schema/auth-schema.d.ts +102 -0
  24. package/dist/schema/auth-users-columns.d.ts +97 -0
  25. package/dist/schema/doctor-policy-checks.d.ts +28 -0
  26. package/dist/schema/doctor.d.ts +41 -25
  27. package/dist/schema/ensure-collection-policies.d.ts +33 -9
  28. package/dist/schema/ensure-collection-tables.d.ts +61 -7
  29. package/dist/schema/generate-drizzle-schema-logic.d.ts +10 -2
  30. package/dist/schema/generate-postgres-ddl-logic.d.ts +53 -5
  31. package/dist/schema/generated-schema-staleness.d.ts +39 -0
  32. package/dist/schema/introspect-db-inference.d.ts +8 -1
  33. package/dist/schema/introspect-db-logic.d.ts +49 -0
  34. package/dist/schema/introspect-db-project.d.ts +21 -0
  35. package/dist/schema/rls-bootstrap-sql.d.ts +135 -0
  36. package/dist/schema/search-column.d.ts +248 -0
  37. package/dist/security/policy-drift.d.ts +34 -0
  38. package/dist/security/rls-enforcement.d.ts +61 -5
  39. package/dist/services/FetchService.d.ts +34 -7
  40. package/dist/services/PersistService.d.ts +21 -17
  41. package/dist/services/RelationService.d.ts +9 -57
  42. package/dist/services/RelationWriteService.d.ts +82 -0
  43. package/dist/services/collection-helpers.d.ts +42 -0
  44. package/dist/services/dataService.d.ts +5 -0
  45. package/dist/services/junction-writes.d.ts +82 -0
  46. package/dist/services/realtimeService.d.ts +164 -23
  47. package/dist/services/write-denial.d.ts +36 -0
  48. package/dist/{src-DoU9yPqq.js → src-DCdn3Val.js} +124 -3
  49. package/dist/src-DCdn3Val.js.map +1 -0
  50. package/dist/utils/drizzle-conditions.d.ts +124 -2
  51. package/dist/{websocket-B2LsrINK.js → websocket-C8ZqVBiV.js} +75 -18
  52. package/dist/websocket-C8ZqVBiV.js.map +1 -0
  53. package/package.json +9 -8
  54. package/src/PostgresBackendDriver.ts +172 -6
  55. package/src/PostgresBootstrapper.ts +136 -11
  56. package/src/auth/ensure-tables.ts +212 -91
  57. package/src/auth/services.ts +82 -5
  58. package/src/backup/backup-cli.ts +59 -57
  59. package/src/cli-errors.ts +6 -6
  60. package/src/cli-helpers.ts +132 -13
  61. package/src/cli-output.ts +43 -0
  62. package/src/cli.ts +371 -161
  63. package/src/collections/buildRegistry.ts +3 -1
  64. package/src/collections/validate-relations.ts +124 -17
  65. package/src/data-transformer.ts +142 -28
  66. package/src/history/ensure-history-table.ts +9 -2
  67. package/src/schema/auth-schema.ts +17 -1
  68. package/src/schema/auth-users-columns.ts +131 -0
  69. package/src/schema/doctor-cli.ts +14 -65
  70. package/src/schema/doctor-policy-checks.ts +105 -0
  71. package/src/schema/doctor.ts +156 -77
  72. package/src/schema/ensure-collection-policies.ts +99 -6
  73. package/src/schema/ensure-collection-tables.ts +374 -32
  74. package/src/schema/generate-drizzle-schema-logic.ts +152 -66
  75. package/src/schema/generate-drizzle-schema.ts +11 -10
  76. package/src/schema/generate-postgres-ddl-logic.ts +294 -16
  77. package/src/schema/generate-postgres-ddl.ts +38 -14
  78. package/src/schema/generated-schema-staleness.ts +171 -0
  79. package/src/schema/introspect-db-inference.ts +9 -2
  80. package/src/schema/introspect-db-logic.ts +251 -75
  81. package/src/schema/introspect-db-project.ts +78 -0
  82. package/src/schema/introspect-db.ts +42 -25
  83. package/src/schema/introspect-runtime.ts +14 -2
  84. package/src/schema/non-sql-collections.test.ts +131 -0
  85. package/src/schema/rls-bootstrap-sql.ts +288 -0
  86. package/src/schema/search-column.ts +643 -0
  87. package/src/security/anonymous-grants.test.ts +4 -2
  88. package/src/security/policy-drift.test.ts +104 -3
  89. package/src/security/policy-drift.ts +129 -7
  90. package/src/security/rls-enforcement.ts +150 -7
  91. package/src/services/BranchService.ts +5 -0
  92. package/src/services/FetchService.ts +253 -115
  93. package/src/services/PersistService.ts +82 -43
  94. package/src/services/RelationService.ts +37 -696
  95. package/src/services/RelationWriteService.ts +653 -0
  96. package/src/services/cdc/trigger-cdc.ts +5 -1
  97. package/src/services/channel-history.ts +14 -0
  98. package/src/services/channel-presence.ts +13 -0
  99. package/src/services/collection-helpers.ts +89 -4
  100. package/src/services/dataService.ts +5 -0
  101. package/src/services/junction-writes.ts +295 -0
  102. package/src/services/pg-notify-listener.ts +1 -1
  103. package/src/services/realtimeService.ts +382 -118
  104. package/src/services/write-denial.ts +55 -0
  105. package/src/utils/drizzle-conditions.ts +433 -35
  106. package/src/utils/pg-error-utils.ts +8 -3
  107. package/src/websocket.ts +113 -16
  108. package/dist/ensure-collection-policies-ViG8XiPn.js +0 -57
  109. package/dist/ensure-collection-policies-ViG8XiPn.js.map +0 -1
  110. package/dist/ensure-collection-tables-CBQdOETu.js.map +0 -1
  111. package/dist/policy-CeA1JcxP.js +0 -105
  112. package/dist/policy-CeA1JcxP.js.map +0 -1
  113. package/dist/schema/auth-bootstrap-sql.d.ts +0 -24
  114. package/dist/src-DlPBctw_.js.map +0 -1
  115. package/dist/src-DoU9yPqq.js.map +0 -1
  116. package/dist/websocket-B2LsrINK.js.map +0 -1
  117. package/src/schema/auth-bootstrap-sql.ts +0 -47
@@ -0,0 +1,131 @@
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
+ /**
52
+ * A column auth owns.
53
+ *
54
+ * Structured rather than one SQL string because the same facts are needed in
55
+ * three grammars — a `CREATE TABLE` column list, an `ADD COLUMN IF NOT EXISTS`,
56
+ * and a pair of `ALTER COLUMN … SET DEFAULT` / `SET NOT NULL` reconciles for a
57
+ * table that already exists with the wrong shape. Parsing a string back apart
58
+ * for the third of those is how the copies drifted in the first place.
59
+ */
60
+ export interface AuthUsersColumn {
61
+ /** Physical column name. */
62
+ column: string;
63
+ /** Postgres type. */
64
+ type: string;
65
+ /** Default expression, verbatim, or absent for no default. */
66
+ default?: string;
67
+ /** Whether the column is NOT NULL. */
68
+ notNull?: boolean;
69
+ }
70
+
71
+ /**
72
+ * `email` is NOT NULL on purpose, and the anonymous sign-in route depends on it
73
+ * — it synthesizes `anon_<32 hex>@anonymous.local` rather than inserting NULL.
74
+ * The 320-char bound (RFC 5321) is a CHECK rather than a `VARCHAR(n)`, added
75
+ * separately by `ensureAuthTablesExist` so it can be `NOT VALID` on an adopted
76
+ * table that already holds a longer row.
77
+ */
78
+ export const AUTH_USERS_COLUMNS: readonly AuthUsersColumn[] = [
79
+ { column: "email", type: "TEXT", notNull: true },
80
+ { column: "display_name", type: "TEXT" },
81
+ { column: "photo_url", type: "TEXT" },
82
+ { column: "roles", type: "TEXT[]", default: "'{}'", notNull: true },
83
+ { column: "password_hash", type: "TEXT" },
84
+ { column: "email_verified", type: "BOOLEAN", default: "FALSE", notNull: true },
85
+ { column: "email_verification_token", type: "TEXT" },
86
+ { column: "email_verification_sent_at", type: "TIMESTAMP WITH TIME ZONE" },
87
+ { column: "is_anonymous", type: "BOOLEAN", default: "FALSE", notNull: true },
88
+ { column: "metadata", type: "JSONB", default: "'{}'", notNull: true },
89
+ { column: "tokens_valid_after", type: "TIMESTAMP WITH TIME ZONE" },
90
+ { column: "created_at", type: "TIMESTAMP WITH TIME ZONE", default: "NOW()", notNull: true },
91
+ { column: "updated_at", type: "TIMESTAMP WITH TIME ZONE", default: "NOW()", notNull: true }
92
+ ];
93
+
94
+ const BY_COLUMN = new Map(AUTH_USERS_COLUMNS.map(c => [c.column, c]));
95
+
96
+ /** Type + inline constraints, as they appear after the column name. */
97
+ export function authUsersColumnSql(spec: AuthUsersColumn): string {
98
+ return [
99
+ spec.type,
100
+ spec.default !== undefined ? `DEFAULT ${spec.default}` : "",
101
+ spec.notNull ? "NOT NULL" : ""
102
+ ].filter(Boolean).join(" ");
103
+ }
104
+
105
+ /**
106
+ * The auth-owned definition for a physical column name, or `undefined` when
107
+ * auth does not own it.
108
+ *
109
+ * Callers pass the RESOLVED column name (after `columnName` mapping), because
110
+ * that is the only name the three creators agree on: the scaffold's users
111
+ * collection spells the property `displayName` and the column `display_name`.
112
+ */
113
+ export function authUsersColumnDefinition(column: string): string | undefined {
114
+ const spec = BY_COLUMN.get(column);
115
+ return spec ? authUsersColumnSql(spec) : undefined;
116
+ }
117
+
118
+ /**
119
+ * Whether a collection is an auth collection, i.e. whether the definitions in
120
+ * this module apply to its table at all.
121
+ *
122
+ * Duplicated in shape from `@rebasepro/common`'s policy defaults on purpose:
123
+ * that one takes a `CollectionConfig`, this one is called from DDL code paths
124
+ * that hold looser objects, and both spellings must accept `auth: true` as well
125
+ * as `auth: { enabled: true }`.
126
+ */
127
+ export function isAuthCollection(collection: unknown): boolean {
128
+ const auth = (collection as { auth?: unknown } | undefined)?.auth;
129
+ if (auth === true) return true;
130
+ return typeof auth === "object" && auth !== null && (auth as { enabled?: unknown }).enabled === true;
131
+ }
@@ -5,62 +5,8 @@
5
5
  */
6
6
  import path from "path";
7
7
  import chalk from "chalk";
8
- import fs from "fs";
9
- import { runDoctor, loadCollections } from "./doctor";
10
- import { checkPolicyDrift, formatPolicyDrift, hasDrift } from "../security/policy-drift";
11
- import { validatePolicyPgRoles, warnOnAnonymousGrants } from "../security/rls-enforcement";
12
- import { logger } from "@rebasepro/server";
13
-
14
- /**
15
- * The RLS half of the doctor: policies actually deployed vs the ones the
16
- * collections describe, plus policy roles this server could never satisfy.
17
- *
18
- * Returns true when something is wrong, so callers can set the exit code.
19
- */
20
- async function runPolicyChecks(collectionsPath: string, databaseUrl?: string): Promise<boolean> {
21
- if (!databaseUrl) {
22
- logger.warn(chalk.yellow(" ⚠ No DATABASE_URL — skipping RLS policy checks"));
23
- return false;
24
- }
25
-
26
- let problems = false;
27
- const { Pool } = await import("pg");
28
- const pool = new Pool({ connectionString: databaseUrl });
29
- try {
30
- const collections = await loadCollections(path.resolve(process.cwd(), collectionsPath));
31
- const runSql = async (text: string) => (await pool.query(text)).rows as Record<string, unknown>[];
32
-
33
- // A policy naming a role the server never runs as filters every row, so
34
- // the collection reads as empty. Report it without booting a server.
35
- try {
36
- await validatePolicyPgRoles(runSql, collections as never);
37
- logger.info(chalk.green(" ✓ Policy roles are usable by this server"));
38
- } catch (err) {
39
- problems = true;
40
- logger.info("");
41
- logger.error(chalk.red(err instanceof Error ? err.message : String(err)));
42
- }
43
-
44
- // A rule that reads as a lockdown but is true for every caller compiles
45
- // to a grant. Report it without booting a server.
46
- warnOnAnonymousGrants(collections as never);
47
-
48
- const drift = await checkPolicyDrift(pool as never, collections);
49
- logger.info("");
50
- if (hasDrift(drift)) {
51
- problems = true;
52
- logger.info(chalk.yellow(" RLS policies: database does not match your collections"));
53
- logger.info(formatPolicyDrift(drift));
54
- } else {
55
- logger.info(chalk.green(" ✓ RLS policies match your collections"));
56
- }
57
- } catch (err) {
58
- logger.warn(chalk.yellow(" ⚠ Could not check RLS policies"), { error: err });
59
- } finally {
60
- await pool.end();
61
- }
62
- return problems;
63
- }
8
+ import { runDoctor } from "./doctor";
9
+ import { exitCodeForPolicyGate, runPolicyChecks } from "./doctor-policy-checks";
64
10
 
65
11
  async function main() {
66
12
  const collectionsArg = process.argv.find((a) => a.startsWith("--collections="));
@@ -80,9 +26,9 @@ async function main() {
80
26
  const dotenv = await import("dotenv");
81
27
  const envPath = process.env.DOTENV_CONFIG_PATH;
82
28
  if (envPath) {
83
- dotenv.config({ path: envPath });
29
+ dotenv.config({ path: envPath, quiet: true });
84
30
  } else {
85
- dotenv.config();
31
+ dotenv.config({ quiet: true });
86
32
  }
87
33
  } catch {
88
34
  // dotenv may not be installed
@@ -91,9 +37,9 @@ async function main() {
91
37
  const databaseUrl = process.env.DATABASE_URL || process.env.ADMIN_CONNECTION_STRING;
92
38
 
93
39
  if (policiesOnly) {
94
- // Non-zero so this can gate CI — the whole point of the flag.
95
- if (await runPolicyChecks(collectionsPath, databaseUrl)) process.exit(1);
96
- return;
40
+ // Non-zero so this can gate CI — the whole point of the flag, and a
41
+ // check that could not run has not passed. See exitCodeForPolicyGate.
42
+ process.exit(exitCodeForPolicyGate(await runPolicyChecks(collectionsPath, databaseUrl)));
97
43
  }
98
44
 
99
45
  const report = await runDoctor({
@@ -103,15 +49,18 @@ async function main() {
103
49
  databaseUrl: databaseUrl ?? undefined
104
50
  });
105
51
 
106
- const policiesDrifted = await runPolicyChecks(collectionsPath, databaseUrl);
52
+ const policyStatus = await runPolicyChecks(collectionsPath, databaseUrl);
107
53
 
108
- // Exit with non-zero code if there are errors
109
- if (report.summary.errors > 0 || policiesDrifted) {
54
+ // Exit non-zero if there are errors. A policy run that could not happen is
55
+ // reported loudly above but does not fail the interactive command — the
56
+ // same treatment the skipped database phase gets. `--policies` is the gate,
57
+ // and that one fails closed.
58
+ if (report.summary.errors > 0 || policyStatus === "problems") {
110
59
  process.exit(1);
111
60
  }
112
61
  }
113
62
 
114
63
  main().catch((err) => {
115
- logger.error(chalk.red(" ✗ Doctor failed"), { error: err });
64
+ console.error(chalk.red(" ✗ Doctor failed"), err instanceof Error ? (err.stack ?? err.message) : String(err));
116
65
  process.exit(1);
117
66
  });
@@ -0,0 +1,105 @@
1
+ /**
2
+ * The RLS half of `rebase doctor`, and the exit code `--policies` gates CI on.
3
+ *
4
+ * Lives beside `doctor-cli.ts` rather than inside it because that file runs
5
+ * `main()` on import: a gate whose failure modes cannot be unit-tested is how
6
+ * this one shipped reporting success for work it never did.
7
+ */
8
+ import path from "path";
9
+ import chalk from "chalk";
10
+ import { CollectionConfig } from "@rebasepro/types";
11
+ import { loadCollections } from "./doctor";
12
+ import { checkPolicyDrift, formatPolicyDrift, hasDrift } from "../security/policy-drift";
13
+ import { validatePolicyPgRoles, warnOnAnonymousGrants } from "../security/rls-enforcement";
14
+
15
+ /**
16
+ * What the RLS checks concluded.
17
+ *
18
+ * `unchecked` exists because "we could not look" and "we looked and it is fine"
19
+ * used to be reported identically: a collections path that did not resolve made
20
+ * the loader return `[]` (it warns, it does not throw), `checkPolicyDrift`
21
+ * early-returned an empty diff, and the gate printed
22
+ * `✓ RLS policies match your collections` having compared zero policies against
23
+ * zero collections. Any exception at all — a collection file that throws on
24
+ * import, a `pg_policies` read the CI role is not granted, a connection reset —
25
+ * did the same thing through a `warn`, and exited 0.
26
+ */
27
+ export type PolicyCheckStatus = "ok" | "problems" | "unchecked";
28
+
29
+ /**
30
+ * The exit code for `rebase doctor --policies`.
31
+ *
32
+ * A gate that could not run has not passed. Only a completed, clean check
33
+ * exits 0 — anything else, including "we never opened a connection", is a
34
+ * failure, or the flag certifies a database nobody looked at.
35
+ */
36
+ export function exitCodeForPolicyGate(status: PolicyCheckStatus): 0 | 1 {
37
+ return status === "ok" ? 0 : 1;
38
+ }
39
+
40
+ /**
41
+ * Policies actually deployed vs the ones the collections describe, plus policy
42
+ * roles this server could never satisfy.
43
+ *
44
+ * Never reports `ok` for work it did not do — see {@link PolicyCheckStatus}.
45
+ */
46
+ export async function runPolicyChecks(collectionsPath: string, databaseUrl?: string): Promise<PolicyCheckStatus> {
47
+ if (!databaseUrl) {
48
+ console.error(chalk.yellow(" ⚠ No DATABASE_URL — RLS policies were NOT checked"));
49
+ return "unchecked";
50
+ }
51
+
52
+ const resolvedPath = path.resolve(process.cwd(), collectionsPath);
53
+ let problems = false;
54
+ const { Pool } = await import("pg");
55
+ const pool = new Pool({ connectionString: databaseUrl });
56
+ try {
57
+ const collections: CollectionConfig[] = await loadCollections(resolvedPath);
58
+
59
+ // Zero collections is not "no drift", it is nothing to compare. The
60
+ // loader returns `[]` for a path that does not exist, so the commonest
61
+ // way to get here is a `--collections` path resolved against the wrong
62
+ // directory — which used to render as a green tick.
63
+ if (collections.length === 0) {
64
+ console.error(chalk.red(` ✗ No collections found in ${resolvedPath}`));
65
+ console.error(chalk.gray(" RLS policies were NOT checked. Pass --collections=<dir> if your collections live elsewhere."));
66
+ return "unchecked";
67
+ }
68
+
69
+ const runSql = async (text: string) => (await pool.query(text)).rows as Record<string, unknown>[];
70
+
71
+ // A policy naming a role the server never runs as filters every row, so
72
+ // the collection reads as empty. Report it without booting a server.
73
+ try {
74
+ await validatePolicyPgRoles(runSql, collections as never);
75
+ console.log(chalk.green(" ✓ Policy roles are usable by this server"));
76
+ } catch (err) {
77
+ problems = true;
78
+ console.log("");
79
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
80
+ }
81
+
82
+ // A rule that reads as a lockdown but is true for every caller compiles
83
+ // to a grant. Report it without booting a server.
84
+ warnOnAnonymousGrants(collections as never);
85
+
86
+ const drift = await checkPolicyDrift(pool as never, collections);
87
+ console.log("");
88
+ if (hasDrift(drift)) {
89
+ problems = true;
90
+ console.log(chalk.yellow(" RLS policies: database does not match your collections"));
91
+ console.log(formatPolicyDrift(drift));
92
+ } else {
93
+ console.log(chalk.green(` ✓ RLS policies match your collections (${collections.length} collection(s) checked)`));
94
+ }
95
+ } catch (err) {
96
+ // Fail closed. This catch covers every query and every collection import
97
+ // above, so returning the pre-catch verdict made the documented CI gate
98
+ // green on exactly the runs it exists to catch.
99
+ console.error(chalk.red(" ✗ Could not check RLS policies:"), err instanceof Error ? err.message : String(err));
100
+ return "unchecked";
101
+ } finally {
102
+ await pool.end();
103
+ }
104
+ return problems ? "problems" : "ok";
105
+ }