@rebasepro/server-postgres 0.24.0 → 0.24.1-canary.g56af4a4

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 (162) hide show
  1. package/dist/{BranchService-BRt78gfa.js → BranchService-Bsn2wfaj.js} +3 -2
  2. package/dist/{BranchService-BRt78gfa.js.map → BranchService-Bsn2wfaj.js.map} +1 -1
  3. package/dist/PostgresBackendDriver.d.ts +83 -2
  4. package/dist/auth/ensure-tables.d.ts +12 -0
  5. package/dist/auth/services.d.ts +6 -0
  6. package/dist/backup/backup-cli.d.ts +14 -0
  7. package/dist/backup/backup-service.d.ts +2 -2
  8. package/dist/backup/pg-binary.d.ts +5 -0
  9. package/dist/{backup-cli-DW5p9_zv.js → backup-cli-aUfRv16h.js} +37 -29
  10. package/dist/backup-cli-aUfRv16h.js.map +1 -0
  11. package/dist/{backup-service-EwcDVG-8.js → backup-service-BTBB7RXn.js} +55 -21
  12. package/dist/backup-service-BTBB7RXn.js.map +1 -0
  13. package/dist/boot-ddl-DVpsFN6r.js +105 -0
  14. package/dist/boot-ddl-DVpsFN6r.js.map +1 -0
  15. package/dist/cli-errors-CaI4ntyD.js +1919 -0
  16. package/dist/cli-errors-CaI4ntyD.js.map +1 -0
  17. package/dist/cli-errors.d.ts +51 -5
  18. package/dist/{cli-flags-BglvpjHv.js → cli-flags-B-Dawocl.js} +5 -2
  19. package/dist/cli-flags-B-Dawocl.js.map +1 -0
  20. package/dist/cli-helpers.d.ts +40 -0
  21. package/dist/cli-output.d.ts +16 -28
  22. package/dist/cli-push.d.ts +87 -0
  23. package/dist/cli.js +563 -182
  24. package/dist/cli.js.map +1 -1
  25. package/dist/{column-plan-helpers-1-LQD0yI.js → column-plan-helpers-drZIsSNf.js} +10 -4
  26. package/dist/{column-plan-helpers-1-LQD0yI.js.map → column-plan-helpers-drZIsSNf.js.map} +1 -1
  27. package/dist/{databasePoolManager-Bj5FbeAs.js → connection-BPoNILnD.js} +19 -110
  28. package/dist/connection-BPoNILnD.js.map +1 -0
  29. package/dist/connection.d.ts +9 -1
  30. package/dist/databasePoolManager-BkEy2w18.js +119 -0
  31. package/dist/databasePoolManager-BkEy2w18.js.map +1 -0
  32. package/dist/{ensure-collection-tables-kkkHk8oo.js → deferred-index-build-DP1_SNFo.js} +484 -346
  33. package/dist/deferred-index-build-DP1_SNFo.js.map +1 -0
  34. package/dist/{doctor-C-sYWbmt.js → doctor-C4WiftHm.js} +63 -52
  35. package/dist/doctor-C4WiftHm.js.map +1 -0
  36. package/dist/ensure-collection-policies-CXpbeNag.js +541 -0
  37. package/dist/ensure-collection-policies-CXpbeNag.js.map +1 -0
  38. package/dist/ensure-collection-tables-BhsU6qUE.js +532 -0
  39. package/dist/ensure-collection-tables-BhsU6qUE.js.map +1 -0
  40. package/dist/{ensure-tables-BmI_tRxc.js → ensure-tables-CHsgyLqg.js} +172 -76
  41. package/dist/ensure-tables-CHsgyLqg.js.map +1 -0
  42. package/dist/enum-labels-kept-D8fI1MoX.js +56 -0
  43. package/dist/enum-labels-kept-D8fI1MoX.js.map +1 -0
  44. package/dist/generate-drizzle-schema-Bz52jH2h.js +57 -0
  45. package/dist/generate-drizzle-schema-Bz52jH2h.js.map +1 -0
  46. package/dist/{generate-drizzle-schema-logic-sSFDp6LR.js → generate-drizzle-schema-logic-B74WN37S.js} +5 -5
  47. package/dist/generate-drizzle-schema-logic-B74WN37S.js.map +1 -0
  48. package/dist/generate-files-CdEckX6c.js +199 -0
  49. package/dist/generate-files-CdEckX6c.js.map +1 -0
  50. package/dist/{generate-postgres-ddl-logic-BJsLaVNX.js → generate-postgres-ddl-logic-B64O2oZF.js} +3 -3
  51. package/dist/{generate-postgres-ddl-logic-BJsLaVNX.js.map → generate-postgres-ddl-logic-B64O2oZF.js.map} +1 -1
  52. package/dist/generated-sql.d.ts +8 -2
  53. package/dist/history/HistoryService.d.ts +38 -6
  54. package/dist/history/ensure-history-table.d.ts +42 -4
  55. package/dist/index.es.js +2922 -7475
  56. package/dist/index.es.js.map +1 -1
  57. package/dist/{introspect-db-logic-kCETE8TY.js → introspect-db-logic-C-Di8dA9.js} +44 -6
  58. package/dist/introspect-db-logic-C-Di8dA9.js.map +1 -0
  59. package/dist/{introspect-db-queries-C_Q5VgQw.js → introspect-db-queries-D6kAqReI.js} +23 -1
  60. package/dist/introspect-db-queries-D6kAqReI.js.map +1 -0
  61. package/dist/pg-error-utils-vJezlXdI.js +388 -0
  62. package/dist/pg-error-utils-vJezlXdI.js.map +1 -0
  63. package/dist/{plan-schema-DU9exq6C.js → plan-schema-B1YF_tYq.js} +151 -41
  64. package/dist/plan-schema-B1YF_tYq.js.map +1 -0
  65. package/dist/{policy-drift-B-J2hhm0.js → policy-drift-Dm8i-s1J.js} +23 -6
  66. package/dist/policy-drift-Dm8i-s1J.js.map +1 -0
  67. package/dist/push-index-prebuild-Db4AQF4i.js +45 -0
  68. package/dist/push-index-prebuild-Db4AQF4i.js.map +1 -0
  69. package/dist/push-legacy-renames-BK6ilQq2.js +34 -0
  70. package/dist/push-legacy-renames-BK6ilQq2.js.map +1 -0
  71. package/dist/{render-ddl-Ds2t_d9V.js → render-ddl-CtC3y9Ol.js} +8 -4
  72. package/dist/render-ddl-CtC3y9Ol.js.map +1 -0
  73. package/dist/{rls-bootstrap-sql-_KNnjanK.js → rls-bootstrap-sql-RQUljnJ1.js} +125 -1270
  74. package/dist/rls-bootstrap-sql-RQUljnJ1.js.map +1 -0
  75. package/dist/{rls-enforcement-CfXOJJaW.js → rls-enforcement-DvzqlaL1.js} +129 -17
  76. package/dist/rls-enforcement-DvzqlaL1.js.map +1 -0
  77. package/dist/schema/auth-schema.d.ts +2 -2
  78. package/dist/schema/boot-ddl.d.ts +47 -0
  79. package/dist/schema/collection-index.d.ts +45 -0
  80. package/dist/schema/column-plan-helpers.d.ts +5 -0
  81. package/dist/schema/deferred-index-build.d.ts +131 -0
  82. package/dist/schema/destructive-sql.d.ts +7 -0
  83. package/dist/schema/doctor-cli.js +110 -6
  84. package/dist/schema/doctor-cli.js.map +1 -1
  85. package/dist/schema/doctor-search-scale.d.ts +45 -0
  86. package/dist/schema/doctor.d.ts +20 -1
  87. package/dist/schema/ensure-collection-policies.d.ts +73 -35
  88. package/dist/schema/ensure-collection-tables.d.ts +56 -3
  89. package/dist/schema/enum-labels-kept.d.ts +26 -0
  90. package/dist/schema/generate-drizzle-schema.js +1 -1
  91. package/dist/schema/generate-files.d.ts +44 -0
  92. package/dist/schema/generate-next-step.d.ts +0 -13
  93. package/dist/schema/generate-postgres-ddl.js +10 -81
  94. package/dist/schema/generate-postgres-ddl.js.map +1 -1
  95. package/dist/schema/introspect-db-logic.d.ts +12 -0
  96. package/dist/schema/introspect-db-queries.d.ts +11 -1
  97. package/dist/schema/introspect-db-structure.d.ts +10 -0
  98. package/dist/schema/introspect-db.js +4 -5
  99. package/dist/schema/introspect-db.js.map +1 -1
  100. package/dist/schema/migration-post-process.d.ts +34 -0
  101. package/dist/schema/plan/diff-plan.d.ts +126 -0
  102. package/dist/schema/plan/types.d.ts +8 -0
  103. package/dist/schema/push-index-prebuild.d.ts +25 -0
  104. package/dist/schema/push-legacy-renames.d.ts +20 -0
  105. package/dist/schema/rls-bootstrap-sql.d.ts +15 -0
  106. package/dist/security/rls-enforcement.d.ts +32 -4
  107. package/dist/services/FetchService.d.ts +58 -8
  108. package/dist/services/PersistService.d.ts +15 -0
  109. package/dist/services/RelationService.d.ts +88 -3
  110. package/dist/services/cdc/CdcListener.d.ts +17 -6
  111. package/dist/services/cdc/trigger-cdc.d.ts +151 -27
  112. package/dist/services/channel-history.d.ts +15 -4
  113. package/dist/services/collection-helpers.d.ts +15 -0
  114. package/dist/services/field-viewer.d.ts +15 -1
  115. package/dist/services/pg-notify-listener.d.ts +7 -0
  116. package/dist/services/realtimeService.d.ts +314 -14
  117. package/dist/services/row-pipeline.d.ts +16 -9
  118. package/dist/services/socket-liveness.d.ts +42 -2
  119. package/dist/services/sql-script.d.ts +207 -3
  120. package/dist/services/vector-iterative-scan.d.ts +40 -0
  121. package/dist/services/write-depth.d.ts +6 -0
  122. package/dist/services/write-transaction-scope.d.ts +9 -0
  123. package/dist/socket-liveness-D3UD5_BW.js +7570 -0
  124. package/dist/socket-liveness-D3UD5_BW.js.map +1 -0
  125. package/dist/socket-opt-ins.d.ts +64 -0
  126. package/dist/source-DBDbBTL4.js +811 -0
  127. package/dist/source-DBDbBTL4.js.map +1 -0
  128. package/dist/tsx-loader.d.ts +14 -0
  129. package/dist/utils/drizzle-conditions.d.ts +21 -1
  130. package/dist/utils/error-cause.d.ts +7 -0
  131. package/dist/utils/pg-error-utils.d.ts +2 -4
  132. package/dist/utils/result-rows.d.ts +10 -0
  133. package/dist/websocket.d.ts +13 -0
  134. package/package.json +7 -7
  135. package/dist/backup-cli-DW5p9_zv.js.map +0 -1
  136. package/dist/backup-service-EwcDVG-8.js.map +0 -1
  137. package/dist/cli-errors-DsA-K9uP.js +0 -893
  138. package/dist/cli-errors-DsA-K9uP.js.map +0 -1
  139. package/dist/cli-flags-BglvpjHv.js.map +0 -1
  140. package/dist/cli-output-CNdMql-L.js +0 -47
  141. package/dist/cli-output-CNdMql-L.js.map +0 -1
  142. package/dist/collection-index-DdnsxiJ_.js +0 -286
  143. package/dist/collection-index-DdnsxiJ_.js.map +0 -1
  144. package/dist/databasePoolManager-Bj5FbeAs.js.map +0 -1
  145. package/dist/doctor-C-sYWbmt.js.map +0 -1
  146. package/dist/ensure-collection-policies-B1ureSIV.js +0 -160
  147. package/dist/ensure-collection-policies-B1ureSIV.js.map +0 -1
  148. package/dist/ensure-collection-tables-kkkHk8oo.js.map +0 -1
  149. package/dist/ensure-tables-BmI_tRxc.js.map +0 -1
  150. package/dist/generate-drizzle-schema-93M0lUxK.js +0 -123
  151. package/dist/generate-drizzle-schema-93M0lUxK.js.map +0 -1
  152. package/dist/generate-drizzle-schema-logic-sSFDp6LR.js.map +0 -1
  153. package/dist/introspect-db-logic-kCETE8TY.js.map +0 -1
  154. package/dist/introspect-db-queries-C_Q5VgQw.js.map +0 -1
  155. package/dist/plan-schema-DU9exq6C.js.map +0 -1
  156. package/dist/policy-drift-B-J2hhm0.js.map +0 -1
  157. package/dist/render-ddl-Ds2t_d9V.js.map +0 -1
  158. package/dist/rls-bootstrap-sql-_KNnjanK.js.map +0 -1
  159. package/dist/rls-enforcement-CfXOJJaW.js.map +0 -1
  160. package/dist/source-Br7L7GOI.js +0 -410
  161. package/dist/source-Br7L7GOI.js.map +0 -1
  162. package/dist/utils/sql-redaction.d.ts +0 -21
@@ -0,0 +1,541 @@
1
+ import { createRequire as __createRequire } from "module";
2
+ __createRequire(import.meta.url);
3
+ import { s as revokeUserRoleSql } from "./rls-enforcement-DvzqlaL1.js";
4
+ import { r as quoteSqlLiteral } from "./plan-schema-B1YF_tYq.js";
5
+ import { s as planCollectionPolicies } from "./generate-postgres-ddl-logic-B64O2oZF.js";
6
+ import { a as assertSafeIdentifier } from "./deferred-index-build-DP1_SNFo.js";
7
+ import { a as isLockTimeout, c as resolveBootLockOptions, l as sleep, o as isMultiStatementRefusal, r as atomicBatch } from "./boot-ddl-DVpsFN6r.js";
8
+ import { t as describeDriverError } from "./ensure-collection-tables-BhsU6qUE.js";
9
+ import { i as isGeneratedPolicyName } from "./policy-drift-Dm8i-s1J.js";
10
+ import { createHash } from "node:crypto";
11
+ //#region src/schema/ensure-collection-policies.ts
12
+ /**
13
+ * Applying a bundle's RLS policies to a database at boot, idempotently.
14
+ *
15
+ * ## Why this exists
16
+ *
17
+ * {@link ensureCollectionTables} creates the collection *tables* a managed
18
+ * runtime boots against, but a table with row-level security disabled and no
19
+ * policies is not servable: authenticated requests run as the restricted
20
+ * `rebase_user` role, so a read with no `SELECT` policy returns nothing (a
21
+ * public collection answered 401) and a write with no `INSERT`/`UPDATE` policy
22
+ * is denied. The policies live in the collections' `securityRules`; nothing at
23
+ * boot applied them. `rebase db push` does — but it drives Atlas against a
24
+ * local `DATABASE_URL`, and a managed tenant's database is reachable only from
25
+ * inside the cluster, by the runtime that is already connected to it. So the
26
+ * runtime is the only thing that *can* apply them, and this is where it does.
27
+ *
28
+ * ## Why this is safe to run on every boot
29
+ *
30
+ * Every statement is idempotent: `ENABLE ROW LEVEL SECURITY` is only issued for
31
+ * a table the catalogue reports RLS-off (on one that has it, it changes nothing
32
+ * and still queues for an ACCESS EXCLUSIVE lock), and each policy is a `DROP
33
+ * POLICY IF EXISTS` immediately followed by a `CREATE POLICY`, so re-applying
34
+ * asserts exactly the declared state. It adds
35
+ * and replaces; it never drops data. (It does not *reconcile* — a policy a
36
+ * previous push left behind under an old name is not removed here; that stays a
37
+ * `db push` / `db migrate` concern, alongside destructive schema changes.)
38
+ *
39
+ * ## A table whose policies are already the declared ones is left alone
40
+ *
41
+ * Every policy this module writes carries a stamp, `COMMENT ON POLICY … IS
42
+ * 'rebase:<hash>'` (see {@link policyStampSql}). The hash covers the
43
+ * `CREATE POLICY` statement that was rendered *and* the definition Postgres
44
+ * stored for it — command, mode, roles, and the deparsed `USING` / `WITH
45
+ * CHECK` — as read back in the same transaction. A boot recomputes it from the
46
+ * statement it would run now and the definition the catalogue holds now, and
47
+ * re-applies a table only when that differs for any of its policies, a declared
48
+ * policy is missing, a superseded one is still there, or RLS is off.
49
+ *
50
+ * So both halves of "the declared state" still win: a rule whose compiled SQL
51
+ * changed (a new rule under the same name, a compiler change) misses on the
52
+ * first half, and a policy someone altered by hand — `ALTER POLICY`, a
53
+ * different role list — misses on the second, and either is re-applied exactly
54
+ * as every boot used to re-apply everything. A boot that changes nothing runs
55
+ * two catalogue reads here, no DDL and takes no table lock. It used to drop and
56
+ * re-create every policy, one ACCESS EXCLUSIVE lock per table: about 1 s at 200
57
+ * collections, and under a `pg_dump` up to the whole retry budget.
58
+ *
59
+ * A policy without a stamp — written by `db push`, by hand, or by a release
60
+ * that predates stamps — counts as different, so the first boot after either
61
+ * re-applies its table once and stamps it. Nothing else reads the comment:
62
+ * drift checks, `doctor`, rls-check and Studio compare names, roles, commands
63
+ * and clauses, never descriptions.
64
+ *
65
+ * Unlike table creation, a failure here is not fatal: RLS stays enabled, so a
66
+ * table whose policies could not be applied fails **closed** (denies) rather
67
+ * than leaking rows. One collection's policy failing (e.g. a rule that
68
+ * references a table a real migration has not created yet) must not crash-loop
69
+ * the whole deployment and take the other collections' working routes down with
70
+ * it. Failures are reported loudly and per-table so the operator can see
71
+ * exactly which collection is not yet servable and why.
72
+ */
73
+ var isCreatePolicy = (statement) => /^\s*CREATE POLICY/i.test(statement);
74
+ /** The policy names a plan creates. */
75
+ var plannedPolicyNames = (plan) => {
76
+ const expected = /* @__PURE__ */ new Set();
77
+ for (const statement of plan.policyStatements) {
78
+ const match = /^\s*CREATE POLICY\s+"([^"]+)"/i.exec(statement);
79
+ if (match) expected.add(match[1]);
80
+ }
81
+ return expected;
82
+ };
83
+ /**
84
+ * Generated policies on this table that the current plan does not produce.
85
+ *
86
+ * Scoped hard, because dropping a policy is destructive: only this one table,
87
+ * only names matching the generated `<table>_<op>_<sha1>` shape, and only names
88
+ * absent from the plan. A hand-written policy, or one belonging to another
89
+ * table, is never touched — `isGeneratedPolicyName` is the same predicate `db
90
+ * push` uses to draw that line.
91
+ *
92
+ * Computed from the names read before anything is applied. The plan only ever
93
+ * drops and re-creates its own names, so "present before, not planned" is the
94
+ * same set the old read-after-apply found.
95
+ */
96
+ var orphansOf = (plan, present) => {
97
+ const expected = plannedPolicyNames(plan);
98
+ return present.filter((name) => !expected.has(name) && isGeneratedPolicyName(name, plan.table));
99
+ };
100
+ var dropPolicySql = (plan, name) => `DROP POLICY IF EXISTS "${name}" ON "${plan.schema}"."${plan.table}"`;
101
+ /** What every policy stamp starts with. See {@link policyStampSql}. */
102
+ var POLICY_STAMP_PREFIX = "rebase:";
103
+ var sha256Hex = (text) => createHash("sha256").update(text, "utf8").digest("hex");
104
+ /** The same digest, computed by Postgres: hex SHA-256 of the text's UTF-8 bytes. */
105
+ var sha256Sql = (expression) => `encode(sha256(convert_to(${expression}, 'UTF8')), 'hex')`;
106
+ /**
107
+ * The definition Postgres holds for the `pg_policy` row aliased `p`, as one
108
+ * text: command, permissive/restrictive, the role OIDs, and the deparsed
109
+ * `USING` and `WITH CHECK` (`-` for an absent clause, so absent and empty
110
+ * differ). Everything `ALTER POLICY` can change, and everything a `CREATE
111
+ * POLICY` sets.
112
+ *
113
+ * Read back with `pg_get_expr`, whose spelling depends on `search_path` (a
114
+ * name on the path prints unqualified). The stamp is written and checked by
115
+ * the same boot connections, so the two agree; a process with a different
116
+ * path would only disagree, and re-apply — what every boot did before stamps.
117
+ */
118
+ var liveDefinitionSql = (p) => `concat_ws(E'\\n', ${p}.polcmd::text, ${p}.polpermissive::text, ${p}.polroles::text, coalesce(pg_get_expr(${p}.polqual, ${p}.polrelid), '-'), coalesce(pg_get_expr(${p}.polwithcheck, ${p}.polrelid), '-'))`;
119
+ /** The stamp a policy rendered as `renderedHash` carries when its stored definition hashes to `liveHash`. */
120
+ var stampOf = (renderedHash, liveHash) => `${POLICY_STAMP_PREFIX}${sha256Hex(`${renderedHash}:${liveHash}`)}`;
121
+ /** The policies a plan creates, by name, each with the hash of its `CREATE POLICY` statement. */
122
+ var renderedHashes = (plan) => {
123
+ const hashes = /* @__PURE__ */ new Map();
124
+ for (const statement of plan.policyStatements) {
125
+ const match = /^\s*CREATE POLICY\s+"([^"]+)"/i.exec(statement);
126
+ if (match) hashes.set(match[1], sha256Hex(statement.trim()));
127
+ }
128
+ return hashes;
129
+ };
130
+ /**
131
+ * One statement that stamps every policy the plan just created on its table:
132
+ * `COMMENT ON POLICY … IS 'rebase:<hash>'`, the hash taken over the rendered
133
+ * statement's hash and the definition Postgres stored — read back here, after
134
+ * the `CREATE`s, in the same transaction. A `DO` block because the stored
135
+ * definition is only known once the policy exists, and a `COMMENT` takes a
136
+ * literal. One line, and it names only this table.
137
+ *
138
+ * `undefined` for a table the plan gives no policies.
139
+ */
140
+ function policyStampSql(plan) {
141
+ const hashes = renderedHashes(plan);
142
+ if (hashes.size === 0) return void 0;
143
+ const target = `"${plan.schema}"."${plan.table}"`;
144
+ const values = [...hashes].map(([name, hash]) => `(${quoteSqlLiteral(name)}, '${hash}')`).join(", ");
145
+ return `DO \$rebase_policy_stamp\$ DECLARE r record; BEGIN FOR r IN SELECT p.polname AS name, ${`${quoteSqlLiteral(POLICY_STAMP_PREFIX)} || ${sha256Sql(`s.rendered || ':' || ${sha256Sql(liveDefinitionSql("p"))}`)}`} AS stamp FROM pg_policy p JOIN (VALUES ${values}) AS s(name, rendered) ON s.name = p.polname WHERE p.polrelid = ${quoteSqlLiteral(target)}::regclass LOOP EXECUTE 'COMMENT ON POLICY ' || quote_ident(r.name) || ${quoteSqlLiteral(` ON ${target} IS `)} || quote_literal(r.stamp); END LOOP; END \$rebase_policy_stamp\$;`;
146
+ }
147
+ /**
148
+ * Every policy on every table in the declared schemas — name, stamp and a hash
149
+ * of its stored definition — in one read. It replaced a `pg_policies` query
150
+ * per table, and later the name-only read that preceded stamps.
151
+ */
152
+ async function readLivePolicies(client, schemas) {
153
+ const policies = /* @__PURE__ */ new Map();
154
+ if (schemas.length === 0) return policies;
155
+ const inList = schemas.map((schema) => `'${assertSafeIdentifier(schema, "schema name")}'`).join(", ");
156
+ const { rows } = await client.query(`SELECT n.nspname AS schemaname, c.relname AS tablename, p.polname AS policyname,
157
+ obj_description(p.oid, 'pg_policy') AS stamp, ${sha256Sql(liveDefinitionSql("p"))} AS live
158
+ FROM pg_policy p
159
+ JOIN pg_class c ON c.oid = p.polrelid
160
+ JOIN pg_namespace n ON n.oid = c.relnamespace
161
+ WHERE n.nspname IN (${inList})`);
162
+ for (const row of rows) {
163
+ const key = `${row.schemaname}.${row.tablename}`;
164
+ if (!policies.has(key)) policies.set(key, /* @__PURE__ */ new Map());
165
+ policies.get(key).set(row.policyname, {
166
+ stamp: row.stamp ?? null,
167
+ live: row.live ?? ""
168
+ });
169
+ }
170
+ return policies;
171
+ }
172
+ /**
173
+ * Whether every policy the plan declares is on the table, stamped for exactly
174
+ * the statement the plan would run and exactly the definition it holds now.
175
+ */
176
+ var policiesAreCurrent = (plan, present) => {
177
+ for (const [name, renderedHash] of renderedHashes(plan)) {
178
+ const policy = present.get(name);
179
+ if (!policy || policy.stamp !== stampOf(renderedHash, policy.live)) return false;
180
+ }
181
+ return true;
182
+ };
183
+ /**
184
+ * Every declared schema's tables, and whether each has row-level security on.
185
+ *
186
+ * One catalogue read for the whole step. It replaced a full `readExistingSchema`
187
+ * — eight catalogue queries and a row probe of every table, of which only the
188
+ * table names were used — and it carries the one fact that lets a boot leave an
189
+ * already-locked table alone: `relrowsecurity`.
190
+ *
191
+ * Presence is still decided by `information_schema.columns`, as it was: the
192
+ * same relations (views included, which have no RLS and are closed by revoking,
193
+ * exactly as before) and the same privilege filter.
194
+ */
195
+ async function readTableSecurity(client, schemas) {
196
+ const tables = /* @__PURE__ */ new Map();
197
+ if (schemas.length === 0) return tables;
198
+ const inList = schemas.map((schema) => `'${assertSafeIdentifier(schema, "schema name")}'`).join(", ");
199
+ const { rows } = await client.query(`SELECT t.table_schema, t.table_name, c.relrowsecurity AS rls
200
+ FROM (SELECT DISTINCT table_schema, table_name FROM information_schema.columns
201
+ WHERE table_schema IN (${inList})) t
202
+ JOIN pg_namespace n ON n.nspname = t.table_schema
203
+ JOIN pg_class c ON c.relnamespace = n.oid AND c.relname = t.table_name`);
204
+ for (const row of rows) tables.set(`${row.table_schema}.${row.table_name}`, { rls: row.rls === true });
205
+ return tables;
206
+ }
207
+ /**
208
+ * Whether the catalogue says RLS is on for one table, read fresh. `false` when
209
+ * it cannot be read, which sends the caller down the fail-closed path.
210
+ */
211
+ async function rlsIsOn(client, plan) {
212
+ try {
213
+ const { rows } = await client.query(`SELECT c.relrowsecurity AS rls FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
214
+ WHERE n.nspname = ${quoteSqlLiteral(plan.schema)} AND c.relname = ${quoteSqlLiteral(plan.table)}`);
215
+ return rows[0]?.rls === true;
216
+ } catch {
217
+ return false;
218
+ }
219
+ }
220
+ /**
221
+ * Switch RLS on for one table, or — when Postgres refuses — take the user
222
+ * role's privileges on it back, so the table is unreachable rather than
223
+ * unprotected.
224
+ *
225
+ * A table the catalogue already reports RLS-on is left alone. `ENABLE` on it
226
+ * changes nothing, and it is not free: it queues for an `ACCESS EXCLUSIVE` lock,
227
+ * so a boot that met a long reader held every live query on that table behind
228
+ * it — and when the wait ended in a timeout, the catch below revoked the user
229
+ * role's privileges on a table that had been protected all along. Every
230
+ * replica shares the grants, so one boot during a `pg_dump` closed the
231
+ * collection for the whole deployment until the next clean boot.
232
+ */
233
+ async function lockTable(client, plan, rlsOn, run = (sql) => client.query(sql)) {
234
+ if (rlsOn) return { locked: true };
235
+ try {
236
+ await run(plan.enableRls);
237
+ return { locked: true };
238
+ } catch (err) {
239
+ const error = describeDriverError(err);
240
+ if (await rlsIsOn(client, plan)) return { locked: true };
241
+ return withdrawGrant(client, plan, error);
242
+ }
243
+ }
244
+ /**
245
+ * Take the user role's privileges on a table back, so an RLS-off table is
246
+ * unreachable rather than unprotected. `REVOKE` waits for no table lock a
247
+ * reader holds, so it runs as a plain statement.
248
+ */
249
+ async function withdrawGrant(client, plan, error) {
250
+ try {
251
+ await client.query(revokeUserRoleSql(plan.schema, plan.table));
252
+ return {
253
+ locked: false,
254
+ error,
255
+ grantWithdrawn: true
256
+ };
257
+ } catch {
258
+ return {
259
+ locked: false,
260
+ error,
261
+ grantWithdrawn: false
262
+ };
263
+ }
264
+ }
265
+ /**
266
+ * Switch RLS on for every table the collections declare — each collection's
267
+ * and each junction's — and do nothing else.
268
+ *
269
+ * The first thing `rebase db push` does once Atlas has committed. Atlas cannot
270
+ * carry `ENABLE ROW LEVEL SECURITY` (its free tier neither plans nor reads it),
271
+ * so a table it creates arrives with RLS off — while the user role already
272
+ * holds DML on it through the schema's default privileges. Everything push runs
273
+ * after Atlas can refuse: the vector, search and trigger DDL, the policies
274
+ * themselves. Locking first means none of those failures can leave a table
275
+ * open: RLS with no policy denies, which is a state a failed push may leave.
276
+ *
277
+ * A declared table that is not in the database is skipped — nothing can reach
278
+ * a table that does not exist, and the step that creates it locks it.
279
+ */
280
+ async function lockCollectionTables(client, collections) {
281
+ const result = {
282
+ locked: 0,
283
+ unsecured: []
284
+ };
285
+ const plans = planCollectionPolicies(collections);
286
+ if (plans.length === 0) return result;
287
+ const tables = await readTableSecurity(client, Array.from(new Set(plans.map((p) => p.schema))));
288
+ for (const plan of plans) {
289
+ const table = tables.get(plan.qualified);
290
+ if (!table) continue;
291
+ const lock = await lockTable(client, plan, table.rls);
292
+ if (lock.locked) result.locked++;
293
+ else result.unsecured.push({
294
+ table: plan.qualified,
295
+ error: lock.error,
296
+ grantWithdrawn: lock.grantWithdrawn
297
+ });
298
+ }
299
+ return result;
300
+ }
301
+ /**
302
+ * Bring the declared collections' RLS policies up to date. Returns what it did.
303
+ *
304
+ * Only tables that already exist are touched: the boot-time table creator runs
305
+ * first, so anything still missing is a table this additive path did not
306
+ * create (it creates junctions too) — one a failed boot DDL left out, or a
307
+ * project whose schema a migration owns. Enabling RLS on a
308
+ * non-existent table would error, so those are recorded as skipped, not failed.
309
+ *
310
+ * ## One transaction per table
311
+ *
312
+ * Each table's statements — `ENABLE` when RLS is off, every `DROP POLICY IF
313
+ * EXISTS` / `CREATE POLICY` pair, the orphan drops — go to Postgres as one
314
+ * string, which it runs as one transaction (see `boot-ddl`). Before, each was
315
+ * its own autocommit: at 240 tables, 4,080 commits and 4,080 ACCESS EXCLUSIVE
316
+ * lock acquisitions per boot, 6–22 s on a disk that really flushes. And between
317
+ * a policy's DROP and its CREATE, every other replica evaluated the table with
318
+ * that policy missing — a user's continuous reads of their own row came back
319
+ * empty once per boot (measured, Postgres 18). Now a concurrent request sees the
320
+ * old policies or the new ones, never neither.
321
+ *
322
+ * If the transaction fails for any reason but a lock timeout, the table is
323
+ * applied again one statement at a time, exactly as before — so a rule that
324
+ * cannot be created (a function a migration has not added yet) leaves the
325
+ * table in the same deny-leaning partial state it always did, rather than
326
+ * rolling back to the old, possibly looser, policies.
327
+ *
328
+ * ## Lock budget
329
+ *
330
+ * The transaction runs under `lock_timeout` (`boot-ddl`), so a table that a
331
+ * long transaction, a report or a dump is holding costs live traffic one
332
+ * second, not the statement timeout. A contended table is retried after the
333
+ * others, with backoff, within a retry budget for the whole step; one still
334
+ * held after that is reported in `deferred` — its transaction rolled back, so
335
+ * it is exactly as it was before this boot — or, if RLS was off on it, closed
336
+ * by revoking the user role's grant, as any table RLS cannot be enabled on is.
337
+ */
338
+ async function ensureCollectionPolicies(client, collections, log, options = {}) {
339
+ const result = {
340
+ policiesApplied: 0,
341
+ tablesSecured: 0,
342
+ unchanged: 0,
343
+ skipped: [],
344
+ failures: [],
345
+ unsecured: [],
346
+ orphansDropped: 0,
347
+ deferred: []
348
+ };
349
+ const plans = planCollectionPolicies(collections);
350
+ if (plans.length === 0) return result;
351
+ const lock = resolveBootLockOptions(options);
352
+ const schemas = Array.from(new Set(plans.map((p) => p.schema)));
353
+ const tables = await readTableSecurity(client, schemas);
354
+ const livePolicies = await readLivePolicies(client, schemas);
355
+ const work = [];
356
+ for (const plan of plans) {
357
+ const table = tables.get(plan.qualified);
358
+ if (!table) {
359
+ result.skipped.push({
360
+ table: plan.qualified,
361
+ reason: "table is not present in the database; create it with `rebase db push` / `rebase db migrate`"
362
+ });
363
+ continue;
364
+ }
365
+ const live = livePolicies.get(plan.qualified) ?? /* @__PURE__ */ new Map();
366
+ const present = [...live.keys()];
367
+ const orphans = orphansOf(plan, present);
368
+ if (table.rls && orphans.length === 0 && policiesAreCurrent(plan, live)) {
369
+ result.tablesSecured++;
370
+ result.unchanged++;
371
+ continue;
372
+ }
373
+ const presentSet = new Set(present);
374
+ const rulesChanged = orphans.length > 0 || [...plannedPolicyNames(plan)].some((name) => !presentSet.has(name));
375
+ work.push({
376
+ plan,
377
+ rlsOn: table.rls,
378
+ orphans,
379
+ rulesChanged
380
+ });
381
+ }
382
+ if (result.unchanged > 0) log?.(`${result.unchanged} table(s) already have exactly the declared policies; left as they are`);
383
+ let atomic = true;
384
+ let retryBudget = lock.retryBudgetMs;
385
+ let queue = work;
386
+ let lastError = /* @__PURE__ */ new Map();
387
+ for (let attempt = 1; queue.length > 0; attempt++) {
388
+ if (attempt > 1) {
389
+ if (attempt > lock.attempts || retryBudget <= 0) break;
390
+ await sleep(lock.backoffMs(attempt - 1));
391
+ }
392
+ const contended = [];
393
+ const errors = /* @__PURE__ */ new Map();
394
+ for (const table of queue) {
395
+ if (attempt > 1 && retryBudget <= 0) {
396
+ contended.push(table);
397
+ errors.set(table, lastError.get(table) ?? "");
398
+ continue;
399
+ }
400
+ if (atomic) {
401
+ const outcome = await applyAtomically(client, table, lock);
402
+ if (outcome.kind === "applied") {
403
+ record(result, table, log);
404
+ continue;
405
+ }
406
+ if (outcome.kind === "lock-timeout") {
407
+ if (attempt > 1) retryBudget -= lock.lockTimeoutMs;
408
+ contended.push(table);
409
+ errors.set(table, outcome.error);
410
+ continue;
411
+ }
412
+ if (outcome.kind === "unsupported") atomic = false;
413
+ else log?.(`${table.plan.qualified}: applying its policies in one transaction failed (${outcome.error}); applying them one statement at a time`);
414
+ }
415
+ await applyEachStatement(client, table, result, log, atomic ? lock : void 0);
416
+ }
417
+ queue = contended;
418
+ lastError = errors;
419
+ }
420
+ for (const table of queue) await giveUp(client, table, lastError.get(table) ?? "", lock, result);
421
+ return result;
422
+ }
423
+ /**
424
+ * The table's statements, in the order they must run — the stamp last, so it
425
+ * reads back the policies this same transaction created.
426
+ */
427
+ var statementsOf = (table) => {
428
+ const stamp = policyStampSql(table.plan);
429
+ return [
430
+ ...table.rlsOn ? [] : [table.plan.enableRls],
431
+ ...table.plan.policyStatements,
432
+ ...table.orphans.map((name) => dropPolicySql(table.plan, name)),
433
+ ...stamp ? [stamp] : []
434
+ ];
435
+ };
436
+ async function applyAtomically(client, table, lock) {
437
+ const statements = statementsOf(table);
438
+ if (statements.length === 0) return { kind: "applied" };
439
+ try {
440
+ await client.query(atomicBatch(statements, lock.lockTimeoutMs));
441
+ return { kind: "applied" };
442
+ } catch (err) {
443
+ if (isLockTimeout(err)) return {
444
+ kind: "lock-timeout",
445
+ error: describeDriverError(err)
446
+ };
447
+ if (isMultiStatementRefusal(err)) return { kind: "unsupported" };
448
+ return {
449
+ kind: "failed",
450
+ error: describeDriverError(err)
451
+ };
452
+ }
453
+ }
454
+ function record(result, table, log) {
455
+ const created = table.plan.policyStatements.filter(isCreatePolicy).length;
456
+ result.tablesSecured++;
457
+ result.policiesApplied += created;
458
+ result.orphansDropped += table.orphans.length;
459
+ log?.(`${table.plan.qualified}: RLS enabled, ${created} policy(ies) applied` + (table.orphans.length > 0 ? `, ${table.orphans.length} orphan(s) dropped` : ""));
460
+ }
461
+ /**
462
+ * The table applied one statement at a time — the path every table took before
463
+ * the per-table transaction, kept as its fallback so a failure ends exactly
464
+ * where it always did.
465
+ *
466
+ * Each statement still carries the lock budget when the handle can take it.
467
+ */
468
+ async function applyEachStatement(client, table, result, log, lock) {
469
+ const { plan } = table;
470
+ const run = (sql) => client.query(lock ? atomicBatch([sql], lock.lockTimeoutMs) : sql);
471
+ const locked = await lockTable(client, plan, table.rlsOn, run);
472
+ if (!locked.locked) {
473
+ result.unsecured.push({
474
+ table: plan.qualified,
475
+ error: locked.error,
476
+ grantWithdrawn: locked.grantWithdrawn
477
+ });
478
+ return;
479
+ }
480
+ result.tablesSecured++;
481
+ try {
482
+ let created = 0;
483
+ for (const statement of plan.policyStatements) {
484
+ await run(statement);
485
+ if (isCreatePolicy(statement)) {
486
+ result.policiesApplied++;
487
+ created++;
488
+ }
489
+ }
490
+ for (const name of table.orphans) {
491
+ await run(dropPolicySql(plan, name));
492
+ result.orphansDropped++;
493
+ }
494
+ log?.(`${plan.qualified}: RLS enabled, ${created} policy(ies) applied` + (table.orphans.length > 0 ? `, ${table.orphans.length} orphan(s) dropped` : ""));
495
+ } catch (err) {
496
+ result.failures.push({
497
+ table: plan.qualified,
498
+ error: describeDriverError(err)
499
+ });
500
+ return;
501
+ }
502
+ const stamp = policyStampSql(plan);
503
+ if (!stamp) return;
504
+ try {
505
+ await run(stamp);
506
+ } catch (err) {
507
+ log?.(`${plan.qualified}: policies applied, but not stamped (${describeDriverError(err)}); the next boot applies them again`);
508
+ }
509
+ }
510
+ /**
511
+ * A table whose lock was held for every attempt. Its transaction rolled back
512
+ * each time, so it is exactly as it was before this boot.
513
+ *
514
+ * With RLS on, that is a safe state to leave it in — reported, not failed. With
515
+ * RLS off it is an open table over the schema-wide grant, so it is closed the
516
+ * way any table RLS cannot be enabled on is: the catalogue is asked once more,
517
+ * and only a table that really is RLS-off has the user role's grant withdrawn.
518
+ */
519
+ async function giveUp(client, table, lastError, lock, result) {
520
+ const { plan } = table;
521
+ const error = `another session held the table's lock for every attempt (lock_timeout ${lock.lockTimeoutMs} ms${lastError ? `: ${lastError.split("\n")[0]}` : ""}) — a long transaction, a report or a dump`;
522
+ if (table.rlsOn || await rlsIsOn(client, plan)) {
523
+ result.tablesSecured++;
524
+ result.deferred.push({
525
+ table: plan.qualified,
526
+ error,
527
+ rulesChanged: table.rulesChanged
528
+ });
529
+ return;
530
+ }
531
+ const closed = await withdrawGrant(client, plan, error);
532
+ if (!closed.locked) result.unsecured.push({
533
+ table: plan.qualified,
534
+ error: closed.error,
535
+ grantWithdrawn: closed.grantWithdrawn
536
+ });
537
+ }
538
+ //#endregion
539
+ export { ensureCollectionPolicies, lockCollectionTables, policyStampSql };
540
+
541
+ //# sourceMappingURL=ensure-collection-policies-CXpbeNag.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ensure-collection-policies-CXpbeNag.js","names":[],"sources":["../src/schema/ensure-collection-policies.ts"],"sourcesContent":["/**\n * Applying a bundle's RLS policies to a database at boot, idempotently.\n *\n * ## Why this exists\n *\n * {@link ensureCollectionTables} creates the collection *tables* a managed\n * runtime boots against, but a table with row-level security disabled and no\n * policies is not servable: authenticated requests run as the restricted\n * `rebase_user` role, so a read with no `SELECT` policy returns nothing (a\n * public collection answered 401) and a write with no `INSERT`/`UPDATE` policy\n * is denied. The policies live in the collections' `securityRules`; nothing at\n * boot applied them. `rebase db push` does — but it drives Atlas against a\n * local `DATABASE_URL`, and a managed tenant's database is reachable only from\n * inside the cluster, by the runtime that is already connected to it. So the\n * runtime is the only thing that *can* apply them, and this is where it does.\n *\n * ## Why this is safe to run on every boot\n *\n * Every statement is idempotent: `ENABLE ROW LEVEL SECURITY` is only issued for\n * a table the catalogue reports RLS-off (on one that has it, it changes nothing\n * and still queues for an ACCESS EXCLUSIVE lock), and each policy is a `DROP\n * POLICY IF EXISTS` immediately followed by a `CREATE POLICY`, so re-applying\n * asserts exactly the declared state. It adds\n * and replaces; it never drops data. (It does not *reconcile* — a policy a\n * previous push left behind under an old name is not removed here; that stays a\n * `db push` / `db migrate` concern, alongside destructive schema changes.)\n *\n * ## A table whose policies are already the declared ones is left alone\n *\n * Every policy this module writes carries a stamp, `COMMENT ON POLICY … IS\n * 'rebase:<hash>'` (see {@link policyStampSql}). The hash covers the\n * `CREATE POLICY` statement that was rendered *and* the definition Postgres\n * stored for it — command, mode, roles, and the deparsed `USING` / `WITH\n * CHECK` — as read back in the same transaction. A boot recomputes it from the\n * statement it would run now and the definition the catalogue holds now, and\n * re-applies a table only when that differs for any of its policies, a declared\n * policy is missing, a superseded one is still there, or RLS is off.\n *\n * So both halves of \"the declared state\" still win: a rule whose compiled SQL\n * changed (a new rule under the same name, a compiler change) misses on the\n * first half, and a policy someone altered by hand — `ALTER POLICY`, a\n * different role list — misses on the second, and either is re-applied exactly\n * as every boot used to re-apply everything. A boot that changes nothing runs\n * two catalogue reads here, no DDL and takes no table lock. It used to drop and\n * re-create every policy, one ACCESS EXCLUSIVE lock per table: about 1 s at 200\n * collections, and under a `pg_dump` up to the whole retry budget.\n *\n * A policy without a stamp — written by `db push`, by hand, or by a release\n * that predates stamps — counts as different, so the first boot after either\n * re-applies its table once and stamps it. Nothing else reads the comment:\n * drift checks, `doctor`, rls-check and Studio compare names, roles, commands\n * and clauses, never descriptions.\n *\n * Unlike table creation, a failure here is not fatal: RLS stays enabled, so a\n * table whose policies could not be applied fails **closed** (denies) rather\n * than leaking rows. One collection's policy failing (e.g. a rule that\n * references a table a real migration has not created yet) must not crash-loop\n * the whole deployment and take the other collections' working routes down with\n * it. Failures are reported loudly and per-table so the operator can see\n * exactly which collection is not yet servable and why.\n */\nimport { createHash } from \"node:crypto\";\nimport { type CollectionConfig } from \"@rebasepro/types\";\nimport { planCollectionPolicies, type CollectionPolicyPlan } from \"./generate-postgres-ddl-logic\";\nimport { isGeneratedPolicyName } from \"../security/policy-drift\";\nimport { describeDriverError, type Queryable } from \"./ensure-collection-tables\";\nimport { assertSafeIdentifier } from \"./plan/diff-plan\";\nimport { quoteSqlLiteral } from \"./generate-postgres-ddl-logic\";\nimport { revokeUserRoleSql } from \"../security/rls-enforcement\";\nimport {\n atomicBatch,\n isLockTimeout,\n isMultiStatementRefusal,\n resolveBootLockOptions,\n sleep,\n type BootLockOptions,\n type ResolvedBootLockOptions\n} from \"./boot-ddl\";\n\nexport interface PolicyEnsureResult {\n /** `CREATE POLICY` statements that ran successfully. */\n policiesApplied: number;\n /** Tables that had RLS enabled — including the {@link unchanged} ones, which already had it. */\n tablesSecured: number;\n /**\n * Tables whose policies were already exactly the declared ones, RLS on, by\n * the stamp each policy carries — left alone: no statement, no lock.\n */\n unchanged: number;\n /** Declared tables absent from the database — left to a real migration. */\n skipped: { table: string; reason: string }[];\n /**\n * Tables that have RLS on but did not get every policy. They deny — RLS\n * with no matching policy is deny-all — so they are safe but not servable.\n */\n failures: { table: string; error: string }[];\n /**\n * Tables RLS could not be enabled on, whose DML grant was withdrawn instead.\n *\n * This state had no name, and that was the bug: `ENABLE ROW LEVEL SECURITY`\n * failing was recorded as a `failure` and reported with the same \"it stays\n * locked (denies)\" wording as a failed policy — but the two are opposites.\n * A policy statement failing leaves RLS on and the table denying. `enableRls`\n * failing leaves RLS *off*, and the schema-wide grant to the user role has\n * already been made by `ensureRlsEnforcement`, so the table is readable and\n * writable by every authenticated request with no row filtering at all.\n */\n unsecured: { table: string; error: string; grantWithdrawn: boolean }[];\n /**\n * Generated policies removed because no current rule produces them.\n *\n * A policy's name embeds a hash of the rule's semantics, so editing a rule\n * does not update a policy — it creates a new one and abandons the old.\n * Postgres ORs permissive policies, so the abandoned one keeps granting:\n * a `USING (true)` tightened to an owner check went on admitting everyone,\n * forever, while the deploy logged success.\n *\n * `db push` reconciles this, and cannot reach a managed tenant's in-cluster\n * database — which is the reason this module exists. So boot has to do it.\n */\n orphansDropped: number;\n /**\n * Tables whose policies were not re-applied because another session held\n * the table's lock for every attempt — a long transaction, a report, a\n * dump. Each table's policy statements run in one transaction, so a table\n * here was left exactly as it was: RLS on, its existing policies in force.\n * `rulesChanged` says whether those differ from the declared ones by name\n * (a rule whose policy is missing, or an orphan still granting), in which\n * case the new rules are not in force on this table until a later boot.\n */\n deferred: { table: string; error: string; rulesChanged: boolean }[];\n}\n\nconst isCreatePolicy = (statement: string): boolean => /^\\s*CREATE POLICY/i.test(statement);\n\n/** The policy names a plan creates. */\nconst plannedPolicyNames = (plan: CollectionPolicyPlan): Set<string> => {\n const expected = new Set<string>();\n for (const statement of plan.policyStatements) {\n const match = /^\\s*CREATE POLICY\\s+\"([^\"]+)\"/i.exec(statement);\n if (match) expected.add(match[1]);\n }\n return expected;\n};\n\n/**\n * Generated policies on this table that the current plan does not produce.\n *\n * Scoped hard, because dropping a policy is destructive: only this one table,\n * only names matching the generated `<table>_<op>_<sha1>` shape, and only names\n * absent from the plan. A hand-written policy, or one belonging to another\n * table, is never touched — `isGeneratedPolicyName` is the same predicate `db\n * push` uses to draw that line.\n *\n * Computed from the names read before anything is applied. The plan only ever\n * drops and re-creates its own names, so \"present before, not planned\" is the\n * same set the old read-after-apply found.\n */\nconst orphansOf = (plan: CollectionPolicyPlan, present: readonly string[]): string[] => {\n const expected = plannedPolicyNames(plan);\n return present.filter(name => !expected.has(name) && isGeneratedPolicyName(name, plan.table));\n};\n\nconst dropPolicySql = (plan: CollectionPolicyPlan, name: string): string =>\n `DROP POLICY IF EXISTS \"${name}\" ON \"${plan.schema}\".\"${plan.table}\"`;\n\n/** What every policy stamp starts with. See {@link policyStampSql}. */\nexport const POLICY_STAMP_PREFIX = \"rebase:\";\n\nconst sha256Hex = (text: string): string => createHash(\"sha256\").update(text, \"utf8\").digest(\"hex\");\n\n/** The same digest, computed by Postgres: hex SHA-256 of the text's UTF-8 bytes. */\nconst sha256Sql = (expression: string): string => `encode(sha256(convert_to(${expression}, 'UTF8')), 'hex')`;\n\n/**\n * The definition Postgres holds for the `pg_policy` row aliased `p`, as one\n * text: command, permissive/restrictive, the role OIDs, and the deparsed\n * `USING` and `WITH CHECK` (`-` for an absent clause, so absent and empty\n * differ). Everything `ALTER POLICY` can change, and everything a `CREATE\n * POLICY` sets.\n *\n * Read back with `pg_get_expr`, whose spelling depends on `search_path` (a\n * name on the path prints unqualified). The stamp is written and checked by\n * the same boot connections, so the two agree; a process with a different\n * path would only disagree, and re-apply — what every boot did before stamps.\n */\nconst liveDefinitionSql = (p: string): string =>\n `concat_ws(E'\\\\n', ${p}.polcmd::text, ${p}.polpermissive::text, ${p}.polroles::text, ` +\n `coalesce(pg_get_expr(${p}.polqual, ${p}.polrelid), '-'), ` +\n `coalesce(pg_get_expr(${p}.polwithcheck, ${p}.polrelid), '-'))`;\n\n/** The stamp a policy rendered as `renderedHash` carries when its stored definition hashes to `liveHash`. */\nconst stampOf = (renderedHash: string, liveHash: string): string =>\n `${POLICY_STAMP_PREFIX}${sha256Hex(`${renderedHash}:${liveHash}`)}`;\n\n/** The policies a plan creates, by name, each with the hash of its `CREATE POLICY` statement. */\nconst renderedHashes = (plan: CollectionPolicyPlan): Map<string, string> => {\n const hashes = new Map<string, string>();\n for (const statement of plan.policyStatements) {\n const match = /^\\s*CREATE POLICY\\s+\"([^\"]+)\"/i.exec(statement);\n if (match) hashes.set(match[1], sha256Hex(statement.trim()));\n }\n return hashes;\n};\n\n/**\n * One statement that stamps every policy the plan just created on its table:\n * `COMMENT ON POLICY … IS 'rebase:<hash>'`, the hash taken over the rendered\n * statement's hash and the definition Postgres stored — read back here, after\n * the `CREATE`s, in the same transaction. A `DO` block because the stored\n * definition is only known once the policy exists, and a `COMMENT` takes a\n * literal. One line, and it names only this table.\n *\n * `undefined` for a table the plan gives no policies.\n */\nexport function policyStampSql(plan: CollectionPolicyPlan): string | undefined {\n const hashes = renderedHashes(plan);\n if (hashes.size === 0) return undefined;\n const target = `\"${plan.schema}\".\"${plan.table}\"`;\n const values = [...hashes].map(([name, hash]) => `(${quoteSqlLiteral(name)}, '${hash}')`).join(\", \");\n const stamp = `${quoteSqlLiteral(POLICY_STAMP_PREFIX)} || ${sha256Sql(`s.rendered || ':' || ${sha256Sql(liveDefinitionSql(\"p\"))}`)}`;\n return \"DO $rebase_policy_stamp$ DECLARE r record; BEGIN \" +\n `FOR r IN SELECT p.polname AS name, ${stamp} AS stamp FROM pg_policy p ` +\n `JOIN (VALUES ${values}) AS s(name, rendered) ON s.name = p.polname ` +\n `WHERE p.polrelid = ${quoteSqlLiteral(target)}::regclass LOOP ` +\n `EXECUTE 'COMMENT ON POLICY ' || quote_ident(r.name) || ${quoteSqlLiteral(` ON ${target} IS `)} || quote_literal(r.stamp); ` +\n \"END LOOP; END $rebase_policy_stamp$;\";\n}\n\n/** One policy as the catalogue has it. */\ninterface LivePolicy {\n /** Its comment — a stamp, when this module wrote it. */\n stamp: string | null;\n /** Hex SHA-256 of {@link liveDefinitionSql}. */\n live: string;\n}\n\n/**\n * Every policy on every table in the declared schemas — name, stamp and a hash\n * of its stored definition — in one read. It replaced a `pg_policies` query\n * per table, and later the name-only read that preceded stamps.\n */\nasync function readLivePolicies(client: Queryable, schemas: string[]): Promise<Map<string, Map<string, LivePolicy>>> {\n const policies = new Map<string, Map<string, LivePolicy>>();\n if (schemas.length === 0) return policies;\n const inList = schemas.map(schema => `'${assertSafeIdentifier(schema, \"schema name\")}'`).join(\", \");\n const { rows } = await client.query<{\n schemaname: string; tablename: string; policyname: string; stamp?: string | null; live?: string | null;\n }>(\n `SELECT n.nspname AS schemaname, c.relname AS tablename, p.polname AS policyname,\n obj_description(p.oid, 'pg_policy') AS stamp, ${sha256Sql(liveDefinitionSql(\"p\"))} AS live\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 n.nspname IN (${inList})`\n );\n for (const row of rows) {\n const key = `${row.schemaname}.${row.tablename}`;\n if (!policies.has(key)) policies.set(key, new Map());\n policies.get(key)!.set(row.policyname, { stamp: row.stamp ?? null, live: row.live ?? \"\" });\n }\n return policies;\n}\n\n/**\n * Whether every policy the plan declares is on the table, stamped for exactly\n * the statement the plan would run and exactly the definition it holds now.\n */\nconst policiesAreCurrent = (plan: CollectionPolicyPlan, present: Map<string, LivePolicy>): boolean => {\n for (const [name, renderedHash] of renderedHashes(plan)) {\n const policy = present.get(name);\n if (!policy || policy.stamp !== stampOf(renderedHash, policy.live)) return false;\n }\n return true;\n};\n\n/**\n * Every declared schema's tables, and whether each has row-level security on.\n *\n * One catalogue read for the whole step. It replaced a full `readExistingSchema`\n * — eight catalogue queries and a row probe of every table, of which only the\n * table names were used — and it carries the one fact that lets a boot leave an\n * already-locked table alone: `relrowsecurity`.\n *\n * Presence is still decided by `information_schema.columns`, as it was: the\n * same relations (views included, which have no RLS and are closed by revoking,\n * exactly as before) and the same privilege filter.\n */\nasync function readTableSecurity(client: Queryable, schemas: string[]): Promise<Map<string, { rls: boolean }>> {\n const tables = new Map<string, { rls: boolean }>();\n if (schemas.length === 0) return tables;\n const inList = schemas.map(schema => `'${assertSafeIdentifier(schema, \"schema name\")}'`).join(\", \");\n const { rows } = await client.query<{ table_schema: string; table_name: string; rls: boolean | null }>(\n `SELECT t.table_schema, t.table_name, c.relrowsecurity AS rls\n FROM (SELECT DISTINCT table_schema, table_name FROM information_schema.columns\n WHERE table_schema IN (${inList})) t\n JOIN pg_namespace n ON n.nspname = t.table_schema\n JOIN pg_class c ON c.relnamespace = n.oid AND c.relname = t.table_name`\n );\n for (const row of rows) tables.set(`${row.table_schema}.${row.table_name}`, { rls: row.rls === true });\n return tables;\n}\n\n/**\n * Whether the catalogue says RLS is on for one table, read fresh. `false` when\n * it cannot be read, which sends the caller down the fail-closed path.\n */\nasync function rlsIsOn(client: Queryable, plan: CollectionPolicyPlan): Promise<boolean> {\n try {\n const { rows } = await client.query<{ rls: boolean }>(\n `SELECT c.relrowsecurity AS rls FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE n.nspname = ${quoteSqlLiteral(plan.schema)} AND c.relname = ${quoteSqlLiteral(plan.table)}`\n );\n return rows[0]?.rls === true;\n } catch {\n return false;\n }\n}\n\n/** What one table's `ENABLE ROW LEVEL SECURITY` came to. */\ntype TableLock =\n | { locked: true }\n | { locked: false; error: string; grantWithdrawn: boolean };\n\n/**\n * Switch RLS on for one table, or — when Postgres refuses — take the user\n * role's privileges on it back, so the table is unreachable rather than\n * unprotected.\n *\n * A table the catalogue already reports RLS-on is left alone. `ENABLE` on it\n * changes nothing, and it is not free: it queues for an `ACCESS EXCLUSIVE` lock,\n * so a boot that met a long reader held every live query on that table behind\n * it — and when the wait ended in a timeout, the catch below revoked the user\n * role's privileges on a table that had been protected all along. Every\n * replica shares the grants, so one boot during a `pg_dump` closed the\n * collection for the whole deployment until the next clean boot.\n */\nasync function lockTable(\n client: Queryable,\n plan: CollectionPolicyPlan,\n rlsOn: boolean,\n run: (sql: string) => Promise<unknown> = sql => client.query(sql)\n): Promise<TableLock> {\n if (rlsOn) return { locked: true };\n try {\n await run(plan.enableRls);\n return { locked: true };\n } catch (err) {\n // The database's own answer, not the ORM's wrapper — see\n // `describeDriverError`. This is the most consequential message in\n // the file: it is what the operator reads about a table that is now\n // denying every request, and `Failed query: <the statement>` told\n // them nothing they did not already have.\n const error = describeDriverError(err);\n // A failed statement says nothing about the state it meant to reach: a\n // lock or statement timeout, a cancelled query, a peer that enabled it\n // first. Ask the catalogue. Only a table that really is RLS-off is\n // revoked — that safety is unchanged; a read that fails counts as off.\n if (await rlsIsOn(client, plan)) return { locked: true };\n // Take the privilege back rather than serve an unprotected table.\n // This is the fail-closed step the old code assumed it already had:\n // per-table, so one collection cannot take the rest of the\n // deployment down, but leaving nothing readable without RLS.\n //\n // Quoted, like every other statement here: unquoted, Postgres folds\n // the name to lower case, so a mixed-case adopted table (`\"User\"`)\n // was revoked as `public.user` — which fails, and a table that could\n // have been closed refused the boot instead.\n return withdrawGrant(client, plan, error);\n }\n}\n\n/**\n * Take the user role's privileges on a table back, so an RLS-off table is\n * unreachable rather than unprotected. `REVOKE` waits for no table lock a\n * reader holds, so it runs as a plain statement.\n */\nasync function withdrawGrant(client: Queryable, plan: CollectionPolicyPlan, error: string): Promise<TableLock> {\n try {\n await client.query(revokeUserRoleSql(plan.schema, plan.table));\n return { locked: false, error, grantWithdrawn: true };\n } catch {\n // Reported with `grantWithdrawn: false`, which the caller escalates.\n // There is nothing else this can do to make the table safe.\n return { locked: false, error, grantWithdrawn: false };\n }\n}\n\nexport interface TableLockResult {\n /** Tables RLS is now on for. */\n locked: number;\n /** Tables it could not be switched on for. See {@link PolicyEnsureResult.unsecured}. */\n unsecured: PolicyEnsureResult[\"unsecured\"];\n}\n\n/**\n * Switch RLS on for every table the collections declare — each collection's\n * and each junction's — and do nothing else.\n *\n * The first thing `rebase db push` does once Atlas has committed. Atlas cannot\n * carry `ENABLE ROW LEVEL SECURITY` (its free tier neither plans nor reads it),\n * so a table it creates arrives with RLS off — while the user role already\n * holds DML on it through the schema's default privileges. Everything push runs\n * after Atlas can refuse: the vector, search and trigger DDL, the policies\n * themselves. Locking first means none of those failures can leave a table\n * open: RLS with no policy denies, which is a state a failed push may leave.\n *\n * A declared table that is not in the database is skipped — nothing can reach\n * a table that does not exist, and the step that creates it locks it.\n */\nexport async function lockCollectionTables(\n client: Queryable,\n collections: CollectionConfig[]\n): Promise<TableLockResult> {\n const result: TableLockResult = { locked: 0, unsecured: [] };\n const plans = planCollectionPolicies(collections);\n if (plans.length === 0) return result;\n\n const tables = await readTableSecurity(client, Array.from(new Set(plans.map(p => p.schema))));\n for (const plan of plans) {\n const table = tables.get(plan.qualified);\n if (!table) continue;\n const lock = await lockTable(client, plan, table.rls);\n if (lock.locked) {\n result.locked++;\n } else {\n result.unsecured.push({ table: plan.qualified, error: lock.error, grantWithdrawn: lock.grantWithdrawn });\n }\n }\n return result;\n}\n\n/** One declared table that exists, and what the catalogue said about it. */\ninterface TableWork {\n plan: CollectionPolicyPlan;\n rlsOn: boolean;\n orphans: string[];\n /** Whether the policy names on the table differ from the plan's. */\n rulesChanged: boolean;\n}\n\ntype AtomicOutcome =\n | { kind: \"applied\" }\n | { kind: \"lock-timeout\"; error: string }\n | { kind: \"unsupported\" }\n | { kind: \"failed\"; error: string };\n\n/**\n * Bring the declared collections' RLS policies up to date. Returns what it did.\n *\n * Only tables that already exist are touched: the boot-time table creator runs\n * first, so anything still missing is a table this additive path did not\n * create (it creates junctions too) — one a failed boot DDL left out, or a\n * project whose schema a migration owns. Enabling RLS on a\n * non-existent table would error, so those are recorded as skipped, not failed.\n *\n * ## One transaction per table\n *\n * Each table's statements — `ENABLE` when RLS is off, every `DROP POLICY IF\n * EXISTS` / `CREATE POLICY` pair, the orphan drops — go to Postgres as one\n * string, which it runs as one transaction (see `boot-ddl`). Before, each was\n * its own autocommit: at 240 tables, 4,080 commits and 4,080 ACCESS EXCLUSIVE\n * lock acquisitions per boot, 6–22 s on a disk that really flushes. And between\n * a policy's DROP and its CREATE, every other replica evaluated the table with\n * that policy missing — a user's continuous reads of their own row came back\n * empty once per boot (measured, Postgres 18). Now a concurrent request sees the\n * old policies or the new ones, never neither.\n *\n * If the transaction fails for any reason but a lock timeout, the table is\n * applied again one statement at a time, exactly as before — so a rule that\n * cannot be created (a function a migration has not added yet) leaves the\n * table in the same deny-leaning partial state it always did, rather than\n * rolling back to the old, possibly looser, policies.\n *\n * ## Lock budget\n *\n * The transaction runs under `lock_timeout` (`boot-ddl`), so a table that a\n * long transaction, a report or a dump is holding costs live traffic one\n * second, not the statement timeout. A contended table is retried after the\n * others, with backoff, within a retry budget for the whole step; one still\n * held after that is reported in `deferred` — its transaction rolled back, so\n * it is exactly as it was before this boot — or, if RLS was off on it, closed\n * by revoking the user role's grant, as any table RLS cannot be enabled on is.\n */\nexport async function ensureCollectionPolicies(\n client: Queryable,\n collections: CollectionConfig[],\n log?: (message: string) => void,\n options: BootLockOptions = {}\n): Promise<PolicyEnsureResult> {\n const result: PolicyEnsureResult = {\n policiesApplied: 0, tablesSecured: 0, unchanged: 0, skipped: [], failures: [], unsecured: [],\n orphansDropped: 0, deferred: []\n };\n\n const plans = planCollectionPolicies(collections);\n if (plans.length === 0) return result;\n const lock = resolveBootLockOptions(options);\n\n const schemas = Array.from(new Set(plans.map(p => p.schema)));\n const tables = await readTableSecurity(client, schemas);\n const livePolicies = await readLivePolicies(client, schemas);\n\n const work: TableWork[] = [];\n for (const plan of plans) {\n const table = tables.get(plan.qualified);\n if (!table) {\n result.skipped.push({\n table: plan.qualified,\n reason: \"table is not present in the database; create it with `rebase db push` / `rebase db migrate`\"\n });\n continue;\n }\n const live = livePolicies.get(plan.qualified) ?? new Map<string, LivePolicy>();\n const present = [...live.keys()];\n const orphans = orphansOf(plan, present);\n // Already exactly what the plan would leave: nothing to run, no lock to take.\n if (table.rls && orphans.length === 0 && policiesAreCurrent(plan, live)) {\n result.tablesSecured++;\n result.unchanged++;\n continue;\n }\n const presentSet = new Set(present);\n const rulesChanged = orphans.length > 0 || [...plannedPolicyNames(plan)].some(name => !presentSet.has(name));\n work.push({ plan, rlsOn: table.rls, orphans, rulesChanged });\n }\n if (result.unchanged > 0) {\n log?.(`${result.unchanged} table(s) already have exactly the declared policies; left as they are`);\n }\n\n // Until the handle refuses a multi-statement string (an extended-protocol\n // driver), after which every table goes one statement at a time.\n let atomic = true;\n let retryBudget = lock.retryBudgetMs;\n let queue = work;\n let lastError = new Map<TableWork, string>();\n\n for (let attempt = 1; queue.length > 0; attempt++) {\n if (attempt > 1) {\n if (attempt > lock.attempts || retryBudget <= 0) break;\n await sleep(lock.backoffMs(attempt - 1));\n }\n const contended: TableWork[] = [];\n const errors = new Map<TableWork, string>();\n for (const table of queue) {\n if (attempt > 1 && retryBudget <= 0) {\n contended.push(table);\n errors.set(table, lastError.get(table) ?? \"\");\n continue;\n }\n if (atomic) {\n const outcome = await applyAtomically(client, table, lock);\n if (outcome.kind === \"applied\") {\n record(result, table, log);\n continue;\n }\n if (outcome.kind === \"lock-timeout\") {\n if (attempt > 1) retryBudget -= lock.lockTimeoutMs;\n contended.push(table);\n errors.set(table, outcome.error);\n continue;\n }\n if (outcome.kind === \"unsupported\") {\n atomic = false;\n } else {\n log?.(`${table.plan.qualified}: applying its policies in one transaction failed ` +\n `(${outcome.error}); applying them one statement at a time`);\n }\n }\n await applyEachStatement(client, table, result, log, atomic ? lock : undefined);\n }\n queue = contended;\n lastError = errors;\n }\n\n // Still contended after every attempt the budget allowed.\n for (const table of queue) {\n await giveUp(client, table, lastError.get(table) ?? \"\", lock, result);\n }\n\n return result;\n}\n\n/**\n * The table's statements, in the order they must run — the stamp last, so it\n * reads back the policies this same transaction created.\n */\nconst statementsOf = (table: TableWork): string[] => {\n const stamp = policyStampSql(table.plan);\n return [\n ...(table.rlsOn ? [] : [table.plan.enableRls]),\n ...table.plan.policyStatements,\n ...table.orphans.map(name => dropPolicySql(table.plan, name)),\n ...(stamp ? [stamp] : [])\n ];\n};\n\nasync function applyAtomically(\n client: Queryable,\n table: TableWork,\n lock: ResolvedBootLockOptions\n): Promise<AtomicOutcome> {\n const statements = statementsOf(table);\n if (statements.length === 0) return { kind: \"applied\" };\n try {\n await client.query(atomicBatch(statements, lock.lockTimeoutMs));\n return { kind: \"applied\" };\n } catch (err) {\n if (isLockTimeout(err)) return { kind: \"lock-timeout\", error: describeDriverError(err) };\n if (isMultiStatementRefusal(err)) return { kind: \"unsupported\" };\n return { kind: \"failed\", error: describeDriverError(err) };\n }\n}\n\nfunction record(result: PolicyEnsureResult, table: TableWork, log?: (message: string) => void): void {\n const created = table.plan.policyStatements.filter(isCreatePolicy).length;\n result.tablesSecured++;\n result.policiesApplied += created;\n result.orphansDropped += table.orphans.length;\n log?.(`${table.plan.qualified}: RLS enabled, ${created} policy(ies) applied` +\n (table.orphans.length > 0 ? `, ${table.orphans.length} orphan(s) dropped` : \"\"));\n}\n\n/**\n * The table applied one statement at a time — the path every table took before\n * the per-table transaction, kept as its fallback so a failure ends exactly\n * where it always did.\n *\n * Each statement still carries the lock budget when the handle can take it.\n */\nasync function applyEachStatement(\n client: Queryable,\n table: TableWork,\n result: PolicyEnsureResult,\n log: ((message: string) => void) | undefined,\n lock: ResolvedBootLockOptions | undefined\n): Promise<void> {\n const { plan } = table;\n const run = (sql: string) => client.query(lock ? atomicBatch([sql], lock.lockTimeoutMs) : sql);\n\n // Enabling RLS is its own step, because its failure is the opposite of\n // every other failure here. Once RLS is on, anything that goes wrong\n // afterwards leaves the table denying; while it is off, the grant made\n // earlier in boot leaves the table wide open. Sharing one `try` meant\n // the dangerous case was reported in the safe case's words.\n const locked = await lockTable(client, plan, table.rlsOn, run);\n if (!locked.locked) {\n result.unsecured.push({ table: plan.qualified, error: locked.error, grantWithdrawn: locked.grantWithdrawn });\n return;\n }\n result.tablesSecured++;\n\n try {\n let created = 0;\n for (const statement of plan.policyStatements) {\n await run(statement);\n if (isCreatePolicy(statement)) {\n result.policiesApplied++;\n created++;\n }\n }\n for (const name of table.orphans) {\n await run(dropPolicySql(plan, name));\n result.orphansDropped++;\n }\n log?.(`${plan.qualified}: RLS enabled, ${created} policy(ies) applied` +\n (table.orphans.length > 0 ? `, ${table.orphans.length} orphan(s) dropped` : \"\"));\n } catch (err) {\n result.failures.push({\n table: plan.qualified,\n error: describeDriverError(err)\n });\n return;\n }\n\n // Only once every policy is in. A stamp that cannot be written costs the\n // next boot a re-apply of this table — what every boot did before stamps —\n // so it is reported, not failed.\n const stamp = policyStampSql(plan);\n if (!stamp) return;\n try {\n await run(stamp);\n } catch (err) {\n log?.(`${plan.qualified}: policies applied, but not stamped (${describeDriverError(err)}); ` +\n \"the next boot applies them again\");\n }\n}\n\n/**\n * A table whose lock was held for every attempt. Its transaction rolled back\n * each time, so it is exactly as it was before this boot.\n *\n * With RLS on, that is a safe state to leave it in — reported, not failed. With\n * RLS off it is an open table over the schema-wide grant, so it is closed the\n * way any table RLS cannot be enabled on is: the catalogue is asked once more,\n * and only a table that really is RLS-off has the user role's grant withdrawn.\n */\nasync function giveUp(\n client: Queryable,\n table: TableWork,\n lastError: string,\n lock: ResolvedBootLockOptions,\n result: PolicyEnsureResult\n): Promise<void> {\n const { plan } = table;\n const error =\n `another session held the table's lock for every attempt (lock_timeout ${lock.lockTimeoutMs} ms` +\n `${lastError ? `: ${lastError.split(\"\\n\")[0]}` : \"\"}) — a long transaction, a report or a dump`;\n if (table.rlsOn || await rlsIsOn(client, plan)) {\n result.tablesSecured++;\n result.deferred.push({ table: plan.qualified, error, rulesChanged: table.rulesChanged });\n return;\n }\n const closed = await withdrawGrant(client, plan, error);\n if (!closed.locked) {\n result.unsecured.push({ table: plan.qualified, error: closed.error, grantWithdrawn: closed.grantWithdrawn });\n }\n}\n"],"mappings":"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"}