@coffre/db 0.14.4 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/README.md +2 -1
  2. package/dist/change-records-_fKvahvs.js +55 -0
  3. package/dist/connect.d.ts +22 -10
  4. package/dist/connect.js +74 -12
  5. package/dist/database-CVteLhp1.d.ts +410 -0
  6. package/dist/{database-BadNeeN-.js → database-D55ENPX-.js} +46 -25
  7. package/dist/dialect.d.ts +4 -3
  8. package/dist/dialect.js +14 -5
  9. package/dist/grants.d.ts +4 -4
  10. package/dist/grants.js +4 -4
  11. package/dist/hyperdrive.d.ts +48 -23
  12. package/dist/hyperdrive.js +168 -91
  13. package/dist/index.d.ts +3 -3
  14. package/dist/index.js +1 -1
  15. package/dist/log-DaD7fyUq.d.ts +722 -0
  16. package/dist/log.d.ts +2 -596
  17. package/dist/log.js +224 -10
  18. package/dist/migrate.d.ts +43 -3
  19. package/dist/migrate.js +149 -29
  20. package/dist/migrations/postgres/0021_prune_vault_releases.sql +19 -0
  21. package/dist/migrations/postgres/0022_log_015.sql +672 -0
  22. package/dist/migrations/postgres/0023_change_path.sql +153 -0
  23. package/dist/migrations/postgres/0024_use_015.sql +556 -0
  24. package/dist/migrations/postgres/0025_app_change.sql +221 -0
  25. package/dist/migrations/postgres/0026_issue_lock.sql +106 -0
  26. package/dist/migrations/postgres/0027_retention.sql +118 -0
  27. package/dist/migrations/postgres/0028_use_stalls.sql +594 -0
  28. package/dist/migrations/postgres/0029_use_objects.sql +887 -0
  29. package/dist/migrations/postgres/0030_unarchive_restores.sql +259 -0
  30. package/dist/migrations/postgres/0031_app_read.sql +65 -0
  31. package/dist/migrations/postgres/0032_secrets_live_index.sql +4 -0
  32. package/dist/migrations/postgres/0033_use_generations.sql +582 -0
  33. package/dist/migrations/postgres/0034_vault_credentials.sql +153 -0
  34. package/dist/migrations/postgres/0035_first_days.sql +3 -0
  35. package/dist/migrations/postgres/0036_vault_issues.sql +91 -0
  36. package/dist/migrations/postgres/0037_vault_bindings.sql +271 -0
  37. package/dist/migrations/postgres/0040_binding_origin.sql +159 -0
  38. package/dist/migrations/postgres/0041_cutover.sql +132 -0
  39. package/dist/migrations/postgres/0042_sign_in_index.sql +7 -0
  40. package/dist/migrations/postgres/0043_issue_revoke_index.sql +4 -0
  41. package/dist/migrations/postgres/0044_use_by_name.sql +705 -0
  42. package/dist/migrations/postgres/0045_decide_first.sql +878 -0
  43. package/dist/migrations/postgres/0046_way_in_uses.sql +1149 -0
  44. package/dist/migrations/postgres/0047_cutover_finish.sql +268 -0
  45. package/dist/migrations/postgres/0048_use_batch.sql +59 -0
  46. package/dist/migrations/postgres/0049_reads.sql +171 -0
  47. package/dist/migrations/postgres/meta/0021_snapshot.json +3334 -0
  48. package/dist/migrations/postgres/meta/0022_snapshot.json +3831 -0
  49. package/dist/migrations/postgres/meta/0023_snapshot.json +3831 -0
  50. package/dist/migrations/postgres/meta/0024_snapshot.json +3831 -0
  51. package/dist/migrations/postgres/meta/0025_snapshot.json +3831 -0
  52. package/dist/migrations/postgres/meta/0026_snapshot.json +3831 -0
  53. package/dist/migrations/postgres/meta/0027_snapshot.json +3853 -0
  54. package/dist/migrations/postgres/meta/0028_snapshot.json +3853 -0
  55. package/dist/migrations/postgres/meta/0029_snapshot.json +3859 -0
  56. package/dist/migrations/postgres/meta/0030_snapshot.json +3859 -0
  57. package/dist/migrations/postgres/meta/0031_snapshot.json +3859 -0
  58. package/dist/migrations/postgres/meta/0032_snapshot.json +3875 -0
  59. package/dist/migrations/postgres/meta/0033_snapshot.json +3875 -0
  60. package/dist/migrations/postgres/meta/0034_snapshot.json +3875 -0
  61. package/dist/migrations/postgres/meta/0035_snapshot.json +3875 -0
  62. package/dist/migrations/postgres/meta/0036_snapshot.json +3875 -0
  63. package/dist/migrations/postgres/meta/0037_snapshot.json +3875 -0
  64. package/dist/migrations/postgres/meta/0038_snapshot.json +3881 -0
  65. package/dist/migrations/postgres/meta/0039_snapshot.json +3881 -0
  66. package/dist/migrations/postgres/meta/0040_snapshot.json +3931 -0
  67. package/dist/migrations/postgres/meta/0041_snapshot.json +3981 -0
  68. package/dist/migrations/postgres/meta/0042_snapshot.json +3981 -0
  69. package/dist/migrations/postgres/meta/0043_snapshot.json +3981 -0
  70. package/dist/migrations/postgres/meta/0044_snapshot.json +3991 -0
  71. package/dist/migrations/postgres/meta/0045_snapshot.json +3997 -0
  72. package/dist/migrations/postgres/meta/0046_snapshot.json +3997 -0
  73. package/dist/migrations/postgres/meta/0047_snapshot.json +4234 -0
  74. package/dist/migrations/postgres/meta/_journal.json +189 -0
  75. package/dist/migrations/sqlite/0018_log_015.sql +84 -0
  76. package/dist/migrations/sqlite/0019_retention.sql +2 -0
  77. package/dist/migrations/sqlite/0020_ended_references.sql +1 -0
  78. package/dist/migrations/sqlite/0021_secrets_live_index.sql +1 -0
  79. package/dist/migrations/sqlite/0022_binding_origin.sql +1 -0
  80. package/dist/migrations/sqlite/0023_sign_in_index.sql +2 -0
  81. package/dist/migrations/sqlite/0024_issue_revoke_index.sql +2 -0
  82. package/dist/migrations/sqlite/0025_way_in_uses.sql +42 -0
  83. package/dist/migrations/sqlite/0026_cutover_finish.sql +11 -0
  84. package/dist/migrations/sqlite/0027_reads.sql +38 -0
  85. package/dist/migrations/sqlite/meta/0018_snapshot.json +3529 -0
  86. package/dist/migrations/sqlite/meta/0019_snapshot.json +3538 -0
  87. package/dist/migrations/sqlite/meta/0020_snapshot.json +3545 -0
  88. package/dist/migrations/sqlite/meta/0021_snapshot.json +3553 -0
  89. package/dist/migrations/sqlite/meta/0022_snapshot.json +3560 -0
  90. package/dist/migrations/sqlite/meta/0023_snapshot.json +3579 -0
  91. package/dist/migrations/sqlite/meta/0024_snapshot.json +3598 -0
  92. package/dist/migrations/sqlite/meta/0025_snapshot.json +3609 -0
  93. package/dist/migrations/sqlite/meta/0026_snapshot.json +3616 -0
  94. package/dist/migrations/sqlite/meta/0027_snapshot.json +3834 -0
  95. package/dist/migrations/sqlite/meta/_journal.json +70 -0
  96. package/dist/platform-Y3gICK6n.d.ts +58 -0
  97. package/dist/portable.d.ts +2 -2
  98. package/dist/postgres-BI3xTnyw.js +1718 -0
  99. package/dist/postgres-platform-node.d.ts +14 -0
  100. package/dist/postgres-platform-node.js +21 -0
  101. package/dist/postgres-platform-workerd.d.ts +61 -0
  102. package/dist/postgres-platform-workerd.js +163 -0
  103. package/dist/{schema-z8GcN_Xu.d.ts → schema-DgoPqqsI.d.ts} +1329 -9
  104. package/dist/schema-sqlite.d.ts +1308 -8
  105. package/dist/schema-sqlite.js +156 -20
  106. package/dist/schema-version.d.ts +1 -1
  107. package/dist/schema-version.js +261 -2
  108. package/dist/schema.d.ts +2 -2
  109. package/dist/schema.js +229 -22
  110. package/package.json +13 -5
  111. package/src/active-subject.ts +9 -0
  112. package/src/change-records.ts +38 -0
  113. package/src/connect.ts +67 -13
  114. package/src/database.ts +67 -51
  115. package/src/dialect.ts +17 -8
  116. package/src/grants.ts +3 -3
  117. package/src/hyperdrive.ts +181 -104
  118. package/src/log.ts +244 -11
  119. package/src/migrate.ts +157 -30
  120. package/src/migrations/postgres/0021_prune_vault_releases.sql +19 -0
  121. package/src/migrations/postgres/0022_log_015.sql +672 -0
  122. package/src/migrations/postgres/0023_change_path.sql +153 -0
  123. package/src/migrations/postgres/0024_use_015.sql +556 -0
  124. package/src/migrations/postgres/0025_app_change.sql +221 -0
  125. package/src/migrations/postgres/0026_issue_lock.sql +106 -0
  126. package/src/migrations/postgres/0027_retention.sql +118 -0
  127. package/src/migrations/postgres/0028_use_stalls.sql +594 -0
  128. package/src/migrations/postgres/0029_use_objects.sql +887 -0
  129. package/src/migrations/postgres/0030_unarchive_restores.sql +259 -0
  130. package/src/migrations/postgres/0031_app_read.sql +65 -0
  131. package/src/migrations/postgres/0032_secrets_live_index.sql +4 -0
  132. package/src/migrations/postgres/0033_use_generations.sql +582 -0
  133. package/src/migrations/postgres/0034_vault_credentials.sql +153 -0
  134. package/src/migrations/postgres/0035_first_days.sql +3 -0
  135. package/src/migrations/postgres/0036_vault_issues.sql +91 -0
  136. package/src/migrations/postgres/0037_vault_bindings.sql +271 -0
  137. package/src/migrations/postgres/0040_binding_origin.sql +159 -0
  138. package/src/migrations/postgres/0041_cutover.sql +132 -0
  139. package/src/migrations/postgres/0042_sign_in_index.sql +7 -0
  140. package/src/migrations/postgres/0043_issue_revoke_index.sql +4 -0
  141. package/src/migrations/postgres/0044_use_by_name.sql +705 -0
  142. package/src/migrations/postgres/0045_decide_first.sql +878 -0
  143. package/src/migrations/postgres/0046_way_in_uses.sql +1149 -0
  144. package/src/migrations/postgres/0047_cutover_finish.sql +268 -0
  145. package/src/migrations/postgres/0048_use_batch.sql +59 -0
  146. package/src/migrations/postgres/0049_reads.sql +171 -0
  147. package/src/migrations/postgres/meta/0021_snapshot.json +3334 -0
  148. package/src/migrations/postgres/meta/0022_snapshot.json +3831 -0
  149. package/src/migrations/postgres/meta/0023_snapshot.json +3831 -0
  150. package/src/migrations/postgres/meta/0024_snapshot.json +3831 -0
  151. package/src/migrations/postgres/meta/0025_snapshot.json +3831 -0
  152. package/src/migrations/postgres/meta/0026_snapshot.json +3831 -0
  153. package/src/migrations/postgres/meta/0027_snapshot.json +3853 -0
  154. package/src/migrations/postgres/meta/0028_snapshot.json +3853 -0
  155. package/src/migrations/postgres/meta/0029_snapshot.json +3859 -0
  156. package/src/migrations/postgres/meta/0030_snapshot.json +3859 -0
  157. package/src/migrations/postgres/meta/0031_snapshot.json +3859 -0
  158. package/src/migrations/postgres/meta/0032_snapshot.json +3875 -0
  159. package/src/migrations/postgres/meta/0033_snapshot.json +3875 -0
  160. package/src/migrations/postgres/meta/0034_snapshot.json +3875 -0
  161. package/src/migrations/postgres/meta/0035_snapshot.json +3875 -0
  162. package/src/migrations/postgres/meta/0036_snapshot.json +3875 -0
  163. package/src/migrations/postgres/meta/0037_snapshot.json +3875 -0
  164. package/src/migrations/postgres/meta/0038_snapshot.json +3881 -0
  165. package/src/migrations/postgres/meta/0039_snapshot.json +3881 -0
  166. package/src/migrations/postgres/meta/0040_snapshot.json +3931 -0
  167. package/src/migrations/postgres/meta/0041_snapshot.json +3981 -0
  168. package/src/migrations/postgres/meta/0042_snapshot.json +3981 -0
  169. package/src/migrations/postgres/meta/0043_snapshot.json +3981 -0
  170. package/src/migrations/postgres/meta/0044_snapshot.json +3991 -0
  171. package/src/migrations/postgres/meta/0045_snapshot.json +3997 -0
  172. package/src/migrations/postgres/meta/0046_snapshot.json +3997 -0
  173. package/src/migrations/postgres/meta/0047_snapshot.json +4234 -0
  174. package/src/migrations/postgres/meta/_journal.json +189 -0
  175. package/src/migrations/sqlite/0018_log_015.sql +84 -0
  176. package/src/migrations/sqlite/0019_retention.sql +2 -0
  177. package/src/migrations/sqlite/0020_ended_references.sql +1 -0
  178. package/src/migrations/sqlite/0021_secrets_live_index.sql +1 -0
  179. package/src/migrations/sqlite/0022_binding_origin.sql +1 -0
  180. package/src/migrations/sqlite/0023_sign_in_index.sql +2 -0
  181. package/src/migrations/sqlite/0024_issue_revoke_index.sql +2 -0
  182. package/src/migrations/sqlite/0025_way_in_uses.sql +42 -0
  183. package/src/migrations/sqlite/0026_cutover_finish.sql +11 -0
  184. package/src/migrations/sqlite/0027_reads.sql +38 -0
  185. package/src/migrations/sqlite/meta/0018_snapshot.json +3529 -0
  186. package/src/migrations/sqlite/meta/0019_snapshot.json +3538 -0
  187. package/src/migrations/sqlite/meta/0020_snapshot.json +3545 -0
  188. package/src/migrations/sqlite/meta/0021_snapshot.json +3553 -0
  189. package/src/migrations/sqlite/meta/0022_snapshot.json +3560 -0
  190. package/src/migrations/sqlite/meta/0023_snapshot.json +3579 -0
  191. package/src/migrations/sqlite/meta/0024_snapshot.json +3598 -0
  192. package/src/migrations/sqlite/meta/0025_snapshot.json +3609 -0
  193. package/src/migrations/sqlite/meta/0026_snapshot.json +3616 -0
  194. package/src/migrations/sqlite/meta/0027_snapshot.json +3834 -0
  195. package/src/migrations/sqlite/meta/_journal.json +70 -0
  196. package/src/postgres-js/README.md +183 -0
  197. package/src/postgres-js/UNLICENSE +24 -0
  198. package/src/postgres-js/bytes.ts +89 -0
  199. package/src/postgres-js/connection.ts +882 -0
  200. package/src/postgres-js/errors.ts +84 -0
  201. package/src/postgres-js/index.ts +516 -0
  202. package/src/postgres-js/platform.node.ts +29 -0
  203. package/src/postgres-js/platform.ts +60 -0
  204. package/src/postgres-js/platform.workerd.ts +225 -0
  205. package/src/postgres-js/query.ts +123 -0
  206. package/src/postgres-js/queue.ts +42 -0
  207. package/src/postgres-js/result.ts +46 -0
  208. package/src/postgres-js/types.ts +52 -0
  209. package/src/postgres.ts +375 -26
  210. package/src/schema-version.ts +2 -2
  211. package/src/schema.sqlite.ts +200 -9
  212. package/src/schema.ts +294 -8
  213. package/src/verify-runtime.ts +18 -29
  214. package/LICENSE +0 -21
  215. package/dist/database-CFhTnnzM.d.ts +0 -73
  216. package/dist/postgres-Bmar8y0q.js +0 -29
  217. package/dist/relations-BfR3JjT1.js +0 -38
  218. package/src/relations.ts +0 -40
package/dist/log.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { t as afterCommit } from "./commits-BuW-NL3n.js";
2
- import { clockMillis, engineOf, forUpdate } from "./dialect.js";
3
- import { a as prepared, i as tablesOf } from "./database-BadNeeN-.js";
4
- import { and, asc, count, desc, eq, gt, gte, lt, lte, sql } from "drizzle-orm";
2
+ import { n as recorded, t as RECORDS } from "./change-records-_fKvahvs.js";
3
+ import { changedRows, clockMillis, engineOf, forUpdate, rowsOf } from "./dialect.js";
4
+ import { a as prepared, i as tablesOf } from "./database-D55ENPX-.js";
5
+ import { and, asc, count, desc, eq, gt, gte, isNotNull, isNull, lt, lte, sql } from "drizzle-orm";
6
+ import { createHash } from "node:crypto";
5
7
  import { GENESIS_HASH, sealEntry } from "@coffre/core/audit";
6
8
  //#region src/log.ts
7
9
  /** The log's head does not name its last entry: something moved it, or cut the log behind it. */
@@ -12,6 +14,19 @@ var LogHeadMismatch = class extends Error {
12
14
  }
13
15
  };
14
16
  /**
17
+ * As many transactions as may already wait for the log's head do
18
+ * (`lockLogHeadQueued`): this one waits for nothing, and holds nothing.
19
+ * Its code is Postgres's for a lock not available, so every caller already
20
+ * answers it as one that gave up waiting: unavailable, ask again shortly.
21
+ */
22
+ var HeadBusy = class extends Error {
23
+ code = "55P03";
24
+ constructor() {
25
+ super("too many requests wait for the audit log's head: ask again shortly");
26
+ this.name = "HeadBusy";
27
+ }
28
+ };
29
+ /**
15
30
  * The log's head is behind one this process wrote: the database was rolled
16
31
  * back, or entries were cut off its end. That cannot be told from a restore,
17
32
  * so the append refuses and someone has to look.
@@ -84,6 +99,35 @@ async function lockLogHead(tx) {
84
99
  return head;
85
100
  }
86
101
  /**
102
+ * Take the issue lock exclusive (migration 0026's, `1668244070, 1`): every
103
+ * issue in flight, a sign-in or a token's, ends first, and none starts
104
+ * until this transaction does. A write that moves what an issue's policy
105
+ * check compares (`issue_token`: the policy, a protected environment's
106
+ * slug) takes it, after its own rows and before the fence its append's
107
+ * trigger takes, as the checkpointer's barrier does: so the issue is
108
+ * ordered against it, and no use waits behind an issue (formal/Lifecycle.tla's
109
+ * IssuePolicyHeld, Migration015.tla's PolicyIssueFirst; T149, review r3).
110
+ * SQLite's transactions hold the whole file.
111
+ */
112
+ async function holdIssues(tx) {
113
+ if (engineOf(tx) === "sqlite") return;
114
+ await tx.execute(sql`SELECT pg_advisory_xact_lock(1668244070, 1)`);
115
+ }
116
+ /**
117
+ * The issue lock shared, then 0.15's fence exclusive (`1668244070, 0`), as
118
+ * 0.14's trigger takes them for an entry that changes access: what 0.15's
119
+ * cutover holds, its member rows and 0.14's head taken first, so that its
120
+ * own 0.14 entry's trigger waits on nothing (formal/Migration015.tla,
121
+ * CutoverIssueFirst; T132). Every use and every change waits for its
122
+ * commit; the checkpointer's barrier, which takes the issue lock exclusive
123
+ * first, too. SQLite's transactions hold the whole file.
124
+ */
125
+ async function holdFence(tx) {
126
+ if (engineOf(tx) === "sqlite") return;
127
+ await tx.execute(sql`SELECT pg_advisory_xact_lock_shared(1668244070, 1)`);
128
+ await tx.execute(sql`SELECT pg_advisory_xact_lock(1668244070, 0)`);
129
+ }
130
+ /**
87
131
  * Lock the log's head if no one holds it, without waiting: whether it is
88
132
  * held now. For work any caller may do, of which one at a time is enough,
89
133
  * which a request that finds the head taken leaves to the one that has it.
@@ -95,10 +139,45 @@ async function lockLogHeadIfFree(tx) {
95
139
  if (held) holding.add(tx);
96
140
  return held;
97
141
  }
98
- const { rows } = await tx.execute(sql`SELECT next_seq FROM audit_chain_head WHERE only_row FOR UPDATE SKIP LOCKED`);
142
+ const rows = await rowsOf(tx, sql`SELECT next_seq FROM audit_chain_head WHERE only_row FOR UPDATE SKIP LOCKED`);
99
143
  if (rows.length > 0) holding.add(tx);
100
144
  return rows.length > 0;
101
145
  }
146
+ /**
147
+ * Lock the log's head, waiting for it as `lockLogHead` does, if fewer than
148
+ * `waiters` other transactions hold one of its places in line; otherwise
149
+ * refuse at once (`HeadBusy`), having waited for nothing. A place is an
150
+ * advisory lock, tried, never waited for, and held to the end of the
151
+ * transaction, which holds the head by then: so at most `waiters`
152
+ * connections wait for the head, or hold it, through here, and a burst
153
+ * beyond them answers in one round trip rather than seconds later, holding
154
+ * a connection all the while. One statement: the place, then, only if one
155
+ * was free, the head; refused, one more says whether the head is there at
156
+ * all. SQLite's transactions hold the whole file.
157
+ */
158
+ async function lockLogHeadQueued(tx, waiters) {
159
+ if (engineOf(tx) === "sqlite") return lockLogHead(tx);
160
+ const [head] = await rowsOf(tx, sql`
161
+ SELECT next_seq, head_hash FROM audit_chain_head
162
+ WHERE only_row AND (SELECT true FROM generate_series(1, ${waiters}::int) AS place WHERE pg_try_advisory_xact_lock(${HEAD_LINE}, place) LIMIT 1)
163
+ FOR UPDATE`);
164
+ if (head === void 0) {
165
+ const [found] = await rowsOf(tx, sql`SELECT EXISTS (SELECT 1 FROM audit_chain_head WHERE only_row) AS found`);
166
+ if (found?.found !== true) throw new Error("audit_chain_head is missing; refusing to write an unchained entry");
167
+ throw new HeadBusy();
168
+ }
169
+ holding.add(tx);
170
+ return {
171
+ nextSeq: BigInt(head.next_seq),
172
+ headHash: head.head_hash
173
+ };
174
+ }
175
+ /**
176
+ * The first key of the head's places in line, Postgres's advisory locks:
177
+ * the second is the place. A space apart from the members' queues and the
178
+ * migrator's key.
179
+ */
180
+ const HEAD_LINE = 1668245096;
102
181
  const lockedHead = prepared((db) => {
103
182
  const { auditChainHead } = tablesOf(db);
104
183
  return forUpdate(db, db.select({
@@ -112,7 +191,7 @@ const lockedHead = prepared((db) => {
112
191
  * log as it stands, which no other append can change until this one
113
192
  * commits. It throws to refuse.
114
193
  */
115
- async function appendEntries(tx, key, entries, check, locked) {
194
+ async function appendEntries(tx, key, entries, check, locked, changes = [], log015 = false) {
116
195
  if (entries.length === 0) throw new Error("appendEntries called with no entries");
117
196
  const { auditChainHead, auditLog } = tablesOf(tx);
118
197
  if (locked === void 0 && !holding.has(tx)) await lockLogHead(tx);
@@ -159,12 +238,16 @@ async function appendEntries(tx, key, entries, check, locked) {
159
238
  ...fields,
160
239
  prevHash,
161
240
  mac,
162
- hash
241
+ hash,
242
+ ...log015 ? { log015: sql`true` } : {}
163
243
  });
164
244
  prevHash = hash;
165
245
  seq += 1n;
166
246
  }
167
- if (engineOf(tx) === "postgres" && rows.length <= ROWS_AT_ONCE) await tx.execute(sql`WITH appended AS (${tx.insert(auditLog).values(rows).getSQL()})
247
+ let numbers = [];
248
+ if (engineOf(tx) === "postgres" && rows.length <= ROWS_AT_ONCE && changes.length > 0) numbers = await callChanges(tx, changes, sql`WITH appended AS (${tx.insert(auditLog).values(rows).getSQL()}),
249
+ moved AS (UPDATE audit_chain_head SET next_seq = ${seq}, head_hash = ${prevHash} WHERE only_row) `);
250
+ else if (engineOf(tx) === "postgres" && rows.length <= ROWS_AT_ONCE) await tx.execute(sql`WITH appended AS (${tx.insert(auditLog).values(rows).getSQL()})
168
251
  UPDATE audit_chain_head SET next_seq = ${seq}, head_hash = ${prevHash} WHERE only_row`);
169
252
  else {
170
253
  await tx.insert(auditLog).values(rows);
@@ -172,6 +255,7 @@ async function appendEntries(tx, key, entries, check, locked) {
172
255
  nextSeq: seq,
173
256
  headHash: prevHash
174
257
  }).where(eq(auditChainHead.onlyRow, true));
258
+ numbers = await appChanges(tx, changes);
175
259
  }
176
260
  const written = {
177
261
  nextSeq: seq,
@@ -181,7 +265,8 @@ async function appendEntries(tx, key, entries, check, locked) {
181
265
  return {
182
266
  seqStart: head.nextSeq,
183
267
  ...written,
184
- occurredAt: now
268
+ occurredAt: now,
269
+ changes: numbers
185
270
  };
186
271
  }
187
272
  /**
@@ -210,7 +295,7 @@ async function readUnderHead(tx, key) {
210
295
  at: read.now
211
296
  };
212
297
  }
213
- /** Entries one statement inserts at most, at 21 parameters each: Postgres takes 65,535. */
298
+ /** Entries one statement inserts at most, at 21 parameters each (`log015` a literal): Postgres takes 65,535. */
214
299
  const ROWS_AT_ONCE = 3e3;
215
300
  /**
216
301
  * `readUnderHead`'s statement. Its `FOR UPDATE` waits for nothing in a
@@ -387,5 +472,134 @@ async function newestAuthentic(page, check) {
387
472
  }
388
473
  return "unreached";
389
474
  }
475
+ /**
476
+ * The app's changes to the objects its rows hold, as the vault sealed them
477
+ * (`Vault.prepare`), each `app_change` (migrations 0025, 0026 and 0030), in its
478
+ * transaction, once its rows and 0.14 entries are written: in order, in one
479
+ * round trip, or in the append's own (`appendEntries`). Each entry's number;
480
+ * 0 for a row of 0.14's, which logs nothing; null where a row's seal moved
481
+ * since the app read it, and nothing was done (ChangeCAS: read again). A row
482
+ * that is not its object's newest change's fails the call (`isStaleChange`),
483
+ * and its transaction with it.
484
+ *
485
+ * On SQLite, which has no functions, the same steps in the transaction,
486
+ * which holds the database's one write lock: the fence is implicit there
487
+ * (0.15's plan, "Postgres serves; SQLite is for tests").
488
+ */
489
+ async function appChanges(tx, calls) {
490
+ if (calls.length === 0) return [];
491
+ if (engineOf(tx) === "sqlite") {
492
+ const numbers = [];
493
+ for (const call of calls) numbers.push(await changeOnSqlite(tx, call, bytes));
494
+ return numbers;
495
+ }
496
+ return callChanges(tx, calls, sql``);
497
+ }
498
+ const bytes = (b64) => b64 === null ? null : Buffer.from(b64, "base64");
499
+ /** `app_change`, once per call, in order, in one statement: after `before`, a `WITH` of the caller's. */
500
+ async function callChanges(tx, calls, before) {
501
+ const rows = sql.join(calls.map((c, i) => sql`(${i}::int, ${c.kind}::text, ${c.id}::text, ${c.requestId}::text, ${c.at}::bigint, ${c.actor}::text, ${c.payload}::text, ${bytes(c.sealFrom)}::bytea, ${bytes(c.seal)}::bytea, ${bytes(c.mac)}::bytea, ${bytes(c.restores)}::bytea)`), sql`, `);
502
+ const numbers = await rowsOf(tx, sql`${before}
503
+ SELECT c.ord, app_change(c.kind, c.id, c.request, c.at, c.actor, c.payload, c.seal_from, c.seal, c.mac, c.restores) AS number
504
+ FROM (VALUES ${rows}) AS c(ord, kind, id, request, at, actor, payload, seal_from, seal, mac, restores)
505
+ ORDER BY c.ord`);
506
+ if (numbers.some(({ ord }, i) => Number(ord) !== i)) throw new Error("app_change answered out of order");
507
+ return numbers.map(({ number }) => number === null ? null : BigInt(number));
508
+ }
509
+ /**
510
+ * An object's key in 0.15's log, `log_changes.object`: the first 16 bytes
511
+ * of SHA-256 over its kind and id, as a uuid (`log_object` in SQL). The
512
+ * vault's (`objectKey`), and the app's, which finds a credential's sign-in
513
+ * by it.
514
+ */
515
+ function logObject(kind, id) {
516
+ const hex = createHash("sha256").update(`coffre.object.v1\0${kind}\0${id}`).digest("hex");
517
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
518
+ }
519
+ /** Whether `error` is `app_change`'s refusal of a row that is not its object's newest change's: Postgres's `data_corrupted`. */
520
+ function isStaleChange(error) {
521
+ for (let cause = error; typeof cause === "object" && cause !== null; cause = cause.cause) if (cause instanceof StaleRow || cause.code === "XX001") return true;
522
+ return false;
523
+ }
524
+ var StaleRow = class extends Error {};
525
+ const APP_OPS = {
526
+ credential: [
527
+ "credential.issue",
528
+ "credential.revoke",
529
+ "credential.renew",
530
+ "credential.scope"
531
+ ],
532
+ connection: ["connection.make", "connection.revoke"],
533
+ secret: ["secret.write", "secret.delete"],
534
+ environment: [
535
+ "environment.create",
536
+ "environment.rename",
537
+ "environment.archive",
538
+ "project.archive",
539
+ "environment.delete"
540
+ ]
541
+ };
542
+ /** `app_change` on SQLite. */
543
+ async function changeOnSqlite(tx, call, bytes) {
544
+ const { logChanges, logUses } = tablesOf(tx);
545
+ const table = appTable(tx, call.kind);
546
+ const [row] = await tx.select({ seal: table.seal }).from(table).where(eq(table.id, call.id));
547
+ const held = row?.seal ?? null;
548
+ const [newest] = await tx.select({ seal: logChanges.seal }).from(logChanges).where(eq(logChanges.object, call.object)).orderBy(desc(logChanges.number)).limit(1);
549
+ if (call.requestId === null) {
550
+ if (held !== null || newest !== void 0) throw new StaleRow(`the ${call.kind} object's row is not its newest change's: stale`);
551
+ return 0n;
552
+ }
553
+ const seal = bytes(call.seal);
554
+ const [prior] = await tx.select({
555
+ number: logChanges.number,
556
+ object: logChanges.object,
557
+ payload: logChanges.payload,
558
+ seal: logChanges.seal
559
+ }).from(logChanges).where(eq(logChanges.requestId, call.requestId));
560
+ if (prior !== void 0) {
561
+ if (prior.object !== call.object || prior.payload !== call.payload || !sameBytes(prior.seal ?? null, seal)) throw Object.assign(/* @__PURE__ */ new Error(`request ${call.requestId} names another change`), { code: "SQLITE_CONSTRAINT_UNIQUE" });
562
+ return BigInt(prior.number);
563
+ }
564
+ const payload = JSON.parse(call.payload ?? "null");
565
+ if (typeof payload?.op !== "string" || !(APP_OPS[call.kind] ?? []).includes(payload.op)) throw new Error(`the app makes no change ${String(payload?.op)} to a ${call.kind}`);
566
+ if (payload.ref?.kind !== call.kind || payload.ref.id !== call.id) throw new Error(`a change names the object it is to: ${call.kind}, ${call.id}`);
567
+ if (!sameBytes(held, newest?.seal ?? null)) throw new StaleRow(`the ${call.kind} object's row is not its newest change's: stale`);
568
+ const restores = bytes(call.restores);
569
+ if (restores !== null) {
570
+ if (call.kind !== "secret" || payload.op !== "secret.write" || call.sealFrom !== null) throw new Error("only a key's object made again from none restores one");
571
+ const [kept] = await tx.select({ seal: logChanges.seal }).from(logChanges).where(and(eq(logChanges.object, call.object), isNotNull(logChanges.seal))).orderBy(desc(logChanges.number)).limit(1);
572
+ if (!sameBytes(restores, kept?.seal ?? null)) throw new StaleRow("the secret object an unarchive restores is not its newest sealed one: stale");
573
+ }
574
+ const from = bytes(call.sealFrom);
575
+ if (changedRows(await tx.update(table).set({ seal }).where(and(eq(table.id, call.id), from === null ? isNull(table.seal) : eq(table.seal, from)))) === 0) return null;
576
+ const [{ last }] = await tx.select({ last: sql`max(n)` }).from(sql`(SELECT max(${logChanges.number}) AS n FROM ${logChanges} UNION ALL SELECT max(${logUses.number}) FROM ${logUses})`);
577
+ const number = BigInt(last ?? 0) + 1n;
578
+ await tx.insert(logChanges).values({
579
+ number,
580
+ at: call.at,
581
+ requestId: call.requestId,
582
+ object: call.object,
583
+ actor: call.actor,
584
+ payload: call.payload,
585
+ seal,
586
+ mac: bytes(call.mac)
587
+ });
588
+ return number;
589
+ }
590
+ /** The table whose rows hold an object of this kind, the app's. */
591
+ function appTable(tx, kind) {
592
+ const { credentials, mcpConnections, secrets, environments } = tablesOf(tx);
593
+ switch (kind) {
594
+ case "credential": return credentials;
595
+ case "connection": return mcpConnections;
596
+ case "secret": return secrets;
597
+ case "environment": return environments;
598
+ default: throw new Error(`no object kind ${String(kind)}`);
599
+ }
600
+ }
601
+ function sameBytes(a, b) {
602
+ return a === null || b === null ? a === b : a.equals(b);
603
+ }
390
604
  //#endregion
391
- export { BATCH_BYTES, KeyRetired, LOOK_BACK, LogHeadMismatch, LogRewound, MOVES_FROM, appendEntries, entryColumns, forgetLogHeads, lockLogHead, lockLogHeadIfFree, missingEntry, newestAuthentic, newestSeqQuery, newestVaultEntries, newestVaultSeq, withinBytes };
605
+ export { BATCH_BYTES, HeadBusy, KeyRetired, LOOK_BACK, LogHeadMismatch, LogRewound, MOVES_FROM, RECORDS, appChanges, appendEntries, entryColumns, forgetLogHeads, holdFence, holdIssues, isStaleChange, lockLogHead, lockLogHeadIfFree, lockLogHeadQueued, logObject, missingEntry, newestAuthentic, newestSeqQuery, newestVaultEntries, newestVaultSeq, recorded, withinBytes };
package/dist/migrate.d.ts CHANGED
@@ -1,8 +1,48 @@
1
+ import { f as PostgresClient } from "./database-CVteLhp1.js";
1
2
  import { Engine } from "./dialect.js";
2
- import { PoolClient } from "pg";
3
3
  //#region src/migrate.d.ts
4
- /** Reassert database privileges that pg_dump and CREATE DATABASE TEMPLATE do not carry. */
5
- export declare function restrictDatabase(client: Pick<PoolClient, 'query'>): Promise<void>;
4
+ /**
5
+ * Reassert database privileges that pg_dump and CREATE DATABASE TEMPLATE do not carry, the hot path's login's
6
+ * group and settings, and the runtime logins' `jit = off`.
7
+ */
8
+ export declare function restrictDatabase(client: Pick<PostgresClient, 'unsafe'>): Promise<void>;
9
+ /**
10
+ * What 0022 gives `coffre_vault_hot`, given again when it lacks it: its
11
+ * membership in coffre_vault, inherited, and its lock and statement
12
+ * timeouts and generic plans. A role dropped and made again, as `coffre
13
+ * setup` makes a missing one, has none of them, and 0022 never runs
14
+ * twice. So whatever 0022 refuses, this refuses too: a role made again
15
+ * may be anyone's. Unsafe attributes, objects it owns, a membership in
16
+ * another role, one of any grantor that may set or grant coffre_vault
17
+ * (which would let it hand the vault's rights to the app's login), and a
18
+ * setting of its own for this database alone, which would outrank the
19
+ * server-wide one at login. Then only what differs, and refused, saying
20
+ * what to run, to a login that may not change the role or grant the group.
21
+ */
22
+ export declare function holdHotLogin(client: Pick<PostgresClient, 'unsafe'>): Promise<void>;
23
+ /**
24
+ * Set `jit = off` on `logins`, when they lack it.
25
+ *
26
+ * Past `jit_above_cost` (100,000 by default) Postgres compiles a plan's
27
+ * expressions at every execution of it: 150 ms where a use costs under 2 ms
28
+ * (0029), and a statement's estimate grows with its tables. The functions that
29
+ * run as their owner say no JIT themselves (0029); the setting here covers
30
+ * every other statement of these logins, now and to come. A role's settings
31
+ * apply to the role that logs in, never through a group, and a pooler such as
32
+ * Hyperdrive may not pass a startup option on, so it is the login's own, on
33
+ * the server. It is the server's, not the database's, as the logins are (and
34
+ * as 0022 sets the hot login's): made once by a login with CREATEROLE and
35
+ * ADMIN on them, and found set by any other that migrates after.
36
+ *
37
+ * Only when it differs, whatever way the boolean is spelled: altering a role
38
+ * takes CREATEROLE and its ADMIN, which a login that only migrates may lack.
39
+ * The row is the server's, and the migration lock each database's, so a run
40
+ * on another database may be changing it too: Postgres fails the second
41
+ * change (a concurrent update, or a duplicate key), and it looks again once
42
+ * the first has committed. A setting of the login's for this database alone
43
+ * outranks the server-wide one at login, so it is refused, as 0022 does.
44
+ */
45
+ export declare function turnJitOff(client: Pick<PostgresClient, 'unsafe'>, logins: readonly string[]): Promise<void>;
6
46
  /** Why `login` cannot be the backup login, before anything runs, or null when it may. */
7
47
  export declare function backupLoginProblem(login: string): string | null;
8
48
  /**
package/dist/migrate.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as postgresConnection } from "./postgres-Bmar8y0q.js";
1
+ import { a as quoteIdentifier, i as postgresSession } from "./postgres-BI3xTnyw.js";
2
2
  import { engineOfUrl } from "./connect.js";
3
3
  import { createHash } from "node:crypto";
4
4
  import { readFile } from "node:fs/promises";
@@ -15,15 +15,144 @@ import { fileURLToPath } from "node:url";
15
15
  * whole run. Restricted runtime roles are Postgres-only (baseline/postgres.sql).
16
16
  */
17
17
  const MIGRATION_LOCK_KEY = "7165058122361679213";
18
- /** Reassert database privileges that pg_dump and CREATE DATABASE TEMPLATE do not carry. */
18
+ /** The logins that run coffre's own statements: the app's, the vault's, and 0.15's hot path's. */
19
+ const RUNTIME_LOGINS = [
20
+ "coffre_runtime",
21
+ "coffre_vault_runtime",
22
+ "coffre_vault_hot"
23
+ ];
24
+ /**
25
+ * Reassert database privileges that pg_dump and CREATE DATABASE TEMPLATE do not carry, the hot path's login's
26
+ * group and settings, and the runtime logins' `jit = off`.
27
+ */
19
28
  async function restrictDatabase(client) {
20
- await client.query(`DO $$ BEGIN
29
+ await client.unsafe(`DO $$ BEGIN
21
30
  EXECUTE format(
22
- 'REVOKE CREATE, TEMPORARY ON DATABASE %I FROM PUBLIC, coffre_app, coffre_runtime, coffre_vault, coffre_vault_runtime, coffre_backup',
31
+ 'REVOKE CREATE, TEMPORARY ON DATABASE %I FROM PUBLIC, coffre_app, coffre_runtime, coffre_vault, coffre_vault_runtime, coffre_vault_hot, coffre_backup',
23
32
  current_database()
24
33
  );
34
+ END $$`);
35
+ await holdHotLogin(client);
36
+ await turnJitOff(client, RUNTIME_LOGINS);
37
+ }
38
+ /**
39
+ * What 0022 gives `coffre_vault_hot`, given again when it lacks it: its
40
+ * membership in coffre_vault, inherited, and its lock and statement
41
+ * timeouts and generic plans. A role dropped and made again, as `coffre
42
+ * setup` makes a missing one, has none of them, and 0022 never runs
43
+ * twice. So whatever 0022 refuses, this refuses too: a role made again
44
+ * may be anyone's. Unsafe attributes, objects it owns, a membership in
45
+ * another role, one of any grantor that may set or grant coffre_vault
46
+ * (which would let it hand the vault's rights to the app's login), and a
47
+ * setting of its own for this database alone, which would outrank the
48
+ * server-wide one at login. Then only what differs, and refused, saying
49
+ * what to run, to a login that may not change the role or grant the group.
50
+ */
51
+ async function holdHotLogin(client) {
52
+ await client.unsafe(`DO $$ DECLARE
53
+ held text[];
54
+ BEGIN
55
+ IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'coffre_vault_hot') THEN
56
+ RAISE EXCEPTION 'coffre_vault_hot does not exist, though 0022 made it: run coffre setup, which makes it a login, or have a login with CREATEROLE run CREATE ROLE coffre_vault_hot NOLOGIN, then migrate again';
57
+ END IF;
58
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'coffre_vault_hot' AND (rolsuper OR rolreplication OR rolbypassrls)) THEN
59
+ RAISE EXCEPTION 'coffre_vault_hot has unsafe role attributes: SUPERUSER, REPLICATION and BYPASSRLS must be off';
60
+ END IF;
61
+ IF EXISTS (SELECT 1 FROM pg_shdepend WHERE refclassid = 'pg_authid'::regclass AND refobjid = 'coffre_vault_hot'::regrole AND deptype = 'o') THEN
62
+ RAISE EXCEPTION 'coffre_vault_hot owns database objects';
63
+ END IF;
64
+ IF EXISTS (SELECT 1 FROM pg_auth_members WHERE member = 'coffre_vault_hot'::regrole AND roleid <> 'coffre_vault'::regrole) THEN
65
+ RAISE EXCEPTION 'coffre_vault_hot is a member of a role other than coffre_vault: revoke its memberships';
66
+ END IF;
67
+ IF EXISTS (
68
+ SELECT 1 FROM pg_auth_members WHERE member = 'coffre_vault_hot'::regrole AND roleid = 'coffre_vault'::regrole AND (set_option OR admin_option)
69
+ ) THEN
70
+ RAISE EXCEPTION 'coffre_vault_hot may set or grant coffre_vault: have its grantor run REVOKE coffre_vault FROM coffre_vault_hot, then migrate again';
71
+ END IF;
72
+ IF EXISTS (
73
+ SELECT 1 FROM pg_db_role_setting s, unnest(s.setconfig) AS setting
74
+ WHERE s.setrole = 'coffre_vault_hot'::regrole AND s.setdatabase = (SELECT oid FROM pg_database WHERE datname = current_database())
75
+ AND split_part(setting, '=', 1) IN ('lock_timeout', 'statement_timeout', 'plan_cache_mode')
76
+ ) THEN
77
+ RAISE EXCEPTION 'coffre_vault_hot has its own lock_timeout, statement_timeout or plan_cache_mode in this database, over the hot path''s: run ALTER ROLE coffre_vault_hot IN DATABASE % RESET ALL, then migrate again', quote_ident(current_database());
78
+ END IF;
79
+ -- The role is the server's, as turnJitOff's logins are: a run on another database may be changing it too, and fails the second change.
80
+ FOR attempt IN 1..5 LOOP
81
+ BEGIN
82
+ SELECT setconfig INTO held FROM pg_db_role_setting WHERE setrole = 'coffre_vault_hot'::regrole AND setdatabase = 0;
83
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'coffre_vault_hot' AND (NOT rolinherit OR rolcreatedb OR rolcreaterole)) THEN
84
+ ALTER ROLE coffre_vault_hot INHERIT NOCREATEDB NOCREATEROLE;
85
+ END IF;
86
+ IF NOT coalesce(held @> ARRAY['lock_timeout=1s'], false) THEN ALTER ROLE coffre_vault_hot SET lock_timeout = '1s'; END IF;
87
+ IF NOT coalesce(held @> ARRAY['statement_timeout=2s'], false) THEN ALTER ROLE coffre_vault_hot SET statement_timeout = '2s'; END IF;
88
+ IF NOT coalesce(held @> ARRAY['plan_cache_mode=force_generic_plan'], false) THEN ALTER ROLE coffre_vault_hot SET plan_cache_mode = force_generic_plan; END IF;
89
+ IF NOT EXISTS (
90
+ SELECT 1 FROM pg_auth_members WHERE member = 'coffre_vault_hot'::regrole AND roleid = 'coffre_vault'::regrole AND inherit_option
91
+ ) THEN
92
+ GRANT coffre_vault TO coffre_vault_hot WITH ADMIN FALSE, INHERIT TRUE, SET FALSE;
93
+ END IF;
94
+ EXIT;
95
+ EXCEPTION
96
+ WHEN insufficient_privilege THEN
97
+ RAISE EXCEPTION '% may not make coffre_vault_hot what 0.15 needs (%): have a login with CREATEROLE and ADMIN on both roles run ALTER ROLE coffre_vault_hot INHERIT NOCREATEDB NOCREATEROLE; ALTER ROLE coffre_vault_hot SET lock_timeout = ''1s''; ALTER ROLE coffre_vault_hot SET statement_timeout = ''2s''; ALTER ROLE coffre_vault_hot SET plan_cache_mode = force_generic_plan; GRANT coffre_vault TO coffre_vault_hot WITH ADMIN FALSE, INHERIT TRUE, SET FALSE first', current_user, SQLERRM;
98
+ WHEN internal_error OR unique_violation THEN
99
+ IF attempt = 5 THEN RAISE; END IF;
100
+ PERFORM pg_sleep(0.2);
101
+ END;
102
+ END LOOP;
25
103
  END $$`);
26
104
  }
105
+ /**
106
+ * Set `jit = off` on `logins`, when they lack it.
107
+ *
108
+ * Past `jit_above_cost` (100,000 by default) Postgres compiles a plan's
109
+ * expressions at every execution of it: 150 ms where a use costs under 2 ms
110
+ * (0029), and a statement's estimate grows with its tables. The functions that
111
+ * run as their owner say no JIT themselves (0029); the setting here covers
112
+ * every other statement of these logins, now and to come. A role's settings
113
+ * apply to the role that logs in, never through a group, and a pooler such as
114
+ * Hyperdrive may not pass a startup option on, so it is the login's own, on
115
+ * the server. It is the server's, not the database's, as the logins are (and
116
+ * as 0022 sets the hot login's): made once by a login with CREATEROLE and
117
+ * ADMIN on them, and found set by any other that migrates after.
118
+ *
119
+ * Only when it differs, whatever way the boolean is spelled: altering a role
120
+ * takes CREATEROLE and its ADMIN, which a login that only migrates may lack.
121
+ * The row is the server's, and the migration lock each database's, so a run
122
+ * on another database may be changing it too: Postgres fails the second
123
+ * change (a concurrent update, or a duplicate key), and it looks again once
124
+ * the first has committed. A setting of the login's for this database alone
125
+ * outranks the server-wide one at login, so it is refused, as 0022 does.
126
+ */
127
+ async function turnJitOff(client, logins) {
128
+ const jit = (database, value) => `EXISTS (
129
+ SELECT 1 FROM pg_db_role_setting s, unnest(s.setconfig) AS setting
130
+ WHERE s.setrole = login::regrole AND s.setdatabase = ${database}
131
+ AND (CASE WHEN split_part(setting, '=', 1) = 'jit' THEN substr(setting, 5)::boolean END) IS ${value})`;
132
+ await client.unsafe(`DO $$ DECLARE
133
+ login text;
134
+ here oid := (SELECT oid FROM pg_database WHERE datname = current_database());
135
+ BEGIN
136
+ FOREACH login IN ARRAY ARRAY[${logins.map((login) => `'${login}'`).join(", ")}] LOOP
137
+ IF ${jit("here", "TRUE")} THEN
138
+ RAISE EXCEPTION '% has its own jit setting in this database, over the server-wide one: run ALTER ROLE % IN DATABASE % RESET jit, then migrate again', login, login, quote_ident(current_database());
139
+ END IF;
140
+ FOR attempt IN 1..5 LOOP
141
+ EXIT WHEN ${jit("0", "FALSE")};
142
+ BEGIN
143
+ EXECUTE format('ALTER ROLE %I SET jit = off', login);
144
+ EXIT;
145
+ EXCEPTION
146
+ WHEN insufficient_privilege THEN
147
+ RAISE EXCEPTION '% may not set jit = off on %, whose statements Postgres would compile at every run: have a login with CREATEROLE and ADMIN on it run ALTER ROLE % SET jit = off, then migrate again', current_user, login, login;
148
+ WHEN internal_error OR unique_violation THEN
149
+ IF attempt = 5 THEN RAISE; END IF;
150
+ PERFORM pg_sleep(0.2);
151
+ END;
152
+ END LOOP;
153
+ END LOOP;
154
+ END $$`);
155
+ }
27
156
  /** Why `login` cannot be the backup login, before anything runs, or null when it may. */
28
157
  function backupLoginProblem(login) {
29
158
  if (login === "") return "the backup login is empty: name the role to grant coffre_backup";
@@ -32,7 +161,8 @@ function backupLoginProblem(login) {
32
161
  "coffre_app",
33
162
  "coffre_vault",
34
163
  "coffre_runtime",
35
- "coffre_vault_runtime"
164
+ "coffre_vault_runtime",
165
+ "coffre_vault_hot"
36
166
  ].includes(login)) return `the backup login is ${login}, one of coffre's own roles: name the login that backs up`;
37
167
  return null;
38
168
  }
@@ -170,40 +300,31 @@ function verifyHistory(expected, applied, requireComplete) {
170
300
  if (requireComplete && rows.length !== expected.length) throw new Error(`migration history is incomplete: expected ${expected.length}, found ${rows.length}`);
171
301
  }
172
302
  async function postgresMigrator(url) {
173
- const [{ default: pg }, { drizzle }, { migrate }] = await Promise.all([
174
- import("pg"),
175
- import("drizzle-orm/node-postgres"),
176
- import("drizzle-orm/node-postgres/migrator")
177
- ]);
178
- const pool = new pg.Pool({
179
- ...postgresConnection(url),
180
- application_name: "coffre-migrations",
181
- max: 1
182
- });
183
- const client = await pool.connect();
303
+ const [{ createDatabase }, { migrate }] = await Promise.all([import("./index.js"), import("drizzle-orm/postgres-js/migrator")]);
304
+ const client = postgresSession(url, { application_name: "coffre-migrations" });
184
305
  return {
185
306
  async applied() {
186
- if (!(await client.query("SELECT to_regclass('drizzle.__drizzle_migrations')::text AS relation")).rows[0]?.relation) return null;
187
- return (await client.query("SELECT hash, created_at::text FROM drizzle.__drizzle_migrations ORDER BY created_at, id")).rows;
307
+ const [found] = await client.unsafe("SELECT to_regclass('drizzle.__drizzle_migrations')::text AS relation");
308
+ if (!found?.relation) return null;
309
+ return [...await client.unsafe("SELECT hash, created_at::text FROM drizzle.__drizzle_migrations ORDER BY created_at, id")];
188
310
  },
189
311
  async lock() {
190
- await client.query("SET lock_timeout = '5min'");
191
- await client.query("SELECT pg_advisory_lock($1::bigint)", [MIGRATION_LOCK_KEY]);
312
+ await client.unsafe("SET lock_timeout = '5min'");
313
+ await client.unsafe("SELECT pg_advisory_lock($1::bigint)", [MIGRATION_LOCK_KEY]);
192
314
  },
193
315
  async unlock() {
194
- await client.query("SELECT pg_advisory_unlock($1::bigint)", [MIGRATION_LOCK_KEY]);
316
+ await client.unsafe("SELECT pg_advisory_unlock($1::bigint)", [MIGRATION_LOCK_KEY]);
195
317
  },
196
- migrate: (migrationsFolder) => migrate(drizzle(client), { migrationsFolder }),
318
+ migrate: (migrationsFolder) => migrate(createDatabase(client), { migrationsFolder }),
197
319
  async restrict(backupLogin) {
198
320
  await restrictDatabase(client);
199
321
  if (backupLogin !== void 0) {
200
- const { rowCount } = await client.query("SELECT 1 FROM pg_roles WHERE rolname::text = $1", [backupLogin]);
201
- if (rowCount === 0) throw new Error(`role ${client.escapeIdentifier(backupLogin)} does not exist: make it, coffre never does`);
202
- await client.query(`GRANT coffre_backup TO ${client.escapeIdentifier(backupLogin)} WITH ADMIN FALSE, INHERIT TRUE, SET FALSE`);
322
+ if ((await client.unsafe("SELECT 1 FROM pg_roles WHERE rolname::text = $1", [backupLogin])).length === 0) throw new Error(`role ${quoteIdentifier(backupLogin)} does not exist: make it, coffre never does`);
323
+ await client.unsafe(`GRANT coffre_backup TO ${quoteIdentifier(backupLogin)} WITH ADMIN FALSE, INHERIT TRUE, SET FALSE`);
203
324
  }
204
325
  },
205
326
  async notOwner({ relations, functions }) {
206
- const { rows } = await client.query(FOREIGN_OWNERS, [relations, functions]);
327
+ const rows = await client.unsafe(FOREIGN_OWNERS, [relations, functions]);
207
328
  if (rows.length === 0) return null;
208
329
  const [{ login, grants_inherit: grantsInherit, inherits }] = rows;
209
330
  return new NotOwner(login, rows.map(({ owner, member }) => ({
@@ -215,8 +336,7 @@ async function postgresMigrator(url) {
215
336
  });
216
337
  },
217
338
  async close() {
218
- client.release();
219
- await pool.end();
339
+ await client.end();
220
340
  }
221
341
  };
222
342
  }
@@ -317,4 +437,4 @@ async function migrateDatabase(url, { onPlan, backupLogin } = {}) {
317
437
  }
318
438
  }
319
439
  //#endregion
320
- export { AfterMigrations, DatabaseAhead, NotOwner, backupLoginProblem, managedObjects, migrateDatabase, migrationStatus, migrationsFolder, restrictDatabase };
440
+ export { AfterMigrations, DatabaseAhead, NotOwner, backupLoginProblem, holdHotLogin, managedObjects, migrateDatabase, migrationStatus, migrationsFolder, restrictDatabase, turnJitOff };
@@ -0,0 +1,19 @@
1
+ -- Releases pruned (W199, T111): the vault's checkpoint forgets the releases no window can count again, those over a day and an hour old by the database's clock, at most at_most a call. A bulk limit's window is a day at most (the vault's config), and the hour is a margin for the clock stepping back: no caller chooses what is kept, so the vault's login, which still deletes no row itself, cannot forget a release a window counts. The release before never calls it.
2
+ CREATE FUNCTION prune_vault_releases(at_most integer) RETURNS integer
3
+ LANGUAGE sql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET lock_timeout = '3s' AS $$
4
+ -- The bound in the DELETE's own WHERE too: a row a read moved on since the subquery's snapshot is checked again,
5
+ -- as it is now, and kept.
6
+ WITH bound AS (SELECT (extract(epoch from clock_timestamp()) * 1000)::bigint - 25 * 3600000 AS before),
7
+ pruned AS (
8
+ DELETE FROM public.vault_releases
9
+ WHERE released_at < (SELECT before FROM bound)
10
+ AND ctid IN (
11
+ -- Any of them, each past every window; a row an append holds is left to the next run, rather than waited for.
12
+ SELECT ctid FROM public.vault_releases WHERE released_at < (SELECT before FROM bound) LIMIT at_most FOR UPDATE SKIP LOCKED
13
+ )
14
+ RETURNING 1
15
+ )
16
+ SELECT count(*)::integer FROM pruned;
17
+ $$;--> statement-breakpoint
18
+ REVOKE ALL ON FUNCTION prune_vault_releases(integer) FROM PUBLIC;--> statement-breakpoint
19
+ GRANT EXECUTE ON FUNCTION prune_vault_releases(integer) TO coffre_vault;