@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
@@ -0,0 +1,672 @@
1
+ -- 0.15's log (docs/design/hot-path-one.md, docs/design/0.15-plan.md, PR 2; the spec: formal/HotPathOne.tla, formal/Migration015.tla): its tables, the seals on the decision inputs, the functions every use and change will be, and a login of its own for the hot path. It only adds, and nothing calls any of it yet: 0.14's code serves on it as before. The vault login reads the new tables and writes them only through the functions, which run as their owner, so that every number is taken under the fence's lock whatever the vault's code does (and 0.14's conformance holds: the vault writes no table but its own).
2
+ CREATE TABLE "log_config" (
3
+ "name" text PRIMARY KEY NOT NULL,
4
+ "value" text NOT NULL,
5
+ "seal" "bytea",
6
+ CONSTRAINT "log_config_name_check" CHECK ("log_config"."name" IN ('policy', 'roles')),
7
+ CONSTRAINT "log_config_value_check" CHECK ("log_config"."value"::jsonb IS NOT NULL),
8
+ CONSTRAINT "log_config_seal_check" CHECK ("log_config"."seal" IS NULL OR octet_length("log_config"."seal") = 16)
9
+ );
10
+ --> statement-breakpoint
11
+ -- CACHE 1 (the default) matters: numbers rise in the order they are taken, across sessions, and the barrier reads the last one taken. A cache per session would hand a later call a smaller number.
12
+ CREATE SEQUENCE "log_number" AS bigint CACHE 1;
13
+ --> statement-breakpoint
14
+ CREATE TABLE "log_changes" (
15
+ "number" bigint PRIMARY KEY NOT NULL,
16
+ "at" bigint NOT NULL,
17
+ "request_id" text NOT NULL,
18
+ "object" uuid NOT NULL,
19
+ "actor" text NOT NULL,
20
+ "payload" text NOT NULL,
21
+ "seal" "bytea",
22
+ "mac" "bytea" NOT NULL,
23
+ CONSTRAINT "log_changes_request_id_key" UNIQUE("request_id"),
24
+ CONSTRAINT "log_changes_seal_check" CHECK ("log_changes"."seal" IS NULL OR octet_length("log_changes"."seal") = 16),
25
+ CONSTRAINT "log_changes_mac_check" CHECK (octet_length("log_changes"."mac") = 16)
26
+ );
27
+ --> statement-breakpoint
28
+ -- Each input's newest change, its seal included, from the index alone: one probe per input.
29
+ CREATE INDEX "log_changes_object_idx" ON "log_changes" USING btree ("object","number") INCLUDE ("seal");
30
+ --> statement-breakpoint
31
+ -- Uses, by the day of their `at` (make_day): retention drops a day whole. Fixed-width columns first, and eight of them: a ninth makes the null bitmap two bytes, and the header 32 bytes instead of 24 (117 B a use against 109, measured). So `purpose` carries whether the use may keep a sensitive value, as 256 added to its code.
32
+ CREATE TABLE "log_uses" (
33
+ "number" bigint NOT NULL,
34
+ "at" bigint NOT NULL,
35
+ "request_id" bigint NOT NULL,
36
+ "credential" uuid NOT NULL,
37
+ "secret" uuid NOT NULL,
38
+ "purpose" smallint NOT NULL,
39
+ "mac" "bytea" NOT NULL,
40
+ "place" uuid,
41
+ CONSTRAINT "log_uses_mac_check" CHECK (octet_length("log_uses"."mac") = 16)
42
+ ) PARTITION BY RANGE ("at");
43
+ --> statement-breakpoint
44
+ -- The replay's index, and the only one: a unique index on a partitioned table must hold its partition key, so a use's number is unique by the sequence alone (the verifier checks it), and the checkpointer finds its window by `at`, from the floor (log_fence).
45
+ CREATE UNIQUE INDEX "log_uses_request_idx" ON "log_uses" USING btree ("at","request_id");
46
+ --> statement-breakpoint
47
+ CREATE TABLE "log_bulk" (
48
+ "principal" text NOT NULL,
49
+ "secret" uuid NOT NULL,
50
+ "last" bigint NOT NULL,
51
+ CONSTRAINT "log_bulk_pkey" PRIMARY KEY("principal","secret")
52
+ );
53
+ --> statement-breakpoint
54
+ CREATE TABLE "log_fence" (
55
+ "only_row" boolean PRIMARY KEY DEFAULT true NOT NULL,
56
+ "at_floor" bigint NOT NULL,
57
+ CONSTRAINT "log_fence_only_row_check" CHECK ("log_fence"."only_row")
58
+ );
59
+ --> statement-breakpoint
60
+ -- The floor starts below every use to come, by the deadline (deadline_max, below): the first barrier raises it.
61
+ INSERT INTO "log_fence" ("only_row", "at_floor") VALUES (true, (extract(epoch FROM clock_timestamp()) * 1000)::bigint - 3000);
62
+ --> statement-breakpoint
63
+ CREATE TABLE "log_checkpoints" (
64
+ "upto" bigint PRIMARY KEY NOT NULL,
65
+ "at" bigint NOT NULL,
66
+ "at_floor" bigint NOT NULL,
67
+ "changes_size" bigint NOT NULL,
68
+ "changes_root" "bytea" NOT NULL,
69
+ "changes_note" text NOT NULL,
70
+ "uses_size" bigint NOT NULL,
71
+ "uses_root" "bytea" NOT NULL,
72
+ "uses_note" text NOT NULL,
73
+ CONSTRAINT "log_checkpoints_changes_root_check" CHECK (octet_length("log_checkpoints"."changes_root") = 32),
74
+ CONSTRAINT "log_checkpoints_uses_root_check" CHECK (octet_length("log_checkpoints"."uses_root") = 32)
75
+ );
76
+ --> statement-breakpoint
77
+ -- The two trees' tiles: the changes tree's kept for good in one table; the uses tree's leaf tiles by leaf range (make_tiles), so that retention drops a table, and its levels above in one table (below).
78
+ CREATE TABLE "log_tiles" (
79
+ "tree" text NOT NULL,
80
+ "level" smallint NOT NULL,
81
+ "leaf" bigint NOT NULL,
82
+ "hashes" "bytea" NOT NULL,
83
+ CONSTRAINT "log_tiles_pkey" PRIMARY KEY("tree","level","leaf"),
84
+ CONSTRAINT "log_tiles_tree_check" CHECK ("log_tiles"."tree" IN ('changes', 'uses')),
85
+ CONSTRAINT "log_tiles_level_check" CHECK ("log_tiles"."level" BETWEEN 0 AND 7),
86
+ CONSTRAINT "log_tiles_leaf_check" CHECK ("log_tiles"."leaf" >= 0 AND "log_tiles"."leaf" % (256::numeric ^ ("log_tiles"."level" + 1)) = 0),
87
+ CONSTRAINT "log_tiles_hashes_check" CHECK (octet_length("log_tiles"."hashes") BETWEEN 32 AND 8192 AND octet_length("log_tiles"."hashes") % 32 = 0)
88
+ ) PARTITION BY LIST ("tree");
89
+ --> statement-breakpoint
90
+ CREATE TABLE "log_tiles_changes" PARTITION OF "log_tiles" FOR VALUES IN ('changes');
91
+ --> statement-breakpoint
92
+ -- The uses tree by level: its leaves' tiles (level 0) by leaf range, which retention drops whole (W212: 1.16 s against 13.8 s for a DELETE); the levels above, 1/256 of the bytes, in one table, where retention deletes the few tiles that end at or below the cut. A tile above level 0 starts low and spans far (2/0 covers 16.7M leaves), so a range of first leaves would drop one that still holds the cut (W217, #370's keptAbove).
93
+ CREATE TABLE "log_tiles_uses" PARTITION OF "log_tiles" FOR VALUES IN ('uses') PARTITION BY LIST ("level");
94
+ --> statement-breakpoint
95
+ CREATE TABLE "log_tiles_uses_leaves" PARTITION OF "log_tiles_uses" FOR VALUES IN (0) PARTITION BY RANGE ("leaf");
96
+ --> statement-breakpoint
97
+ CREATE TABLE "log_tiles_uses_upper" PARTITION OF "log_tiles_uses" FOR VALUES IN (1, 2, 3, 4, 5, 6, 7);
98
+ --> statement-breakpoint
99
+ -- The seals, null until 0.15's cutover seals each row: one on each table an object of W216's lives in, a member's covering its grants, a secret's its current version and a reference's source. Adding a nullable column rewrites nothing; the checks are NOT VALID so that adding them scans nothing either (every row is null, and every new row is checked).
100
+ ALTER TABLE "credentials" ADD COLUMN "seal" "bytea";--> statement-breakpoint
101
+ ALTER TABLE "environments" ADD COLUMN "seal" "bytea";--> statement-breakpoint
102
+ -- Whether the environment is protected: 0.14 keeps it in the policy's entries, 0.15 on the environment's object (W216). Null until the cutover writes it.
103
+ ALTER TABLE "environments" ADD COLUMN "protected" boolean;--> statement-breakpoint
104
+ ALTER TABLE "secrets" ADD COLUMN "seal" "bytea";--> statement-breakpoint
105
+ ALTER TABLE "service_bindings" ADD COLUMN "seal" "bytea";--> statement-breakpoint
106
+ ALTER TABLE "vault_members" ADD COLUMN "seal" "bytea";--> statement-breakpoint
107
+ ALTER TABLE "credentials" ADD CONSTRAINT "credentials_seal_check" CHECK ("credentials"."seal" IS NULL OR octet_length("credentials"."seal") = 16) NOT VALID;--> statement-breakpoint
108
+ ALTER TABLE "environments" ADD CONSTRAINT "environments_seal_check" CHECK ("environments"."seal" IS NULL OR octet_length("environments"."seal") = 16) NOT VALID;--> statement-breakpoint
109
+ ALTER TABLE "secrets" ADD CONSTRAINT "secrets_seal_check" CHECK ("secrets"."seal" IS NULL OR octet_length("secrets"."seal") = 16) NOT VALID;--> statement-breakpoint
110
+ ALTER TABLE "service_bindings" ADD CONSTRAINT "service_bindings_seal_check" CHECK ("service_bindings"."seal" IS NULL OR octet_length("service_bindings"."seal") = 16) NOT VALID;--> statement-breakpoint
111
+ ALTER TABLE "vault_members" ADD CONSTRAINT "vault_members_seal_check" CHECK ("vault_members"."seal" IS NULL OR octet_length("vault_members"."seal") = 16) NOT VALID;--> statement-breakpoint
112
+ -- An object's key in the log: the first 16 bytes of SHA-256('coffre.object.v1' 0x00 kind 0x00 id), read as a uuid, which the vault computes the same way. Kinds (W216's objectKey, in @coffre/vault): principal (a member, by its principal), credential (a token or a binding, by its id), secret, environment (each by its id), roles and policy (id 'instance'). Plain SQL, so that each probe inlines it, and STABLE as convert_to is: declared IMMUTABLE, it would not be inlined, and a statement would call it as a function of its own, 32 µs a call against 3.7 (measured).
113
+ CREATE FUNCTION log_object(p_kind text, p_id text) RETURNS uuid
114
+ LANGUAGE sql STABLE PARALLEL SAFE AS $$
115
+ SELECT pg_catalog.encode(pg_catalog.substr(pg_catalog.sha256(
116
+ '\x636f666672652e6f626a6563742e763100'::bytea || pg_catalog.convert_to(p_kind, 'UTF8') || '\x00'::bytea || pg_catalog.convert_to(p_id, 'UTF8')
117
+ ), 1, 16), 'hex')::uuid
118
+ $$;
119
+ --> statement-breakpoint
120
+ -- The longest a use may take, from its `at` to its deadline, in ms (R53, R54): sealing trusts that nothing stamped longer ago still arrives.
121
+ CREATE FUNCTION deadline_max() RETURNS bigint
122
+ LANGUAGE sql IMMUTABLE PARALLEL SAFE AS 'SELECT 3000::bigint';
123
+ --> statement-breakpoint
124
+ -- A day of uses, [day, day + 1) in ms since the epoch, UTC, made ahead of time: a use at a time no day holds fails. Made apart, then attached: CREATE TABLE … PARTITION OF takes the parent ACCESS EXCLUSIVE and queues every use behind whatever reads it (10.7 s measured at 10M), ATTACH takes SHARE UPDATE EXCLUSIVE, which inserts do not wait for (12 ms).
125
+ CREATE FUNCTION make_day(p_day date) RETURNS text
126
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp AS $$
127
+ DECLARE
128
+ name text := 'log_uses_' || to_char(p_day, 'YYYYMMDD');
129
+ lo bigint := (p_day - date '1970-01-01')::bigint * 86400000;
130
+ BEGIN
131
+ -- A day made already, by its bound: a table of that name that is not that day is no day.
132
+ IF to_regclass('public.' || quote_ident(name)) IS NOT NULL THEN
133
+ IF NOT EXISTS (
134
+ SELECT 1 FROM pg_inherits i JOIN pg_class c ON c.oid = i.inhrelid
135
+ WHERE i.inhparent = 'public.log_uses'::regclass AND c.oid = to_regclass('public.' || quote_ident(name))
136
+ AND pg_get_expr(c.relpartbound, c.oid) = format('FOR VALUES FROM (%L) TO (%L)', lo, lo + 86400000)
137
+ ) THEN
138
+ RAISE EXCEPTION '% exists, and is not the uses of %', name, p_day;
139
+ END IF;
140
+ RETURN name;
141
+ END IF;
142
+ BEGIN
143
+ EXECUTE format('CREATE TABLE public.%I (LIKE public.log_uses INCLUDING DEFAULTS INCLUDING CONSTRAINTS)', name);
144
+ EXECUTE format('CREATE UNIQUE INDEX ON public.%I (at, request_id)', name);
145
+ -- pg_dump reads each partition as a table of its own.
146
+ EXECUTE format('GRANT SELECT ON public.%I TO coffre_backup', name);
147
+ EXECUTE format('ALTER TABLE public.log_uses ATTACH PARTITION public.%I FOR VALUES FROM (%s) TO (%s)', name, lo, lo + 86400000);
148
+ EXCEPTION WHEN duplicate_table OR unique_violation THEN
149
+ -- Another call made it meanwhile, and committed: it is checked as any day found.
150
+ RETURN public.make_day(p_day);
151
+ END;
152
+ RETURN name;
153
+ END
154
+ $$;
155
+ --> statement-breakpoint
156
+ -- A range of the uses tree's leaf tiles (level 0), those whose first leaf is in [p_from, p_to), made apart and attached as make_day makes a day. What ranges, and when, is the checkpointer's.
157
+ CREATE FUNCTION make_tiles(p_from bigint, p_to bigint) RETURNS text
158
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp AS $$
159
+ DECLARE
160
+ name text := 'log_tiles_uses_' || p_from;
161
+ BEGIN
162
+ IF p_from < 0 OR p_to <= p_from THEN RAISE EXCEPTION 'no leaves in [%, %)', p_from, p_to; END IF;
163
+ IF to_regclass('public.' || quote_ident(name)) IS NOT NULL THEN
164
+ IF NOT EXISTS (
165
+ SELECT 1 FROM pg_inherits i JOIN pg_class c ON c.oid = i.inhrelid
166
+ WHERE i.inhparent = 'public.log_tiles_uses_leaves'::regclass AND c.oid = to_regclass('public.' || quote_ident(name))
167
+ AND pg_get_expr(c.relpartbound, c.oid) = format('FOR VALUES FROM (%L) TO (%L)', p_from, p_to)
168
+ ) THEN
169
+ RAISE EXCEPTION '% exists, and does not hold the tiles of [%, %)', name, p_from, p_to;
170
+ END IF;
171
+ RETURN name;
172
+ END IF;
173
+ BEGIN
174
+ EXECUTE format('CREATE TABLE public.%I (LIKE public.log_tiles_uses_leaves INCLUDING DEFAULTS INCLUDING CONSTRAINTS)', name);
175
+ EXECUTE format('ALTER TABLE public.%I ADD PRIMARY KEY (tree, level, leaf)', name);
176
+ EXECUTE format('GRANT SELECT ON public.%I TO coffre_backup', name);
177
+ EXECUTE format('ALTER TABLE public.log_tiles_uses_leaves ATTACH PARTITION public.%I FOR VALUES FROM (%s) TO (%s)', name, p_from, p_to);
178
+ EXCEPTION WHEN duplicate_table OR unique_violation THEN
179
+ RETURN public.make_tiles(p_from, p_to);
180
+ END;
181
+ RETURN name;
182
+ END
183
+ $$;
184
+ --> statement-breakpoint
185
+ -- What a use reads, as use_credential returns it. `state` is null when it did its work; otherwise it did nothing, and the rest is to be ignored: 'late' (past its deadline, or below the floor), 'ahead' (stamped more than a minute past the database's clock), 'unconfigured' (no policy), 'reprobe' and 'lock' never leave the function. `inputs` is the newest change of any of its inputs; each `*_seal` is a row's, each `*_last` its object's newest change's (null in the cached form, where the isolate holds them).
186
+ CREATE TYPE use_view AS (
187
+ state text, number bigint, covered bigint, principal text, newest bigint, newest_mac bytea, inputs bigint,
188
+ c_kind text, c_generation integer, c_scope text, c_places text, c_constraints text, c_revoked boolean, c_expires bigint, c_not_after bigint, c_seal bytea, c_last bytea,
189
+ m_status text, m_role text, m_scope text, m_generation integer, grants json, m_seal bytea, m_last bytea,
190
+ s_environment uuid, s_archived boolean, s_version uuid, s_seal bytea, s_last bytea,
191
+ v_envelope integer, v_ciphertext bytea, v_iv bytea, v_auth_tag bytea, v_wrapped_dek bytea, v_kek_provider text, v_kek_id text, v_kek_version text, v_sensitive boolean,
192
+ e_project uuid, e_parent uuid, e_owner text, e_archived boolean, e_deleted boolean, e_protected boolean, e_seal bytea, e_last bytea,
193
+ cap_count integer, cap_window bigint, policy_seal bytea, policy_last bytea,
194
+ in_set boolean, others integer
195
+ );
196
+ --> statement-breakpoint
197
+ -- A use (hot-path-one.md, "A use"; formal/HotPathOne.tla): the fence shared, then one statement, with a snapshot of its own taken after the lock (each of a volatile function's statements takes a new one under READ COMMITTED), that reads every input the decision needs and inserts the entry, numbered now. The lock goes with the commit. The Worker decides on what it returns; the log records the request, not the outcome.
198
+ --
199
+ -- The deadline: a call that comes after it, or with a deadline more than deadline_max() after its `at`, or an `at` below the floor (or no floor row), writes nothing and answers 'late'; one that runs past it, waiting for its principal's lock say, rolls back (57014). A replay of a call that wrote fails on (at, request_id), 23505: deciding again on today's state could allow, after a revocation, a use the log has before it. (Not ON CONFLICT: its speculative insertion doubles a use's buffers and WAL, measured.)
200
+ --
201
+ -- The bulk cap (R28, R52), its count and window the policy object's newest change's, read in the statement's own snapshot (CapSealed, CapFromPolicy): a policy row put back cannot move the cap, and a use before the policy is logged, or with a policy that names no count or window, answers 'unconfigured' and writes nothing. The cap's row moves for every request it admits, whatever the seals say: the Worker refuses a stale use after (HotPathOneStaleBulk), and the replay counts it the same. A key is in the principal's set at t if its log_bulk row's `last` > t - window - refresh, with refresh a minute or a tenth of the window. A key in the set passes; one not in it passes if fewer than count others are (counted over a deadline more: uses are decided in number order, not by `at`), and enters. A row older than t - refresh moves on to t. Every request counts, allowed or refused for any reason but the cap. Both writes are made under the principal's own lock: a use that may write takes it and reads again, so the writes come in number order and the replay decides each the same.
202
+ --
203
+ -- Covers (p_cover, with its entry's at and request id): a repeat of an allowed use this isolate wrote, of the same credential and secret, with no change to any input numbered after it and the key's bulk row fresh, writes nothing; the Worker decides on what was read. The cover's entry must still be in the log.
204
+ --
205
+ -- The seal cache (p_known, p_known_mac): if the log's newest change is still exactly the one the isolate read its seals at, by number and MAC, no input changed since, and the statement probes nothing. Otherwise it writes nothing, answers 'reprobe' inside the function, and the probing statement runs. Two statements, not one with its probes skipped: a probe's plan nodes cost most of it even when it does not run (measured).
206
+ --
207
+ -- Five inputs, each one object (W216's): the credential, its principal (the member, with its grants, under the member's seal), the secret (with its current version, under the secret's seal), its environment (whose `deleted` says its project's too), and the policy (the cap). Roles, the environment asked at (`place`) and a reference's source are read by the 0.15 vault's statement when it is built (plan, PR 5).
208
+ CREATE FUNCTION use_credential(
209
+ p_credential uuid, p_secret uuid, p_request bigint, p_at bigint, p_purpose smallint, p_place uuid, p_mac bytea, p_deadline bigint,
210
+ p_cover bigint DEFAULT NULL, p_cover_at bigint DEFAULT NULL, p_cover_request bigint DEFAULT NULL, p_known bigint DEFAULT NULL, p_known_mac bytea DEFAULT NULL
211
+ ) RETURNS use_view
212
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp AS $$
213
+ DECLARE
214
+ r public.use_view;
215
+ locked boolean := false;
216
+ probe boolean := p_known IS NULL;
217
+ BEGIN
218
+ IF p_deadline IS NULL OR p_deadline > p_at + public.deadline_max() THEN r.state := 'late'; RETURN r; END IF;
219
+ -- A use stamped more than a minute past the database's clock: its Worker's clock is ahead. Written, it would lie
220
+ -- beyond where a proof looks for it from its window (the uses have no index by number), and the honest uses
221
+ -- after it would read as changed. Checked here, at no cost, and again in the statement that writes, in case the
222
+ -- database's clock stepped back meanwhile (ClockBack).
223
+ IF p_at > (extract(epoch FROM clock_timestamp()) * 1000)::bigint + 60000 THEN r.state := 'ahead'; RETURN r; END IF;
224
+ PERFORM pg_advisory_xact_lock_shared(1668244070, 0);
225
+ LOOP
226
+ IF probe THEN
227
+ WITH n AS MATERIALIZED (
228
+ SELECT x.number AS newest, x.mac AS newest_mac, coalesce(x.number = p_known AND x.mac = p_known_mac, false) AS cached
229
+ FROM public.log_changes x ORDER BY x.number DESC LIMIT 1
230
+ ),
231
+ -- The rows, and each input's object key, computed once: a key in a probe's own condition is computed again for every row the probe reads.
232
+ i AS MATERIALIZED (
233
+ SELECT n.newest, n.newest_mac, coalesce(n.cached, false) AS cached,
234
+ f.at_floor, cap.count AS cap_count, cap.window_ms AS cap_window, cap.refresh AS cap_refresh, b.last AS bulk_last,
235
+ c.principal AS p, c.kind AS c_kind, c.generation AS c_generation, c.scope AS c_scope, c.places AS c_places, c.constraints AS c_constraints,
236
+ c.revoked_at IS NOT NULL AS c_revoked, (extract(epoch FROM c.expires_at) * 1000)::bigint AS c_expires,
237
+ (extract(epoch FROM c.not_after) * 1000)::bigint AS c_not_after, c.seal AS c_seal,
238
+ m.status AS m_status, m.role AS m_role, m.scope AS m_scope, m.generation AS m_generation, m.seal AS m_seal,
239
+ s.environment_id AS s_environment, s.archived_at IS NOT NULL AS s_archived, s.current_version_id AS s_version, s.seal AS s_seal,
240
+ sv.envelope_version, sv.ciphertext, sv.iv, sv.auth_tag, sv.wrapped_dek, sv.kek_provider, sv.kek_id, sv.kek_version, sv.sensitive,
241
+ e.project_id AS e_project, e.parent_id AS e_parent, e.owner AS e_owner, e.archived_at IS NOT NULL AS e_archived, strpos(e.slug, '~') > 0 AS e_deleted,
242
+ e.protected AS e_protected, e.seal AS e_seal,
243
+ pol.seal AS policy_seal, lpol.number AS policy_number, lpol.seal AS policy_last,
244
+ public.log_object('credential', p_credential::text) AS k_credential,
245
+ public.log_object('principal', c.principal) AS k_member,
246
+ public.log_object('secret', p_secret::text) AS k_secret,
247
+ public.log_object('environment', s.environment_id::text) AS k_environment
248
+ FROM (SELECT 1) one
249
+ LEFT JOIN n ON true
250
+ LEFT JOIN public.log_fence f ON f.only_row
251
+ LEFT JOIN public.log_config pol ON pol.name = 'policy'
252
+ -- The cap as the policy's newest change says it, not as its row does (CapSealed): every policy change logs the whole policy as its `value`.
253
+ LEFT JOIN LATERAL (
254
+ SELECT x.number, x.seal, x.payload FROM public.log_changes x WHERE x.object = public.log_object('policy', 'instance') ORDER BY x.number DESC LIMIT 1
255
+ ) lpol ON true
256
+ LEFT JOIN LATERAL (
257
+ SELECT (lpol.payload::jsonb #>> '{value,bulk,count}')::integer AS count, (lpol.payload::jsonb #>> '{value,bulk,windowMs}')::bigint AS window_ms
258
+ ) cap0 ON true
259
+ LEFT JOIN LATERAL (SELECT cap0.count, cap0.window_ms, least(60000, cap0.window_ms / 10) AS refresh) cap ON true
260
+ LEFT JOIN public.credentials c ON c.id = p_credential
261
+ LEFT JOIN public.vault_members m ON m.principal = c.principal
262
+ LEFT JOIN public.secrets s ON s.id = p_secret
263
+ LEFT JOIN public.secret_versions sv ON sv.id = s.current_version_id
264
+ LEFT JOIN public.environments e ON e.id = s.environment_id
265
+ LEFT JOIN public.log_bulk b ON b.principal = c.principal AND b.secret = p_secret
266
+ ),
267
+ v AS MATERIALIZED (
268
+ SELECT i.*,
269
+ CASE WHEN i.at_floor IS NULL OR p_at < i.at_floor OR (extract(epoch FROM clock_timestamp()) * 1000)::bigint > p_deadline THEN 'late'
270
+ WHEN p_at > (extract(epoch FROM clock_timestamp()) * 1000)::bigint + 60000 THEN 'ahead'
271
+ WHEN i.cap_count IS NULL OR i.cap_window IS NULL THEN 'unconfigured'
272
+ WHEN i.p IS NOT NULL AND NOT locked AND coalesce(i.bulk_last <= p_at - i.cap_refresh, true) THEN 'lock' END AS state,
273
+ greatest(lc.number, lm.number, ls.number, le.number, i.policy_number) AS inputs,
274
+ lc.seal AS c_last, lm.seal AS m_last, ls.seal AS s_last, le.seal AS e_last,
275
+ p_cover IS NOT NULL AND i.bulk_last > p_at - i.cap_refresh
276
+ AND EXISTS (SELECT 1 FROM public.log_uses u WHERE u.at = p_cover_at AND u.request_id = p_cover_request AND u.number = p_cover AND u.credential = p_credential AND u.secret = p_secret)
277
+ AND p_cover > coalesce(greatest(lc.number, lm.number, ls.number, le.number, i.policy_number), 0) AS is_covered,
278
+ coalesce(i.bulk_last > p_at - i.cap_window - i.cap_refresh, false) AS is_in,
279
+ coalesce(i.bulk_last <= p_at - i.cap_refresh, true) AS stale,
280
+ -- The other keys in the set, up to the cap: read only under the principal's lock, for a key not in it.
281
+ CASE WHEN locked AND i.p IS NOT NULL AND NOT coalesce(i.bulk_last > p_at - i.cap_window - i.cap_refresh, false) THEN (
282
+ SELECT count(*)::integer FROM (
283
+ SELECT 1 FROM public.log_bulk o
284
+ WHERE o.principal = i.p AND o.secret <> p_secret AND o.last > p_at - i.cap_window - i.cap_refresh - public.deadline_max()
285
+ LIMIT i.cap_count
286
+ ) x
287
+ ) ELSE 0 END AS n_others,
288
+ i.cap_count AS n_cap
289
+ FROM i
290
+ -- Each input's newest change, unless the isolate's seals are known current (the other form).
291
+ LEFT JOIN LATERAL (SELECT x.number, x.seal FROM public.log_changes x WHERE x.object = i.k_credential ORDER BY x.number DESC LIMIT 1) lc ON true
292
+ LEFT JOIN LATERAL (SELECT x.number, x.seal FROM public.log_changes x WHERE x.object = i.k_member ORDER BY x.number DESC LIMIT 1) lm ON true
293
+ LEFT JOIN LATERAL (SELECT x.number, x.seal FROM public.log_changes x WHERE x.object = i.k_secret ORDER BY x.number DESC LIMIT 1) ls ON true
294
+ LEFT JOIN LATERAL (SELECT x.number, x.seal FROM public.log_changes x WHERE x.object = i.k_environment ORDER BY x.number DESC LIMIT 1) le ON true
295
+ ),
296
+ w AS (
297
+ INSERT INTO public.log_uses (number, at, request_id, credential, secret, purpose, mac, place)
298
+ SELECT nextval('public.log_number'), p_at, p_request, p_credential, p_secret, p_purpose, p_mac, p_place
299
+ FROM v WHERE v.state IS NULL AND NOT v.is_covered
300
+ RETURNING log_uses.number
301
+ ),
302
+ k AS (
303
+ INSERT INTO public.log_bulk (principal, secret, last)
304
+ SELECT v.p, p_secret, p_at FROM v
305
+ WHERE v.state IS NULL AND v.p IS NOT NULL AND NOT v.is_covered AND v.stale AND (v.is_in OR v.n_others < v.n_cap)
306
+ ON CONFLICT ON CONSTRAINT log_bulk_pkey DO UPDATE SET last = EXCLUDED.last
307
+ )
308
+ SELECT v.state, (SELECT w.number FROM w), CASE WHEN v.is_covered THEN p_cover END, v.p, v.newest, v.newest_mac, CASE WHEN NOT v.cached THEN v.inputs END,
309
+ v.c_kind, v.c_generation, v.c_scope, v.c_places, v.c_constraints, v.c_revoked, v.c_expires, v.c_not_after, v.c_seal, v.c_last,
310
+ v.m_status, v.m_role, v.m_scope, v.m_generation,
311
+ (SELECT json_agg(json_build_array(g.project_id, g.environment_id, g.environment_slug, g.role, g.expires_at)) FROM public.vault_grants g WHERE g.principal = v.p),
312
+ v.m_seal, v.m_last,
313
+ v.s_environment, v.s_archived, v.s_version, v.s_seal, v.s_last,
314
+ v.envelope_version, v.ciphertext, v.iv, v.auth_tag, v.wrapped_dek, v.kek_provider, v.kek_id, v.kek_version, v.sensitive,
315
+ v.e_project, v.e_parent, v.e_owner, v.e_archived, v.e_deleted, v.e_protected, v.e_seal, v.e_last,
316
+ v.cap_count, v.cap_window, v.policy_seal, v.policy_last,
317
+ v.is_in, v.n_others
318
+ INTO r FROM v;
319
+ ELSE
320
+ -- The isolate's seals: no probe, only whether a change came since (then nothing is written: 'reprobe').
321
+ WITH n AS MATERIALIZED (
322
+ SELECT x.number AS newest, x.mac AS newest_mac, coalesce(x.number = p_known AND x.mac = p_known_mac, false) AS cached
323
+ FROM public.log_changes x ORDER BY x.number DESC LIMIT 1
324
+ ),
325
+ v AS MATERIALIZED (
326
+ SELECT n.newest, n.newest_mac, coalesce(n.cached, false) AS cached,
327
+ CASE WHEN NOT coalesce(n.cached, false) THEN 'reprobe'
328
+ WHEN f.at_floor IS NULL OR p_at < f.at_floor OR (extract(epoch FROM clock_timestamp()) * 1000)::bigint > p_deadline THEN 'late'
329
+ WHEN p_at > (extract(epoch FROM clock_timestamp()) * 1000)::bigint + 60000 THEN 'ahead'
330
+ WHEN cap.count IS NULL OR cap.window_ms IS NULL THEN 'unconfigured'
331
+ WHEN c.principal IS NOT NULL AND NOT locked AND coalesce(b.last <= p_at - cap.refresh, true) THEN 'lock' END AS state,
332
+ c.principal AS p, c.kind AS c_kind, c.generation AS c_generation, c.scope AS c_scope, c.places AS c_places, c.constraints AS c_constraints,
333
+ c.revoked_at IS NOT NULL AS c_revoked, (extract(epoch FROM c.expires_at) * 1000)::bigint AS c_expires,
334
+ (extract(epoch FROM c.not_after) * 1000)::bigint AS c_not_after, c.seal AS c_seal,
335
+ m.status AS m_status, m.role AS m_role, m.scope AS m_scope, m.generation AS m_generation, m.seal AS m_seal,
336
+ s.environment_id AS s_environment, s.archived_at IS NOT NULL AS s_archived, s.current_version_id AS s_version, s.seal AS s_seal,
337
+ sv.envelope_version, sv.ciphertext, sv.iv, sv.auth_tag, sv.wrapped_dek, sv.kek_provider, sv.kek_id, sv.kek_version, sv.sensitive,
338
+ e.project_id AS e_project, e.parent_id AS e_parent, e.owner AS e_owner, e.archived_at IS NOT NULL AS e_archived, strpos(e.slug, '~') > 0 AS e_deleted,
339
+ e.protected AS e_protected, e.seal AS e_seal,
340
+ cap.count AS cap_count, cap.window_ms AS cap_window, pol.seal AS policy_seal,
341
+ -- The isolate checked its cover against the seals' numbers when it read them.
342
+ p_cover IS NOT NULL AND b.last > p_at - cap.refresh
343
+ AND EXISTS (SELECT 1 FROM public.log_uses u WHERE u.at = p_cover_at AND u.request_id = p_cover_request AND u.number = p_cover AND u.credential = p_credential AND u.secret = p_secret) AS is_covered,
344
+ coalesce(b.last > p_at - cap.window_ms - cap.refresh, false) AS is_in,
345
+ coalesce(b.last <= p_at - cap.refresh, true) AS stale,
346
+ CASE WHEN locked AND c.principal IS NOT NULL AND NOT coalesce(b.last > p_at - cap.window_ms - cap.refresh, false) THEN (
347
+ SELECT count(*)::integer FROM (
348
+ SELECT 1 FROM public.log_bulk o
349
+ WHERE o.principal = c.principal AND o.secret <> p_secret AND o.last > p_at - cap.window_ms - cap.refresh - public.deadline_max()
350
+ LIMIT cap.count
351
+ ) x
352
+ ) ELSE 0 END AS n_others,
353
+ cap.count AS n_cap
354
+ FROM (SELECT 1) one
355
+ LEFT JOIN n ON true
356
+ LEFT JOIN public.log_fence f ON f.only_row
357
+ LEFT JOIN public.log_config pol ON pol.name = 'policy'
358
+ -- The cap as the policy's newest change says it, not as its row does (CapSealed): every policy change logs the whole policy as its `value`.
359
+ LEFT JOIN LATERAL (
360
+ SELECT x.number, x.seal, x.payload FROM public.log_changes x WHERE x.object = public.log_object('policy', 'instance') ORDER BY x.number DESC LIMIT 1
361
+ ) lpol ON true
362
+ LEFT JOIN LATERAL (
363
+ SELECT (lpol.payload::jsonb #>> '{value,bulk,count}')::integer AS count, (lpol.payload::jsonb #>> '{value,bulk,windowMs}')::bigint AS window_ms
364
+ ) cap0 ON true
365
+ LEFT JOIN LATERAL (SELECT cap0.count, cap0.window_ms, least(60000, cap0.window_ms / 10) AS refresh) cap ON true
366
+ LEFT JOIN public.credentials c ON c.id = p_credential
367
+ LEFT JOIN public.vault_members m ON m.principal = c.principal
368
+ LEFT JOIN public.secrets s ON s.id = p_secret
369
+ LEFT JOIN public.secret_versions sv ON sv.id = s.current_version_id
370
+ LEFT JOIN public.environments e ON e.id = s.environment_id
371
+ LEFT JOIN public.log_bulk b ON b.principal = c.principal AND b.secret = p_secret
372
+ ),
373
+ w AS (
374
+ INSERT INTO public.log_uses (number, at, request_id, credential, secret, purpose, mac, place)
375
+ SELECT nextval('public.log_number'), p_at, p_request, p_credential, p_secret, p_purpose, p_mac, p_place
376
+ FROM v WHERE v.state IS NULL AND NOT v.is_covered
377
+ RETURNING log_uses.number
378
+ ),
379
+ k AS (
380
+ INSERT INTO public.log_bulk (principal, secret, last)
381
+ SELECT v.p, p_secret, p_at FROM v
382
+ WHERE v.state IS NULL AND v.p IS NOT NULL AND NOT v.is_covered AND v.stale AND (v.is_in OR v.n_others < v.n_cap)
383
+ ON CONFLICT ON CONSTRAINT log_bulk_pkey DO UPDATE SET last = EXCLUDED.last
384
+ )
385
+ SELECT v.state, (SELECT w.number FROM w), CASE WHEN v.is_covered THEN p_cover END, v.p, v.newest, v.newest_mac, NULL::bigint,
386
+ v.c_kind, v.c_generation, v.c_scope, v.c_places, v.c_constraints, v.c_revoked, v.c_expires, v.c_not_after, v.c_seal, NULL::bytea,
387
+ v.m_status, v.m_role, v.m_scope, v.m_generation,
388
+ (SELECT json_agg(json_build_array(g.project_id, g.environment_id, g.environment_slug, g.role, g.expires_at)) FROM public.vault_grants g WHERE g.principal = v.p),
389
+ v.m_seal, NULL::bytea,
390
+ v.s_environment, v.s_archived, v.s_version, v.s_seal, NULL::bytea,
391
+ v.envelope_version, v.ciphertext, v.iv, v.auth_tag, v.wrapped_dek, v.kek_provider, v.kek_id, v.kek_version, v.sensitive,
392
+ v.e_project, v.e_parent, v.e_owner, v.e_archived, v.e_deleted, v.e_protected, v.e_seal, NULL::bytea,
393
+ v.cap_count, v.cap_window, v.policy_seal, NULL::bytea,
394
+ v.is_in, v.n_others
395
+ INTO r FROM v;
396
+ END IF;
397
+ IF r.state = 'reprobe' THEN probe := true; CONTINUE; END IF;
398
+ EXIT WHEN r.state IS DISTINCT FROM 'lock';
399
+ -- Its key's bulk row is stale or missing: this use may write it, so it takes its principal's lock and reads again.
400
+ PERFORM pg_advisory_xact_lock(1668244066, hashtext(r.principal));
401
+ locked := true;
402
+ END LOOP;
403
+ -- The deadline holds in Postgres to the end: a call that ran past it rolls back and writes nothing. The hot-path login's lock_timeout and statement_timeout bound the waits.
404
+ IF r.state IS NULL AND (extract(epoch FROM clock_timestamp()) * 1000)::bigint > p_deadline THEN
405
+ RAISE EXCEPTION 'past its deadline' USING ERRCODE = '57014';
406
+ END IF;
407
+ RETURN r;
408
+ END
409
+ $$;
410
+ --> statement-breakpoint
411
+ -- A change (hot-path-one.md, "A change"): the fence exclusive, so that no use overlaps it; a retry finds its entry and answers its number (a request id names one change: another under it is refused); a row that is not its object's newest change's is refused, as a use refuses it (ChangeChecksSeal: else a row put back from before a change, validly sealed once, would be sealed again under the next change, and serve what the log says was taken away); the object's row moves only from the seal the vault read (ChangeCAS: of two changes to one object, the second finds another seal and does nothing: null); then its entry, numbered now. It answers the number, which a change waits to see sealed (R51).
412
+ --
413
+ -- This schema knows one op, `genesis`: the object's row sealed as it is, or, for the policy and the roles, made from the payload's `value`. That is the cutover's (plan, PR 9); the ops that change what a row says come with the 0.15 change path (PR 4), each applied as its payload says.
414
+ CREATE FUNCTION change_input(
415
+ p_kind text, p_id text, p_request text, p_at bigint, p_actor text, p_payload text, p_seal_from bytea, p_seal bytea, p_mac bytea
416
+ ) RETURNS bigint
417
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp AS $$
418
+ DECLARE
419
+ op jsonb := p_payload::jsonb;
420
+ target uuid := public.log_object(p_kind, p_id);
421
+ n bigint;
422
+ done integer;
423
+ member text;
424
+ held bytea;
425
+ newest bytea;
426
+ BEGIN
427
+ -- Its locks in the order 0.14's writers take theirs, so that none waits on the other in a circle until 0.16 (row,
428
+ -- head, fence): the member's queue (MEMBER_QUEUE, 0x636f666d) and row first, a principal's own or a credential's
429
+ -- (0.14's vault locks them before the head, lockMembers; a change that fences 0.14 too moves the member's
430
+ -- access_seq, plan PR 9), then 0.14's head (its app takes it before its rows), then the fence (0.14's appends
431
+ -- take it after the head, audit_log_fence). The head held, no 0.14 write lands between this change and its entry
432
+ -- (ChangeHeadFirst).
433
+ member := CASE p_kind
434
+ WHEN 'principal' THEN p_id
435
+ WHEN 'credential' THEN coalesce(
436
+ (SELECT c.principal FROM public.credentials c WHERE c.id = p_id::uuid),
437
+ (SELECT b.principal FROM public.service_bindings b WHERE b.id = p_id::uuid))
438
+ END;
439
+ IF member IS NOT NULL THEN
440
+ PERFORM pg_advisory_xact_lock(1668245101, hashtext(m.principal)) FROM public.vault_members m WHERE m.principal = member FOR NO KEY UPDATE;
441
+ END IF;
442
+ PERFORM 1 FROM public.audit_chain_head WHERE only_row FOR UPDATE;
443
+ PERFORM pg_advisory_xact_lock(1668244070, 0);
444
+ SELECT c.number INTO n FROM public.log_changes c WHERE c.request_id = p_request;
445
+ IF FOUND THEN
446
+ IF NOT EXISTS (
447
+ SELECT 1 FROM public.log_changes c
448
+ WHERE c.request_id = p_request AND c.object = target AND c.payload = p_payload AND c.seal IS NOT DISTINCT FROM p_seal
449
+ ) THEN
450
+ RAISE EXCEPTION 'request % names another change', p_request USING ERRCODE = 'unique_violation';
451
+ END IF;
452
+ RETURN n;
453
+ END IF;
454
+ IF op ->> 'op' IS DISTINCT FROM 'genesis' THEN
455
+ RAISE EXCEPTION 'this schema knows no change %', op ->> 'op' USING ERRCODE = 'invalid_parameter_value';
456
+ END IF;
457
+ -- ChangeChecksSeal: the row's seal is its object's newest change's, or the object has neither yet (one probe, as
458
+ -- a use makes). A row deleted outright, or a seal no change wrote, is stale too.
459
+ held := CASE p_kind
460
+ WHEN 'principal' THEN (SELECT m.seal FROM public.vault_members m WHERE m.principal = p_id)
461
+ WHEN 'credential' THEN coalesce(
462
+ (SELECT c.seal FROM public.credentials c WHERE c.id = p_id::uuid),
463
+ (SELECT b.seal FROM public.service_bindings b WHERE b.id = p_id::uuid))
464
+ WHEN 'environment' THEN (SELECT e.seal FROM public.environments e WHERE e.id = p_id::uuid)
465
+ WHEN 'secret' THEN (SELECT x.seal FROM public.secrets x WHERE x.id = p_id::uuid)
466
+ WHEN 'policy' THEN (SELECT g.seal FROM public.log_config g WHERE g.name = 'policy')
467
+ WHEN 'roles' THEN (SELECT g.seal FROM public.log_config g WHERE g.name = 'roles')
468
+ END;
469
+ newest := (SELECT c.seal FROM public.log_changes c WHERE c.object = target ORDER BY c.number DESC LIMIT 1);
470
+ IF held IS DISTINCT FROM newest THEN
471
+ RAISE EXCEPTION 'the % object''s row is not its newest change''s: stale', p_kind USING ERRCODE = 'data_corrupted';
472
+ END IF;
473
+ CASE p_kind
474
+ WHEN 'principal' THEN
475
+ UPDATE public.vault_members SET seal = p_seal WHERE principal = p_id AND seal IS NOT DISTINCT FROM p_seal_from;
476
+ WHEN 'credential' THEN
477
+ -- A token's row, or a trust binding's: one kind of object, ids that never meet.
478
+ UPDATE public.credentials SET seal = p_seal WHERE id = p_id::uuid AND seal IS NOT DISTINCT FROM p_seal_from;
479
+ GET DIAGNOSTICS done = ROW_COUNT;
480
+ IF done = 0 THEN
481
+ UPDATE public.service_bindings SET seal = p_seal WHERE id = p_id::uuid AND seal IS NOT DISTINCT FROM p_seal_from;
482
+ END IF;
483
+ WHEN 'environment' THEN
484
+ UPDATE public.environments SET seal = p_seal WHERE id = p_id::uuid AND seal IS NOT DISTINCT FROM p_seal_from;
485
+ WHEN 'secret' THEN
486
+ UPDATE public.secrets SET seal = p_seal WHERE id = p_id::uuid AND seal IS NOT DISTINCT FROM p_seal_from;
487
+ WHEN 'policy', 'roles' THEN
488
+ IF p_id <> 'instance' OR p_seal_from IS NOT NULL OR op -> 'value' IS NULL THEN
489
+ RAISE EXCEPTION 'the % object is one, made once from its value', p_kind USING ERRCODE = 'invalid_parameter_value';
490
+ END IF;
491
+ INSERT INTO public.log_config (name, value, seal) VALUES (p_kind, (op -> 'value')::text, p_seal) ON CONFLICT (name) DO NOTHING;
492
+ ELSE
493
+ RAISE EXCEPTION 'no object kind %', p_kind USING ERRCODE = 'invalid_parameter_value';
494
+ END CASE;
495
+ -- A row gone, a row whose seal is no longer the one read, or an object made already: nothing was done.
496
+ GET DIAGNOSTICS done = ROW_COUNT;
497
+ IF done = 0 THEN RETURN NULL; END IF;
498
+ INSERT INTO public.log_changes (number, at, request_id, object, actor, payload, seal, mac)
499
+ VALUES (nextval('public.log_number'), p_at, p_request, target, p_actor, p_payload, p_seal, p_mac)
500
+ RETURNING number INTO n;
501
+ RETURN n;
502
+ END
503
+ $$;
504
+ --> statement-breakpoint
505
+ -- The checkpointer's barrier: under the fence exclusive, no use or change is in flight, so every number handed out so far is final (committed, or a gap for good). It answers the last one, which the next checkpoint closes up to: no lag, and no number check in the uses. It raises the floor of `at` to its clock less deadline_max(), never lowers it; the uses after it are held to it. It also answers the floor it found: the one the last barrier set, which the checkpointer holds against the floor of its last signed checkpoint (BarrierChecksFloor: a writer who lowers the floor puts uses where no checkpoint reads). On any other, the checkpointer closes and signs nothing, before it reads the window, and alarms.
506
+ CREATE FUNCTION close_one() RETURNS TABLE (upto bigint, at_floor bigint, floor_found bigint)
507
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp AS $$
508
+ DECLARE
509
+ found_floor bigint;
510
+ BEGIN
511
+ PERFORM pg_advisory_xact_lock(1668244070, 0);
512
+ SELECT f.at_floor INTO found_floor FROM public.log_fence f WHERE f.only_row;
513
+ UPDATE public.log_fence f SET at_floor = greatest(f.at_floor, (extract(epoch FROM clock_timestamp()) * 1000)::bigint - public.deadline_max()) WHERE f.only_row;
514
+ RETURN QUERY SELECT pg_sequence_last_value('public.log_number'), f.at_floor, found_floor FROM public.log_fence f WHERE f.only_row;
515
+ END
516
+ $$;
517
+ --> statement-breakpoint
518
+ -- A checkpoint, written as one (plan PR 6, W212's rule 3, PairWrittenTogether): the tiles that grew, and the pair of heads, in one call, so that no state holds one head without the other, or a head over tiles that disagree. A tile only grows (a partial one at the right edge is written again, wider, each tick): one whose stored hashes are not where the new ones start is refused, and so is the whole call. One written again with the same bytes is fine (a pair finished after an earlier attempt failed). A second pair for one `upto` is refused. A leaf tile needs its range (make_tiles) first. The window it seals is read in the statement after close_one's, whose snapshot sees every append the barrier waited for.
519
+ CREATE FUNCTION write_checkpoint(
520
+ p_upto bigint, p_at bigint, p_at_floor bigint,
521
+ p_changes_size bigint, p_changes_root bytea, p_changes_note text,
522
+ p_uses_size bigint, p_uses_root bytea, p_uses_note text,
523
+ p_tree text[], p_level smallint[], p_leaf bigint[], p_hashes bytea[]
524
+ ) RETURNS void
525
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp AS $$
526
+ DECLARE
527
+ written integer;
528
+ BEGIN
529
+ -- The tiles as four lists of one length, each tile once: a caller's mistake, said as one, never retried as a tamper.
530
+ IF p_tree IS NULL OR p_level IS NULL OR p_leaf IS NULL OR p_hashes IS NULL
531
+ OR cardinality(p_level) <> cardinality(p_tree) OR cardinality(p_leaf) <> cardinality(p_tree) OR cardinality(p_hashes) <> cardinality(p_tree)
532
+ OR (SELECT count(DISTINCT (tree, level, leaf)) FROM unnest(p_tree, p_level, p_leaf) AS x (tree, level, leaf)) <> cardinality(p_tree) THEN
533
+ RAISE EXCEPTION 'the tiles are four lists of one length, each tile once' USING ERRCODE = 'invalid_parameter_value';
534
+ END IF;
535
+ WITH grown AS (
536
+ INSERT INTO public.log_tiles AS t (tree, level, leaf, hashes)
537
+ SELECT * FROM unnest(p_tree, p_level, p_leaf, p_hashes)
538
+ ON CONFLICT (tree, level, leaf) DO UPDATE SET hashes = excluded.hashes
539
+ WHERE substr(excluded.hashes, 1, octet_length(t.hashes)) = t.hashes
540
+ RETURNING 1
541
+ )
542
+ SELECT count(*) INTO written FROM grown;
543
+ IF written <> cardinality(p_tree) THEN
544
+ RAISE EXCEPTION 'a tile would shrink or change: % of % written', written, cardinality(p_tree) USING ERRCODE = 'data_corrupted';
545
+ END IF;
546
+ INSERT INTO public.log_checkpoints (upto, at, at_floor, changes_size, changes_root, changes_note, uses_size, uses_root, uses_note)
547
+ VALUES (p_upto, p_at, p_at_floor, p_changes_size, p_changes_root, p_changes_note, p_uses_size, p_uses_root, p_uses_note);
548
+ END
549
+ $$;
550
+ --> statement-breakpoint
551
+ -- The bulk cap's rows that count for nothing any more, at most p_limit a call: older than the policy's window (its newest change's, as a use reads it), its refresh and the deadline, by the database's clock, less an hour for the clock stepping back (as prune_vault_releases). No caller chooses the bound, so the vault's login, which deletes no row itself, cannot forget a key the window counts; the replay keeps them, which changes none of its decisions. A row a use holds is left to the next call.
552
+ CREATE FUNCTION prune_bulk(p_limit integer) RETURNS integer
553
+ LANGUAGE sql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET lock_timeout = '3s' AS $$
554
+ WITH bound AS (
555
+ SELECT (extract(epoch FROM clock_timestamp()) * 1000)::bigint - w.window_ms - least(60000, w.window_ms / 10) - public.deadline_max() - 3600000 AS before
556
+ FROM (
557
+ SELECT (x.payload::jsonb #>> '{value,bulk,windowMs}')::bigint AS window_ms FROM public.log_changes x
558
+ WHERE x.object = public.log_object('policy', 'instance') ORDER BY x.number DESC LIMIT 1
559
+ ) w
560
+ WHERE w.window_ms IS NOT NULL
561
+ ),
562
+ pruned AS (
563
+ DELETE FROM public.log_bulk
564
+ WHERE last < (SELECT before FROM bound)
565
+ AND ctid IN (SELECT ctid FROM public.log_bulk WHERE last < (SELECT before FROM bound) LIMIT p_limit FOR UPDATE SKIP LOCKED)
566
+ RETURNING 1
567
+ )
568
+ SELECT count(*)::integer FROM pruned;
569
+ $$;
570
+ --> statement-breakpoint
571
+ -- OldWritesFence (plan, PR 9: "old uses in flight"): until 0.16 removes 0.14's path, 0.14 still writes decision inputs, nearly always with an entry in audit_log in the transaction that writes them. Each such entry takes the 0.15 fence exclusive, as a 0.15 change does, so that no 0.15 use is in flight across it: one that read before it commits first, and one after reads what it wrote (refused stale until the vault adopts it). It runs after 0.14's append took the log's head, so 0.14's order is head then fence, the order a 0.15 change that fences 0.14 too takes them (the reverse deadlocks). Every allowed entry fences but those that change no input, the list W211's spec (Migration015.tla, OldWritesFence) names: reads and uses, sign-ins (a credential nobody holds yet; a sign-out revokes one, and fences), and the log's own bookkeeping and alarms. So 0.14's uses never wait on it, and an action this list does not name fences rather than slips past. A refused entry changes nothing. Nothing takes the fence before 0.15 runs: until then it costs an uncontended lock per change. The fence orders only what has an entry: 0.14's entry-less writes (a session's renewal, renewSession, moving a credential's expiry; last-use stamps) are ordered by the seals instead, a renewed row matching no change until the vault adopts it, and a stamp no part of any object (W216).
572
+ CREATE FUNCTION log_fence_old_write() RETURNS trigger
573
+ LANGUAGE plpgsql AS $$
574
+ BEGIN
575
+ PERFORM pg_catalog.pg_advisory_xact_lock(1668244070, 0);
576
+ RETURN NULL;
577
+ END
578
+ $$;
579
+ --> statement-breakpoint
580
+ -- A trigger runs its function whoever may execute it: no login may call it otherwise.
581
+ REVOKE ALL ON FUNCTION log_fence_old_write() FROM PUBLIC;
582
+ --> statement-breakpoint
583
+ CREATE TRIGGER audit_log_fence AFTER INSERT ON audit_log FOR EACH ROW
584
+ WHEN (NEW.decision = 'allow' AND NEW.action NOT IN (
585
+ 'credential.use', 'token.use', 'secret.read', 'secret.use', 'secret.list', 'secret.history', 'secret.version', 'missing.list', 'personal.list',
586
+ 'roles.list', 'roles.preview', 'mcp.read', 'mcp.view', 'audit.read',
587
+ 'sign_in', 'device.approve', 'device.deny',
588
+ 'audit.heartbeat', 'audit.checkpoint', 'audit.finding', 'audit.finding_cleared', 'key.check', 'key.intent', 'workload.alert', 'vault.tampered'
589
+ ))
590
+ EXECUTE FUNCTION log_fence_old_write();
591
+ --> statement-breakpoint
592
+ REVOKE ALL ON FUNCTION make_day(date), make_tiles(bigint, bigint), use_credential(uuid, uuid, bigint, bigint, smallint, uuid, bytea, bigint, bigint, bigint, bigint, bigint, bytea), change_input(text, text, text, bigint, text, text, bytea, bytea, bytea), close_one(), write_checkpoint(bigint, bigint, bigint, bigint, bytea, text, bigint, bytea, text, text[], smallint[], bigint[], bytea[]), prune_bulk(integer) FROM PUBLIC;
593
+ --> statement-breakpoint
594
+ GRANT EXECUTE ON FUNCTION make_day(date), make_tiles(bigint, bigint), use_credential(uuid, uuid, bigint, bigint, smallint, uuid, bytea, bigint, bigint, bigint, bigint, bigint, bytea), change_input(text, text, text, bigint, text, text, bytea, bytea, bytea), close_one(), write_checkpoint(bigint, bigint, bigint, bigint, bytea, text, bigint, bytea, text, text[], smallint[], bigint[], bytea[]), prune_bulk(integer) TO coffre_vault;
595
+ --> statement-breakpoint
596
+ -- The vault reads all of it; the backup reads every table, partitions included (pg_dump reads each), and the sequence. The app reads none: it never decides.
597
+ GRANT SELECT ON log_config, log_changes, log_uses, log_bulk, log_fence, log_checkpoints, log_tiles TO coffre_vault;
598
+ --> statement-breakpoint
599
+ GRANT SELECT ON log_config, log_changes, log_uses, log_bulk, log_fence, log_checkpoints, log_tiles, log_tiles_changes, log_tiles_uses, log_tiles_uses_leaves, log_tiles_uses_upper TO coffre_backup;
600
+ --> statement-breakpoint
601
+ GRANT SELECT ON SEQUENCE log_number TO coffre_backup;
602
+ --> statement-breakpoint
603
+ -- The 0.15 hot path's login (plan, PR 2): the vault's, through coffre_vault, with the hot path's settings, which set on coffre_vault_runtime would change 0.14's behaviour from this migration to the deploy. lock_timeout and statement_timeout bound what a use waits for and runs (R53); generic plans, because Postgres's auto mode planned use_credential's statement about ten times as often, 26 ms a plan under load (hot-path-one.md, decision 7). A role's settings apply to the role that logs in, never through a group, so they are this role's own. Made here unable to log in, as 0019 makes coffre_backup: `coffre setup`, or the host, gives it a password (plan, PR 5), which is never ours.
604
+ DO $$
605
+ DECLARE
606
+ held text[];
607
+ BEGIN
608
+ IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'coffre_vault_hot') THEN
609
+ IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = current_user AND (rolsuper OR rolcreaterole)) THEN
610
+ RAISE EXCEPTION '% may not create roles, and this migration makes coffre_vault_hot: migrate as a login with CREATEROLE, or have one run CREATE ROLE coffre_vault_hot NOLOGIN; 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;
611
+ END IF;
612
+ -- Roles are the server's, and the migration lock each database's: another database's 0022 may make it meanwhile.
613
+ BEGIN
614
+ CREATE ROLE coffre_vault_hot NOLOGIN INHERIT NOCREATEDB NOCREATEROLE;
615
+ EXCEPTION WHEN duplicate_object OR unique_violation THEN
616
+ NULL;
617
+ END;
618
+ END IF;
619
+ -- As 0019 holds coffre_backup: the superuser-only attributes refused (managed owners cannot ALTER them, even
620
+ -- off), the others turned off, only when they must be (altering a role takes CREATEROLE too). Whether it logs in is
621
+ -- left as found: a role made here cannot, one a host made for 0.15 may.
622
+ IF EXISTS (
623
+ SELECT 1 FROM pg_roles WHERE rolname = 'coffre_vault_hot'
624
+ AND (rolsuper OR rolreplication OR rolbypassrls)
625
+ ) THEN
626
+ RAISE EXCEPTION 'coffre_vault_hot has unsafe role attributes: SUPERUSER, REPLICATION and BYPASSRLS must be off';
627
+ END IF;
628
+ IF EXISTS (
629
+ SELECT 1 FROM pg_shdepend
630
+ WHERE refclassid = 'pg_authid'::regclass AND refobjid = 'coffre_vault_hot'::regrole AND deptype = 'o'
631
+ ) THEN
632
+ RAISE EXCEPTION 'coffre_vault_hot owns database objects';
633
+ END IF;
634
+ IF EXISTS (
635
+ SELECT 1 FROM pg_auth_members WHERE member = 'coffre_vault_hot'::regrole AND roleid <> 'coffre_vault'::regrole
636
+ ) THEN
637
+ RAISE EXCEPTION 'coffre_vault_hot is a member of a role other than coffre_vault: revoke its memberships';
638
+ END IF;
639
+ -- Memberships are kept per grantor: one that may set the group, or grant it, stays in force beside the one made here.
640
+ IF EXISTS (
641
+ SELECT 1 FROM pg_auth_members WHERE member = 'coffre_vault_hot'::regrole AND roleid = 'coffre_vault'::regrole AND (set_option OR admin_option)
642
+ ) THEN
643
+ 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';
644
+ END IF;
645
+ -- A setting for this database alone would override the role's at login: one of the three there is refused.
646
+ IF EXISTS (
647
+ SELECT 1 FROM pg_db_role_setting s, unnest(s.setconfig) AS setting
648
+ WHERE s.setrole = 'coffre_vault_hot'::regrole AND s.setdatabase = (SELECT oid FROM pg_database WHERE datname = current_database())
649
+ AND split_part(setting, '=', 1) IN ('lock_timeout', 'statement_timeout', 'plan_cache_mode')
650
+ ) THEN
651
+ 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());
652
+ END IF;
653
+ -- Only what differs, each tried, and a refusal said with what to run: changing a role another made takes, on
654
+ -- Postgres 16, CREATEROLE and its ADMIN, and granting the group its ADMIN (as setup's changeRole says it).
655
+ SELECT setconfig INTO held FROM pg_db_role_setting WHERE setrole = 'coffre_vault_hot'::regrole AND setdatabase = 0;
656
+ BEGIN
657
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'coffre_vault_hot' AND (NOT rolinherit OR rolcreatedb OR rolcreaterole)) THEN
658
+ ALTER ROLE coffre_vault_hot INHERIT NOCREATEDB NOCREATEROLE;
659
+ END IF;
660
+ IF NOT coalesce(held @> ARRAY['lock_timeout=1s'], false) THEN ALTER ROLE coffre_vault_hot SET lock_timeout = '1s'; END IF;
661
+ IF NOT coalesce(held @> ARRAY['statement_timeout=2s'], false) THEN ALTER ROLE coffre_vault_hot SET statement_timeout = '2s'; END IF;
662
+ 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;
663
+ IF NOT EXISTS (
664
+ SELECT 1 FROM pg_auth_members WHERE member = 'coffre_vault_hot'::regrole AND roleid = 'coffre_vault'::regrole AND inherit_option
665
+ ) THEN
666
+ GRANT coffre_vault TO coffre_vault_hot WITH ADMIN FALSE, INHERIT TRUE, SET FALSE;
667
+ END IF;
668
+ EXCEPTION WHEN insufficient_privilege THEN
669
+ 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;
670
+ END;
671
+ END
672
+ $$;