@coffre/db 0.14.5 → 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 (214) 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 -622
  17. package/dist/log.js +178 -13
  18. package/dist/migrate.d.ts +43 -3
  19. package/dist/migrate.js +149 -29
  20. package/dist/migrations/postgres/0022_log_015.sql +672 -0
  21. package/dist/migrations/postgres/0023_change_path.sql +153 -0
  22. package/dist/migrations/postgres/0024_use_015.sql +556 -0
  23. package/dist/migrations/postgres/0025_app_change.sql +221 -0
  24. package/dist/migrations/postgres/0026_issue_lock.sql +106 -0
  25. package/dist/migrations/postgres/0027_retention.sql +118 -0
  26. package/dist/migrations/postgres/0028_use_stalls.sql +594 -0
  27. package/dist/migrations/postgres/0029_use_objects.sql +887 -0
  28. package/dist/migrations/postgres/0030_unarchive_restores.sql +259 -0
  29. package/dist/migrations/postgres/0031_app_read.sql +65 -0
  30. package/dist/migrations/postgres/0032_secrets_live_index.sql +4 -0
  31. package/dist/migrations/postgres/0033_use_generations.sql +582 -0
  32. package/dist/migrations/postgres/0034_vault_credentials.sql +153 -0
  33. package/dist/migrations/postgres/0035_first_days.sql +3 -0
  34. package/dist/migrations/postgres/0036_vault_issues.sql +91 -0
  35. package/dist/migrations/postgres/0037_vault_bindings.sql +271 -0
  36. package/dist/migrations/postgres/0040_binding_origin.sql +159 -0
  37. package/dist/migrations/postgres/0041_cutover.sql +132 -0
  38. package/dist/migrations/postgres/0042_sign_in_index.sql +7 -0
  39. package/dist/migrations/postgres/0043_issue_revoke_index.sql +4 -0
  40. package/dist/migrations/postgres/0044_use_by_name.sql +705 -0
  41. package/dist/migrations/postgres/0045_decide_first.sql +878 -0
  42. package/dist/migrations/postgres/0046_way_in_uses.sql +1149 -0
  43. package/dist/migrations/postgres/0047_cutover_finish.sql +268 -0
  44. package/dist/migrations/postgres/0048_use_batch.sql +59 -0
  45. package/dist/migrations/postgres/0049_reads.sql +171 -0
  46. package/dist/migrations/postgres/meta/0022_snapshot.json +3831 -0
  47. package/dist/migrations/postgres/meta/0023_snapshot.json +3831 -0
  48. package/dist/migrations/postgres/meta/0024_snapshot.json +3831 -0
  49. package/dist/migrations/postgres/meta/0025_snapshot.json +3831 -0
  50. package/dist/migrations/postgres/meta/0026_snapshot.json +3831 -0
  51. package/dist/migrations/postgres/meta/0027_snapshot.json +3853 -0
  52. package/dist/migrations/postgres/meta/0028_snapshot.json +3853 -0
  53. package/dist/migrations/postgres/meta/0029_snapshot.json +3859 -0
  54. package/dist/migrations/postgres/meta/0030_snapshot.json +3859 -0
  55. package/dist/migrations/postgres/meta/0031_snapshot.json +3859 -0
  56. package/dist/migrations/postgres/meta/0032_snapshot.json +3875 -0
  57. package/dist/migrations/postgres/meta/0033_snapshot.json +3875 -0
  58. package/dist/migrations/postgres/meta/0034_snapshot.json +3875 -0
  59. package/dist/migrations/postgres/meta/0035_snapshot.json +3875 -0
  60. package/dist/migrations/postgres/meta/0036_snapshot.json +3875 -0
  61. package/dist/migrations/postgres/meta/0037_snapshot.json +3875 -0
  62. package/dist/migrations/postgres/meta/0038_snapshot.json +3881 -0
  63. package/dist/migrations/postgres/meta/0039_snapshot.json +3881 -0
  64. package/dist/migrations/postgres/meta/0040_snapshot.json +3931 -0
  65. package/dist/migrations/postgres/meta/0041_snapshot.json +3981 -0
  66. package/dist/migrations/postgres/meta/0042_snapshot.json +3981 -0
  67. package/dist/migrations/postgres/meta/0043_snapshot.json +3981 -0
  68. package/dist/migrations/postgres/meta/0044_snapshot.json +3991 -0
  69. package/dist/migrations/postgres/meta/0045_snapshot.json +3997 -0
  70. package/dist/migrations/postgres/meta/0046_snapshot.json +3997 -0
  71. package/dist/migrations/postgres/meta/0047_snapshot.json +4234 -0
  72. package/dist/migrations/postgres/meta/_journal.json +182 -0
  73. package/dist/migrations/sqlite/0018_log_015.sql +84 -0
  74. package/dist/migrations/sqlite/0019_retention.sql +2 -0
  75. package/dist/migrations/sqlite/0020_ended_references.sql +1 -0
  76. package/dist/migrations/sqlite/0021_secrets_live_index.sql +1 -0
  77. package/dist/migrations/sqlite/0022_binding_origin.sql +1 -0
  78. package/dist/migrations/sqlite/0023_sign_in_index.sql +2 -0
  79. package/dist/migrations/sqlite/0024_issue_revoke_index.sql +2 -0
  80. package/dist/migrations/sqlite/0025_way_in_uses.sql +42 -0
  81. package/dist/migrations/sqlite/0026_cutover_finish.sql +11 -0
  82. package/dist/migrations/sqlite/0027_reads.sql +38 -0
  83. package/dist/migrations/sqlite/meta/0018_snapshot.json +3529 -0
  84. package/dist/migrations/sqlite/meta/0019_snapshot.json +3538 -0
  85. package/dist/migrations/sqlite/meta/0020_snapshot.json +3545 -0
  86. package/dist/migrations/sqlite/meta/0021_snapshot.json +3553 -0
  87. package/dist/migrations/sqlite/meta/0022_snapshot.json +3560 -0
  88. package/dist/migrations/sqlite/meta/0023_snapshot.json +3579 -0
  89. package/dist/migrations/sqlite/meta/0024_snapshot.json +3598 -0
  90. package/dist/migrations/sqlite/meta/0025_snapshot.json +3609 -0
  91. package/dist/migrations/sqlite/meta/0026_snapshot.json +3616 -0
  92. package/dist/migrations/sqlite/meta/0027_snapshot.json +3834 -0
  93. package/dist/migrations/sqlite/meta/_journal.json +70 -0
  94. package/dist/platform-Y3gICK6n.d.ts +58 -0
  95. package/dist/portable.d.ts +2 -2
  96. package/dist/postgres-BI3xTnyw.js +1718 -0
  97. package/dist/postgres-platform-node.d.ts +14 -0
  98. package/dist/postgres-platform-node.js +21 -0
  99. package/dist/postgres-platform-workerd.d.ts +61 -0
  100. package/dist/postgres-platform-workerd.js +163 -0
  101. package/dist/{schema-BfQEBqFv.d.ts → schema-DgoPqqsI.d.ts} +1326 -7
  102. package/dist/schema-sqlite.d.ts +1306 -6
  103. package/dist/schema-sqlite.js +154 -18
  104. package/dist/schema-version.d.ts +1 -1
  105. package/dist/schema-version.js +254 -2
  106. package/dist/schema.d.ts +2 -2
  107. package/dist/schema.js +226 -20
  108. package/package.json +13 -5
  109. package/src/active-subject.ts +9 -0
  110. package/src/change-records.ts +38 -0
  111. package/src/connect.ts +67 -13
  112. package/src/database.ts +67 -51
  113. package/src/dialect.ts +17 -8
  114. package/src/grants.ts +3 -3
  115. package/src/hyperdrive.ts +181 -104
  116. package/src/log.ts +196 -15
  117. package/src/migrate.ts +157 -30
  118. package/src/migrations/postgres/0022_log_015.sql +672 -0
  119. package/src/migrations/postgres/0023_change_path.sql +153 -0
  120. package/src/migrations/postgres/0024_use_015.sql +556 -0
  121. package/src/migrations/postgres/0025_app_change.sql +221 -0
  122. package/src/migrations/postgres/0026_issue_lock.sql +106 -0
  123. package/src/migrations/postgres/0027_retention.sql +118 -0
  124. package/src/migrations/postgres/0028_use_stalls.sql +594 -0
  125. package/src/migrations/postgres/0029_use_objects.sql +887 -0
  126. package/src/migrations/postgres/0030_unarchive_restores.sql +259 -0
  127. package/src/migrations/postgres/0031_app_read.sql +65 -0
  128. package/src/migrations/postgres/0032_secrets_live_index.sql +4 -0
  129. package/src/migrations/postgres/0033_use_generations.sql +582 -0
  130. package/src/migrations/postgres/0034_vault_credentials.sql +153 -0
  131. package/src/migrations/postgres/0035_first_days.sql +3 -0
  132. package/src/migrations/postgres/0036_vault_issues.sql +91 -0
  133. package/src/migrations/postgres/0037_vault_bindings.sql +271 -0
  134. package/src/migrations/postgres/0040_binding_origin.sql +159 -0
  135. package/src/migrations/postgres/0041_cutover.sql +132 -0
  136. package/src/migrations/postgres/0042_sign_in_index.sql +7 -0
  137. package/src/migrations/postgres/0043_issue_revoke_index.sql +4 -0
  138. package/src/migrations/postgres/0044_use_by_name.sql +705 -0
  139. package/src/migrations/postgres/0045_decide_first.sql +878 -0
  140. package/src/migrations/postgres/0046_way_in_uses.sql +1149 -0
  141. package/src/migrations/postgres/0047_cutover_finish.sql +268 -0
  142. package/src/migrations/postgres/0048_use_batch.sql +59 -0
  143. package/src/migrations/postgres/0049_reads.sql +171 -0
  144. package/src/migrations/postgres/meta/0022_snapshot.json +3831 -0
  145. package/src/migrations/postgres/meta/0023_snapshot.json +3831 -0
  146. package/src/migrations/postgres/meta/0024_snapshot.json +3831 -0
  147. package/src/migrations/postgres/meta/0025_snapshot.json +3831 -0
  148. package/src/migrations/postgres/meta/0026_snapshot.json +3831 -0
  149. package/src/migrations/postgres/meta/0027_snapshot.json +3853 -0
  150. package/src/migrations/postgres/meta/0028_snapshot.json +3853 -0
  151. package/src/migrations/postgres/meta/0029_snapshot.json +3859 -0
  152. package/src/migrations/postgres/meta/0030_snapshot.json +3859 -0
  153. package/src/migrations/postgres/meta/0031_snapshot.json +3859 -0
  154. package/src/migrations/postgres/meta/0032_snapshot.json +3875 -0
  155. package/src/migrations/postgres/meta/0033_snapshot.json +3875 -0
  156. package/src/migrations/postgres/meta/0034_snapshot.json +3875 -0
  157. package/src/migrations/postgres/meta/0035_snapshot.json +3875 -0
  158. package/src/migrations/postgres/meta/0036_snapshot.json +3875 -0
  159. package/src/migrations/postgres/meta/0037_snapshot.json +3875 -0
  160. package/src/migrations/postgres/meta/0038_snapshot.json +3881 -0
  161. package/src/migrations/postgres/meta/0039_snapshot.json +3881 -0
  162. package/src/migrations/postgres/meta/0040_snapshot.json +3931 -0
  163. package/src/migrations/postgres/meta/0041_snapshot.json +3981 -0
  164. package/src/migrations/postgres/meta/0042_snapshot.json +3981 -0
  165. package/src/migrations/postgres/meta/0043_snapshot.json +3981 -0
  166. package/src/migrations/postgres/meta/0044_snapshot.json +3991 -0
  167. package/src/migrations/postgres/meta/0045_snapshot.json +3997 -0
  168. package/src/migrations/postgres/meta/0046_snapshot.json +3997 -0
  169. package/src/migrations/postgres/meta/0047_snapshot.json +4234 -0
  170. package/src/migrations/postgres/meta/_journal.json +182 -0
  171. package/src/migrations/sqlite/0018_log_015.sql +84 -0
  172. package/src/migrations/sqlite/0019_retention.sql +2 -0
  173. package/src/migrations/sqlite/0020_ended_references.sql +1 -0
  174. package/src/migrations/sqlite/0021_secrets_live_index.sql +1 -0
  175. package/src/migrations/sqlite/0022_binding_origin.sql +1 -0
  176. package/src/migrations/sqlite/0023_sign_in_index.sql +2 -0
  177. package/src/migrations/sqlite/0024_issue_revoke_index.sql +2 -0
  178. package/src/migrations/sqlite/0025_way_in_uses.sql +42 -0
  179. package/src/migrations/sqlite/0026_cutover_finish.sql +11 -0
  180. package/src/migrations/sqlite/0027_reads.sql +38 -0
  181. package/src/migrations/sqlite/meta/0018_snapshot.json +3529 -0
  182. package/src/migrations/sqlite/meta/0019_snapshot.json +3538 -0
  183. package/src/migrations/sqlite/meta/0020_snapshot.json +3545 -0
  184. package/src/migrations/sqlite/meta/0021_snapshot.json +3553 -0
  185. package/src/migrations/sqlite/meta/0022_snapshot.json +3560 -0
  186. package/src/migrations/sqlite/meta/0023_snapshot.json +3579 -0
  187. package/src/migrations/sqlite/meta/0024_snapshot.json +3598 -0
  188. package/src/migrations/sqlite/meta/0025_snapshot.json +3609 -0
  189. package/src/migrations/sqlite/meta/0026_snapshot.json +3616 -0
  190. package/src/migrations/sqlite/meta/0027_snapshot.json +3834 -0
  191. package/src/migrations/sqlite/meta/_journal.json +70 -0
  192. package/src/postgres-js/README.md +183 -0
  193. package/src/postgres-js/UNLICENSE +24 -0
  194. package/src/postgres-js/bytes.ts +89 -0
  195. package/src/postgres-js/connection.ts +882 -0
  196. package/src/postgres-js/errors.ts +84 -0
  197. package/src/postgres-js/index.ts +516 -0
  198. package/src/postgres-js/platform.node.ts +29 -0
  199. package/src/postgres-js/platform.ts +60 -0
  200. package/src/postgres-js/platform.workerd.ts +225 -0
  201. package/src/postgres-js/query.ts +123 -0
  202. package/src/postgres-js/queue.ts +42 -0
  203. package/src/postgres-js/result.ts +46 -0
  204. package/src/postgres-js/types.ts +52 -0
  205. package/src/postgres.ts +375 -26
  206. package/src/schema-version.ts +2 -2
  207. package/src/schema.sqlite.ts +198 -7
  208. package/src/schema.ts +291 -6
  209. package/src/verify-runtime.ts +18 -29
  210. package/LICENSE +0 -21
  211. package/dist/database-wq1AVRAP.d.ts +0 -73
  212. package/dist/postgres-Bmar8y0q.js +0 -29
  213. package/dist/relations-BfR3JjT1.js +0 -38
  214. package/src/relations.ts +0 -40
@@ -0,0 +1,268 @@
1
+ -- 0.15's cutover from 0.14, finished (docs/design/0.15-plan.md, "As built: PR 9b"; formal/Migration015.tla). It adds a column 0.14 never writes, a trigger, and replaces three functions only 0.15's vault calls.
2
+ --
3
+ -- Once the cutover is there, 0.14's code writes nothing (OldWritesRefused, OldWritesBeforeAnchor). An isolate of 0.14 may still serve after 0.15 cut over: its uses go on, its writes would leave rows no change of 0.15's seals, `stale` to every use (the plan's "late 0.14 write"). Each entry a writer under 0.15's log appends carries `log015`, which no MAC covers and 0.14 never sets; the trigger refuses an allowed entry without it, but a read's, a use's or the log's own (`audit.*`: its beats, checkpoints, findings and the losses an Owner accepts; the alarms), once the cutover's change is in `log_changes`. The append fails, and with it the transaction that wrote its rows: the old isolate answers an error, and its client asks again of 0.15. Every 0.14 append holds 0.14's head, which the cutover holds to its commit, so an append either commits before the cutover or sees it. Sign-ins are refused too (`sign_in`, `token.exchange`, `device.approve`): a credential 0.14 makes after the cutover is one 0.15 never sealed. Before the cutover it costs a 0.14 write one probe of `log_changes_object_idx`; a 0.15 entry passes it by its WHEN. An owner who drops the trigger lets a late write land, its object `stale`; a row put back over it is still refused by the use's probe of 0.14's newest access entry (UsesSeeOldWrites).
4
+ ALTER TABLE "audit_log" ADD COLUMN "log015" boolean;
5
+ --> statement-breakpoint
6
+ CREATE FUNCTION log_refuse_old_write() RETURNS trigger
7
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET jit = off AS $$
8
+ BEGIN
9
+ IF EXISTS (SELECT 1 FROM public.log_changes c WHERE c.object = public.log_object('log', 'cutover')) THEN
10
+ RAISE EXCEPTION '0.15''s log is cut over from 0.14: 0.14''s % is refused, as every write of 0.14''s after it (an isolate of the earlier release still serving: ask again)', NEW.action
11
+ USING ERRCODE = 'object_not_in_prerequisite_state';
12
+ END IF;
13
+ RETURN NEW;
14
+ END
15
+ $$;
16
+ --> statement-breakpoint
17
+ REVOKE ALL ON FUNCTION log_refuse_old_write() FROM PUBLIC;
18
+ --> statement-breakpoint
19
+ CREATE TRIGGER audit_log_refuse_old BEFORE INSERT ON audit_log FOR EACH ROW
20
+ WHEN (NEW.decision = 'allow' AND NEW.log015 IS NULL AND NEW.action NOT LIKE 'audit.%' AND NEW.action NOT IN (
21
+ 'credential.use', 'token.use', 'secret.read', 'secret.use', 'secret.list', 'secret.history', 'secret.version', 'missing.list', 'personal.list',
22
+ 'roles.list', 'roles.preview', 'mcp.read', 'mcp.view', 'key.check', 'key.intent', 'workload.alert', 'vault.tampered'
23
+ ))
24
+ EXECUTE FUNCTION log_refuse_old_write();
25
+ --> statement-breakpoint
26
+ -- A revoke as 0034 made it (its comment says how), but for 0.14's head, taken first (EntryLessHead, Migration015EntryLess): it writes no 0.14 entry, and 0.14's use of the credential, on an isolate still serving, checks its row under the head, then commits. Without the head, the revoke commits and answers between the two, and that use goes on. So: 0.14's head, the row, the fence, as the app's writes take them (AppHeadFirst). Until 0.16 removes 0.14's path.
27
+ CREATE OR REPLACE FUNCTION revoke_credential(
28
+ p_request text, p_at bigint, p_actor text, p_payload text, p_mac bytea, p_seal_from bytea, p_seal bytea,
29
+ p_id uuid, p_mac_from bytea, p_auth_mac bytea, p_revoked_at bigint, p_revoked_by text,
30
+ p_policy boolean, p_policy_seq bigint, p_protected uuid[], p_slugs text[]
31
+ ) RETURNS json
32
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET jit = off SET lock_timeout = '1s' AS $$
33
+ DECLARE
34
+ n bigint;
35
+ target uuid := public.log_object('credential', p_id::text);
36
+ BEGIN
37
+ IF p_seal_from IS NULL OR p_seal IS NULL THEN
38
+ RAISE EXCEPTION 'the vault revokes a credential the log holds, from its seal' USING ERRCODE = 'invalid_parameter_value';
39
+ END IF;
40
+ PERFORM 1 FROM public.audit_chain_head WHERE only_row FOR UPDATE;
41
+ PERFORM 1 FROM public.credentials c
42
+ WHERE c.id = p_id AND c.revoked_at IS NULL AND c.auth_mac = p_mac_from AND c.seal IS NOT DISTINCT FROM p_seal_from
43
+ FOR UPDATE;
44
+ IF NOT FOUND THEN RETURN json_build_object('state', 'moved'); END IF;
45
+ PERFORM pg_advisory_xact_lock(1668244070, 0);
46
+ -- ChangeChecksSeal: a row the log sealed once, put back since, is not its newest change's.
47
+ IF p_seal_from IS DISTINCT FROM (SELECT c.seal FROM public.log_changes c WHERE c.object = target ORDER BY c.number DESC LIMIT 1) THEN
48
+ RETURN json_build_object('state', 'stale');
49
+ END IF;
50
+ IF p_policy AND (
51
+ (SELECT a.seq FROM public.audit_log a WHERE a.author = 'vault' AND a.action = 'policy.change' AND a.decision = 'allow' ORDER BY a.seq DESC LIMIT 1)
52
+ IS DISTINCT FROM p_policy_seq
53
+ -- Each protected environment's slug as it stands, none once it or its project is deleted (a tombstone), as `policyNow` reads them.
54
+ OR EXISTS (
55
+ SELECT 1 FROM unnest(p_protected, p_slugs) AS p(id, slug)
56
+ WHERE (SELECT e.slug FROM public.environments e JOIN public.projects q ON q.id = e.project_id
57
+ WHERE e.id = p.id AND strpos(e.slug, '~') = 0 AND strpos(q.slug, '~') = 0) IS DISTINCT FROM p.slug
58
+ )
59
+ ) THEN
60
+ RETURN json_build_object('state', 'policy');
61
+ END IF;
62
+ UPDATE public.credentials
63
+ SET revoked_at = timestamptz 'epoch' + p_revoked_at * interval '1 millisecond', revoked_by = p_revoked_by, auth_mac = p_auth_mac, seal = p_seal
64
+ WHERE id = p_id;
65
+ INSERT INTO public.log_changes (number, at, request_id, object, actor, payload, seal, mac)
66
+ VALUES (nextval('public.log_number'), p_at, p_request, target, p_actor, p_payload, p_seal, p_mac)
67
+ RETURNING number INTO n;
68
+ RETURN json_build_object('number', n::text);
69
+ END
70
+ $$;
71
+ --> statement-breakpoint
72
+ -- A change of bindings as 0040 made it (its comment says how), but for 0.14's head when it ends any, for the revokes it writes with no 0.14 entry, as `revoke_credential` above: after the members' rows (0.14's vault takes a member's row before the head) and before any row of the app's (the credential it came in on, the bindings, their credentials), as 0.14's app takes the head before its rows. A bind that ends nothing takes none.
73
+ CREATE OR REPLACE FUNCTION change_bindings(
74
+ p_at bigint, p_actor text,
75
+ p_make boolean, p_principal text, p_generation integer, p_principal_seq bigint, p_actor_generation integer, p_actor_seq bigint,
76
+ p_via uuid, p_via_seal bytea, p_via_end bigint, p_connection uuid, p_connection_generation integer,
77
+ p_policy_seq bigint, p_protected uuid[], p_slugs text[], p_app_key text, p_max integer,
78
+ p_live uuid[], p_read_keys uuid[], p_read_names text[],
79
+ p_id uuid, p_auth_mac bytea, p_profile text, p_issuer text, p_jwks_uri text, p_jwks text, p_claims text, p_scope text, p_policy text, p_origin text,
80
+ p_label text, p_created_by text, p_created_at bigint, p_seal bytea,
81
+ p_ends uuid[], p_end_mac_from bytea[], p_end_seal_from bytea[], p_end_auth_mac bytea[], p_end_seal bytea[],
82
+ p_creds uuid[], p_cred_binding uuid[], p_cred_mac_from bytea[], p_cred_seal_from bytea[], p_cred_auth_mac bytea[], p_cred_seal bytea[],
83
+ p_revoked_at bigint, p_revoked_by text,
84
+ p_change_request text[], p_change_object uuid[], p_change_payload text[], p_change_seal bytea[], p_change_mac bytea[]
85
+ ) RETURNS json
86
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET jit = off SET lock_timeout = '1s' AS $$
87
+ DECLARE
88
+ n bigint;
89
+ -- A value, not `clock_timestamp()` in a predicate: volatile there, it bounds no index.
90
+ now_ts timestamptz := clock_timestamp();
91
+ ended timestamptz := timestamptz 'epoch' + p_revoked_at * interval '1 millisecond';
92
+ BEGIN
93
+ IF p_make = (p_id IS NULL) OR (NOT p_make AND cardinality(p_ends) = 0) OR cardinality(p_creds) <> cardinality(p_cred_binding)
94
+ OR p_make = (p_live IS NULL) OR cardinality(p_read_keys) <> cardinality(p_read_names)
95
+ OR cardinality(p_change_request) <> (CASE WHEN p_make THEN 1 ELSE 0 END) + (SELECT count(*) FROM unnest(p_end_seal) AS s WHERE s IS NOT NULL)
96
+ + (SELECT count(*) FROM unnest(p_cred_seal) AS s WHERE s IS NOT NULL) THEN
97
+ RAISE EXCEPTION 'a change of bindings makes one, ends some, or both, with a change for each object' USING ERRCODE = 'invalid_parameter_value';
98
+ END IF;
99
+ IF p_make THEN
100
+ -- IssuerLocked, IssuerCAS, as `issue_token`, by principal: the account's row first (`token:` sorts before `user:`),
101
+ -- its update lock, so that two binds of it take turns over its count of bindings (a sign-in through one of its
102
+ -- bindings waits for the statement too); then the maker's, shared. Then both compared.
103
+ IF p_principal >= p_actor THEN RAISE EXCEPTION 'a binding is a service account''s, made by a person' USING ERRCODE = 'invalid_parameter_value'; END IF;
104
+ PERFORM 1 FROM public.vault_members m WHERE m.principal = p_principal FOR NO KEY UPDATE;
105
+ PERFORM 1 FROM public.vault_members m WHERE m.principal = p_actor FOR SHARE;
106
+ -- 0.14's head, when it ends any, after the members' rows and before any row of the app's (row, head, issue lock, fence).
107
+ IF cardinality(p_ends) > 0 THEN PERFORM 1 FROM public.audit_chain_head WHERE only_row FOR UPDATE; END IF;
108
+ IF NOT EXISTS (SELECT 1 FROM public.vault_members m WHERE m.principal = p_actor AND m.status = 'active' AND m.generation = p_actor_generation AND m.access_seq = p_actor_seq)
109
+ OR NOT EXISTS (SELECT 1 FROM public.vault_members m WHERE m.principal = p_principal AND m.status = 'active' AND m.generation = p_generation AND m.access_seq = p_principal_seq) THEN
110
+ RETURN json_build_object('state', 'moved');
111
+ END IF;
112
+ IF p_via IS NOT NULL THEN
113
+ PERFORM 1 FROM public.credentials c WHERE c.id = p_via FOR SHARE;
114
+ IF NOT EXISTS (SELECT 1 FROM public.credentials c
115
+ WHERE c.id = p_via AND c.principal = p_actor AND c.revoked_at IS NULL AND c.seal = p_via_seal
116
+ AND floor(extract(epoch FROM least(c.expires_at, coalesce(c.not_after, c.expires_at))) * 1000)::bigint = p_via_end)
117
+ -- The clock once the locks are had, not when the statement began: one that ended while it waited has ended.
118
+ OR p_via_end <= (extract(epoch FROM clock_timestamp()) * 1000)::bigint
119
+ OR p_via_seal IS DISTINCT FROM (SELECT c.seal FROM public.log_changes c WHERE c.object = public.log_object('credential', p_via::text) ORDER BY c.number DESC LIMIT 1) THEN
120
+ RETURN json_build_object('state', 'ended');
121
+ END IF;
122
+ END IF;
123
+ -- The MCP connection it came through, still connected at the caller's generation (as `credentialStill`): shared,
124
+ -- so a disconnect waits for this, or is seen (review r3).
125
+ IF p_connection IS NOT NULL THEN
126
+ PERFORM 1 FROM public.mcp_connections k WHERE k.id = p_connection AND k.revoked_at IS NULL AND k.generation = p_connection_generation FOR SHARE;
127
+ IF NOT FOUND THEN RETURN json_build_object('state', 'ended'); END IF;
128
+ END IF;
129
+ -- An id the log holds already is no binding to make, even once its row is gone: its object stands (review r3).
130
+ IF EXISTS (SELECT 1 FROM public.log_changes l WHERE l.object = public.log_object('binding', p_id::text)) THEN
131
+ RETURN json_build_object('state', 'stale');
132
+ END IF;
133
+ END IF;
134
+ -- An unbind alone: 0.14's head first, as above.
135
+ IF NOT p_make THEN PERFORM 1 FROM public.audit_chain_head WHERE only_row FOR UPDATE; END IF;
136
+ -- The bindings it ends, then the credentials they issued: each row locked, by id, then held to its MAC and seal as
137
+ -- read. Every live credential they issued must be named, so one that a sign-in made meanwhile is read again (`moved`),
138
+ -- never left live.
139
+ PERFORM 1 FROM public.service_bindings b WHERE b.id = ANY(p_ends) ORDER BY b.id FOR UPDATE;
140
+ IF (SELECT count(*) FROM unnest(p_ends, p_end_mac_from, p_end_seal_from) AS e(id, mac, seal)
141
+ JOIN public.service_bindings b ON b.id = e.id AND b.principal = p_principal AND b.revoked_at IS NULL AND b.auth_mac = e.mac AND b.seal IS NOT DISTINCT FROM e.seal)
142
+ <> cardinality(p_ends) THEN
143
+ RETURN json_build_object('state', 'moved');
144
+ END IF;
145
+ -- A credential 0.14 made, or one written around the vault, has no seal: ended with its binding all the same, its row
146
+ -- revoked with no change, the log holding no object of it.
147
+ PERFORM 1 FROM public.credentials c WHERE c.id = ANY(p_creds) ORDER BY c.id FOR UPDATE;
148
+ IF (SELECT count(*) FROM unnest(p_creds, p_cred_binding, p_cred_mac_from, p_cred_seal_from) AS x(id, binding, mac, seal)
149
+ JOIN public.credentials c ON c.id = x.id AND x.binding = ANY(p_ends) AND c.created_by = 'binding:' || x.binding::text
150
+ AND c.revoked_at IS NULL AND c.auth_mac = x.mac AND c.seal IS NOT DISTINCT FROM x.seal)
151
+ <> cardinality(p_creds)
152
+ OR EXISTS (SELECT 1 FROM unnest(p_ends) AS e(id) JOIN public.credentials c
153
+ ON c.principal = p_principal AND c.created_by = 'binding:' || e.id::text AND c.revoked_at IS NULL AND c.expires_at > now_ts
154
+ WHERE NOT (c.id = ANY(p_creds))) THEN
155
+ RETURN json_build_object('state', 'moved');
156
+ END IF;
157
+ IF p_make THEN
158
+ -- The account's live bindings as the app read them, which it weighed this one beside (an issuer's keys moved, the
159
+ -- bindings it replaces): another made or ended since is read again. Under the account's row's lock, which every
160
+ -- bind takes, so two binds take turns. At most `p_max`, those it ends left out.
161
+ IF ARRAY(SELECT b.id FROM public.service_bindings b WHERE b.principal = p_principal AND b.generation = p_generation AND b.revoked_at IS NULL ORDER BY b.id)
162
+ IS DISTINCT FROM ARRAY(SELECT l FROM unnest(p_live) AS l ORDER BY l) THEN
163
+ RETURN json_build_object('state', 'moved');
164
+ END IF;
165
+ IF cardinality(p_live) - (SELECT count(*) FROM unnest(p_live) AS l WHERE l = ANY(p_ends)) >= p_max THEN
166
+ RETURN json_build_object('state', 'full');
167
+ END IF;
168
+ END IF;
169
+ -- IssuePolicyHeld: the issue lock, shared; then, under it, the policy and the slugs this change was weighed on, and the numbers.
170
+ PERFORM pg_advisory_xact_lock_shared(1668244070, 1);
171
+ IF (SELECT a.seq FROM public.audit_log a WHERE a.author = 'vault' AND a.action = 'policy.change' AND a.decision = 'allow' ORDER BY a.seq DESC LIMIT 1)
172
+ IS DISTINCT FROM p_policy_seq
173
+ OR EXISTS (
174
+ SELECT 1 FROM unnest(p_protected, p_slugs) AS p(id, slug)
175
+ WHERE (SELECT e.slug FROM public.environments e JOIN public.projects q ON q.id = e.project_id
176
+ WHERE e.id = p.id AND strpos(e.slug, '~') = 0 AND strpos(q.slug, '~') = 0) IS DISTINCT FROM p.slug
177
+ ) THEN
178
+ RETURN json_build_object('state', 'policy');
179
+ END IF;
180
+ IF EXISTS (SELECT 1 FROM public.audit_log a WHERE a.author = 'app' AND a.subject_principal = p_app_key AND a.action IN ('key.reseal', 'key.rotate')) THEN
181
+ RETURN json_build_object('state', 'retired');
182
+ END IF;
183
+ -- What it ends is a use's input: the fence exclusive (RevokeTakesFence), after the issue lock (PolicyIssueFirst), for
184
+ -- the statement's work only. A bind that ends nothing takes none.
185
+ IF cardinality(p_ends) > 0 THEN PERFORM pg_advisory_xact_lock(1668244070, 0); END IF;
186
+ -- The keys a binding it makes reads, by the names they were asked by: still those names (a rename meanwhile names
187
+ -- another). Compared, not locked (a key's writer takes the fence before its row: a share lock here, held into the
188
+ -- fence, would be a cycle; review r3), and once the fence is had, which a rename takes too: one that committed while
189
+ -- this waited is seen (review r4). With no fence, one that commits after this is a rename after the bind.
190
+ IF p_make AND (SELECT count(*) FROM unnest(p_read_keys, p_read_names) AS r(id, name) JOIN public.secrets x ON x.id = r.id AND x.key = r.name) <> cardinality(p_read_keys) THEN
191
+ RETURN json_build_object('state', 'moved');
192
+ END IF;
193
+ -- ChangeChecksSeal: each row it ends is its object's newest change's; one put back is stale. A row with no seal is one
194
+ -- the log never held, or none at all once a seal it had was cleared around the vault: that one is stale too, so the
195
+ -- log's binding is never ended with no change of it (review r2).
196
+ IF EXISTS (SELECT 1 FROM unnest(p_ends, p_end_seal_from) AS e(id, seal)
197
+ WHERE e.seal IS DISTINCT FROM (SELECT l.seal FROM public.log_changes l WHERE l.object = public.log_object('binding', e.id::text) ORDER BY l.number DESC LIMIT 1))
198
+ OR EXISTS (SELECT 1 FROM unnest(p_creds, p_cred_seal_from) AS x(id, seal)
199
+ WHERE x.seal IS DISTINCT FROM (SELECT l.seal FROM public.log_changes l WHERE l.object = public.log_object('credential', x.id::text) ORDER BY l.number DESC LIMIT 1)) THEN
200
+ RETURN json_build_object('state', 'stale');
201
+ END IF;
202
+ IF p_make THEN
203
+ -- Again, now that no lock is left to wait for: a make of the same id that committed meanwhile, its row deleted since,
204
+ -- is seen; one not yet committed holds the id's key, which this insert then waits for and fails on (review r4).
205
+ IF EXISTS (SELECT 1 FROM public.log_changes l WHERE l.object = public.log_object('binding', p_id::text)) THEN
206
+ RETURN json_build_object('state', 'stale');
207
+ END IF;
208
+ INSERT INTO public.service_bindings (id, auth_mac, principal, generation, profile, issuer, jwks_uri, jwks, claims, scope, policy, origin, label, created_by, created_at, seal)
209
+ VALUES (p_id, p_auth_mac, p_principal, p_generation, p_profile, p_issuer, p_jwks_uri, p_jwks, p_claims, p_scope, p_policy, p_origin, p_label, p_created_by,
210
+ timestamptz 'epoch' + p_created_at * interval '1 millisecond', p_seal);
211
+ END IF;
212
+ UPDATE public.service_bindings b SET revoked_at = ended, revoked_by = p_revoked_by, auth_mac = e.mac, seal = e.seal
213
+ FROM unnest(p_ends, p_end_auth_mac, p_end_seal) AS e(id, mac, seal) WHERE b.id = e.id;
214
+ UPDATE public.credentials c SET revoked_at = ended, revoked_by = p_revoked_by, auth_mac = x.mac, seal = x.seal
215
+ FROM unnest(p_creds, p_cred_auth_mac, p_cred_seal) AS x(id, mac, seal) WHERE c.id = x.id;
216
+ -- Each object's change, numbered in the order named: the binding made, those ended, their credentials.
217
+ FOR i IN 1 .. cardinality(p_change_request) LOOP
218
+ INSERT INTO public.log_changes (number, at, request_id, object, actor, payload, seal, mac)
219
+ VALUES (nextval('public.log_number'), p_at, p_change_request[i], p_change_object[i], p_actor, p_change_payload[i], p_change_seal[i], p_change_mac[i])
220
+ RETURNING number INTO n;
221
+ END LOOP;
222
+ -- Only rows the log never held, ended: no change, nothing to seal.
223
+ RETURN json_build_object('number', coalesce(n::text, '0'));
224
+ END
225
+ $$;
226
+ --> statement-breakpoint
227
+ -- A binding's genesis as 0041 made it, but for its scope, held to the object too (PR 9a's review, P3): a row whose scope moved past the cutover's read, under its locks, is not sealed (`moved`: the cutover reads again), rather than sealed to an object every use of it then refuses. A scope is canonical JSON in its object and any JSON in its row: compared as jsonb. One the vault cannot read as a scope is no binding's of 0.14 (`planCutover`), never sealed.
228
+ CREATE OR REPLACE FUNCTION seal_binding(p_id uuid, p_request text, p_at bigint, p_actor text, p_payload text, p_seal bytea, p_mac bytea) RETURNS bigint
229
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET jit = off AS $$
230
+ DECLARE
231
+ op jsonb := p_payload::jsonb;
232
+ target uuid := public.log_object('binding', p_id::text);
233
+ n bigint;
234
+ done integer;
235
+ BEGIN
236
+ IF op ->> 'op' IS DISTINCT FROM 'genesis' OR op #>> '{ref,kind}' IS DISTINCT FROM 'binding' OR op #>> '{ref,id}' IS DISTINCT FROM p_id::text
237
+ OR op #>> '{value,kind}' IS DISTINCT FROM 'binding' OR op #>> '{value,id}' IS DISTINCT FROM p_id::text OR op #>> '{value,origin}' IS NULL OR p_seal IS NULL THEN
238
+ RAISE EXCEPTION 'not a binding''s genesis: %', p_payload USING ERRCODE = 'invalid_parameter_value';
239
+ END IF;
240
+ PERFORM 1 FROM public.audit_chain_head WHERE only_row FOR UPDATE;
241
+ PERFORM pg_advisory_xact_lock(1668244070, 0);
242
+ SELECT c.number INTO n FROM public.log_changes c WHERE c.request_id = p_request;
243
+ IF FOUND THEN
244
+ IF NOT EXISTS (SELECT 1 FROM public.log_changes c WHERE c.request_id = p_request AND c.object = target AND c.payload = p_payload AND c.seal = p_seal) THEN
245
+ RAISE EXCEPTION 'request % names another change', p_request USING ERRCODE = 'unique_violation';
246
+ END IF;
247
+ RETURN n;
248
+ END IF;
249
+ IF EXISTS (SELECT 1 FROM public.log_changes c WHERE c.object = public.log_object('log', 'cutover')) THEN
250
+ RAISE EXCEPTION 'the log is cut over from 0.14 already: a binding is the vault''s to make' USING ERRCODE = 'invalid_parameter_value';
251
+ END IF;
252
+ IF EXISTS (SELECT 1 FROM public.log_changes c WHERE c.object = target) THEN
253
+ RETURN NULL;
254
+ END IF;
255
+ UPDATE public.service_bindings b SET seal = p_seal, origin = op #>> '{value,origin}'
256
+ WHERE b.id = p_id AND b.seal IS NULL AND b.revoked_at IS NULL AND b.origin IS NULL
257
+ AND b.principal = op #>> '{value,principal}' AND b.generation = (op #>> '{value,generation}')::integer
258
+ AND b.profile = op #>> '{value,profile}' AND b.issuer = op #>> '{value,issuer}' AND b.jwks_uri = op #>> '{value,jwksUri}'
259
+ AND b.claims = op #>> '{value,claims}' AND b.policy IS NOT DISTINCT FROM op #>> '{value,policy}'
260
+ AND (CASE WHEN b.scope IS NULL THEN 'null'::jsonb WHEN pg_input_is_valid(b.scope, 'jsonb') THEN b.scope::jsonb END) = op #> '{value,scope}';
261
+ GET DIAGNOSTICS done = ROW_COUNT;
262
+ IF done = 0 THEN RETURN NULL; END IF;
263
+ INSERT INTO public.log_changes (number, at, request_id, object, actor, payload, seal, mac)
264
+ VALUES (nextval('public.log_number'), p_at, p_request, target, p_actor, p_payload, p_seal, p_mac)
265
+ RETURNING number INTO n;
266
+ RETURN n;
267
+ END
268
+ $$;
@@ -0,0 +1,59 @@
1
+ -- B2 (T167): a request's several keys in one statement, where each had a call of its own (docs/design/0.15-plan.md,
2
+ -- "As built: T167"; formal/Batching.tla). Each key's use is `use_credential`'s, by ids, as 0046 has it, made in the
3
+ -- request's order in this one transaction: the same reads, the same entry, the same row, so that every guarantee of a
4
+ -- use holds for each key, and the batch adds only what Batching.tla holds of several uses together.
5
+ --
6
+ -- - One fence span: the fence (shared) is taken by the first key's use and kept to the commit, so no change is numbered
7
+ -- among the batch's entries, and every key is decided on the same changes.
8
+ -- - One principal's lock, its strongest first (`p_lock`, StrongestFirst): a batch whose keys take the principal's lock
9
+ -- (the vault's second call, its keys decided) takes it exclusive, after the fence, before its first key. Each key's
10
+ -- use then takes it again as it needs, which its own transaction already holds. Shared for one key then exclusive for
11
+ -- the next, two batches of one principal's would each wait for the other (Postgres ends one after deadlock_timeout).
12
+ -- - Each key counts the cap's set as its own statements read it, the batch's earlier keys included (CountsOwnBatch):
13
+ -- inside the transaction, each sees what the ones before it wrote.
14
+ -- - Nothing else: a key that would take the lock in a first call (`p_decided` false) answers `decide` and writes and
15
+ -- locks nothing, as alone; whether there is a second call is the vault's, once it has every key's rows
16
+ -- (AllDecidedFirst).
17
+ --
18
+ -- Its answer is a JSON array of `use_credential`'s answers, in the request's order. Any key's raise (past the deadline
19
+ -- by Postgres's clock, a lock not had within the hot login's lock_timeout, a replayed request id) rolls the whole batch
20
+ -- back: nothing of it is written.
21
+ CREATE FUNCTION use_batch(
22
+ p_credential uuid, p_secrets uuid[], p_requests bigint[], p_at bigint, p_purpose smallint, p_places uuid[], p_macs bytea[], p_deadline bigint,
23
+ p_covers bigint[], p_cover_ats bigint[], p_cover_requests bigint[], p_knowns bigint[], p_known_macs bytea[], p_cap_counts integer[], p_cap_windows bigint[],
24
+ p_decided boolean[], p_way smallint, p_principal text, p_lock text
25
+ ) RETURNS json
26
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET bytea_output = 'hex' SET jit = off AS $$
27
+ DECLARE
28
+ n integer := coalesce(cardinality(p_secrets), 0);
29
+ answers json[] := '{}';
30
+ BEGIN
31
+ IF n = 0
32
+ OR cardinality(p_requests) IS DISTINCT FROM n OR cardinality(p_places) IS DISTINCT FROM n OR cardinality(p_macs) IS DISTINCT FROM n
33
+ OR cardinality(p_covers) IS DISTINCT FROM n OR cardinality(p_cover_ats) IS DISTINCT FROM n OR cardinality(p_cover_requests) IS DISTINCT FROM n
34
+ OR cardinality(p_knowns) IS DISTINCT FROM n OR cardinality(p_known_macs) IS DISTINCT FROM n
35
+ OR cardinality(p_cap_counts) IS DISTINCT FROM n OR cardinality(p_cap_windows) IS DISTINCT FROM n OR cardinality(p_decided) IS DISTINCT FROM n THEN
36
+ RAISE EXCEPTION 'a batch names a key at least, and each of its lists one entry a key' USING ERRCODE = 'invalid_parameter_value';
37
+ END IF;
38
+ IF p_lock IS NOT NULL THEN
39
+ -- The fence first, as every use takes it, then the principal's lock, exclusive, before any key (StrongestFirst).
40
+ PERFORM pg_advisory_xact_lock_shared(1668244070, 0);
41
+ PERFORM pg_advisory_xact_lock(1668244066, hashtext(p_lock));
42
+ END IF;
43
+ FOR k IN 1 .. n LOOP
44
+ answers := answers || public.use_credential(
45
+ p_credential, p_secrets[k], p_requests[k], p_at, p_purpose, p_places[k], p_macs[k], p_deadline,
46
+ p_covers[k], p_cover_ats[k], p_cover_requests[k], p_knowns[k], p_known_macs[k], p_cap_counts[k], p_cap_windows[k],
47
+ NULL, NULL, NULL, NULL,
48
+ coalesce(p_decided[k], false),
49
+ false, NULL, NULL, NULL, NULL,
50
+ p_way, p_principal
51
+ );
52
+ END LOOP;
53
+ RETURN array_to_json(answers);
54
+ END
55
+ $$;
56
+ --> statement-breakpoint
57
+ REVOKE ALL ON FUNCTION use_batch(uuid, uuid[], bigint[], bigint, smallint, uuid[], bytea[], bigint, bigint[], bigint[], bigint[], bigint[], bytea[], integer[], bigint[], boolean[], smallint, text, text) FROM PUBLIC;
58
+ --> statement-breakpoint
59
+ GRANT EXECUTE ON FUNCTION use_batch(uuid, uuid[], bigint[], bigint, smallint, uuid[], bytea[], bigint, bigint[], bigint[], bigint[], bigint[], bytea[], integer[], bigint[], boolean[], smallint, text, text) TO coffre_vault;
@@ -0,0 +1,171 @@
1
+ -- The audit pages list 0.15's reads from a sealed projection of its uses (T166; docs/design/0.15-plan.md, "As built: T166"; formal/Reads.tla). With 0.15's log a read writes no 0.14 `secret.read` entry: its use, the slim row of R53, is the record, which names neither whose read it was nor where, and whose MAC cannot cover the number the sequence gives it. So the vault's projector, apart from the checkpointer, writes a row of `log_reads` for each use a signed pair seals: its account, its key's place, the fork it was read in, through a reference the source and its place, and the version read, every one from the sealed objects at the use's number, under a MAC of the vault's over all of it, the number included. The audit pages list them on the whole log, and by member (`log_reads_principal_idx`); a key's or an environment's page lists none in 0.15 (D160). `log_reads_cursor` says how far the projector has gone; the pages say so when it is behind. Nothing of this is on a use's path. Additive: the previous release reads nothing here.
2
+ --> statement-breakpoint
3
+ CREATE TABLE "log_reads" (
4
+ "number" bigint NOT NULL,
5
+ "at" bigint NOT NULL,
6
+ "credential" uuid NOT NULL,
7
+ "purpose" smallint NOT NULL,
8
+ "principal" text NOT NULL,
9
+ "secret" uuid NOT NULL,
10
+ "environment" uuid NOT NULL,
11
+ "project" uuid NOT NULL,
12
+ "place" uuid,
13
+ "source" uuid,
14
+ "source_environment" uuid,
15
+ "source_project" uuid,
16
+ "version" uuid,
17
+ "mac" "bytea" NOT NULL,
18
+ CONSTRAINT "log_reads_mac_check" CHECK (octet_length("log_reads"."mac") = 16)
19
+ ) PARTITION BY RANGE ("at");
20
+ --> statement-breakpoint
21
+ CREATE TABLE "log_read_days" (
22
+ "principal" text NOT NULL,
23
+ "day" integer NOT NULL,
24
+ CONSTRAINT "log_read_days_pkey" PRIMARY KEY("principal","day")
25
+ );
26
+ --> statement-breakpoint
27
+ CREATE TABLE "log_reads_cursor" (
28
+ "only_row" boolean PRIMARY KEY DEFAULT true NOT NULL,
29
+ "through" bigint NOT NULL,
30
+ "pair" bigint NOT NULL,
31
+ "at" bigint NOT NULL,
32
+ "mac" "bytea",
33
+ CONSTRAINT "log_reads_cursor_only_row_check" CHECK ("log_reads_cursor"."only_row"),
34
+ CONSTRAINT "log_reads_cursor_mac_check" CHECK ("log_reads_cursor"."mac" IS NULL OR octet_length("log_reads_cursor"."mac") = 16)
35
+ );
36
+ --> statement-breakpoint
37
+ -- A use's projection once, by its time and number (a unique index of a partitioned table holds its partition key).
38
+ CREATE UNIQUE INDEX "log_reads_number_idx" ON "log_reads" USING btree ("at","number");--> statement-breakpoint
39
+ -- A member's reads, within a day: `log_read_days` says which days hold any, so that a page of theirs reads those days
40
+ -- alone, never a probe of each day since (an index of a partitioned table is each day's; review r6: a member's page
41
+ -- read 26 buffers, then 125 with 100 newer days of others' reads).
42
+ CREATE INDEX "log_reads_principal_idx" ON "log_reads" USING btree ("principal","at","number");--> statement-breakpoint
43
+ INSERT INTO "log_reads_cursor" ("only_row", "through", "pair", "at") VALUES (true, 0, 0, 0);--> statement-breakpoint
44
+ -- A day of reads, made as make_day makes one of uses, and beside it.
45
+ CREATE FUNCTION make_reads_day(p_day date) RETURNS text
46
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET jit = off AS $$
47
+ DECLARE
48
+ lo bigint := (p_day - date '1970-01-01')::bigint * 86400000;
49
+ parent text;
50
+ name text;
51
+ BEGIN
52
+ FOREACH parent IN ARRAY ARRAY['log_reads'] LOOP
53
+ name := parent || '_' || to_char(p_day, 'YYYYMMDD');
54
+ IF to_regclass('public.' || quote_ident(name)) IS NOT NULL THEN
55
+ IF NOT EXISTS (
56
+ SELECT 1 FROM pg_inherits i JOIN pg_class c ON c.oid = i.inhrelid
57
+ WHERE i.inhparent = to_regclass('public.' || parent) AND c.oid = to_regclass('public.' || quote_ident(name))
58
+ AND pg_get_expr(c.relpartbound, c.oid) = format('FOR VALUES FROM (%L) TO (%L)', lo, lo + 86400000)
59
+ ) THEN
60
+ RAISE EXCEPTION '% exists, and is not the % of %', name, parent, p_day;
61
+ END IF;
62
+ CONTINUE;
63
+ END IF;
64
+ BEGIN
65
+ EXECUTE format('CREATE TABLE public.%I PARTITION OF public.%I FOR VALUES FROM (%s) TO (%s)', name, parent, lo, lo + 86400000);
66
+ -- pg_dump reads each partition as a table of its own.
67
+ EXECUTE format('GRANT SELECT ON public.%I TO coffre_backup', name);
68
+ EXCEPTION WHEN duplicate_table THEN
69
+ -- Another call made it meanwhile, and committed.
70
+ NULL;
71
+ END;
72
+ END LOOP;
73
+ RETURN to_char(p_day, 'YYYYMMDD');
74
+ END
75
+ $$;--> statement-breakpoint
76
+ -- A day of reads dropped, once its uses are gone from the log (retention detached their day): what is projected of them
77
+ -- goes with them, before the uses' partition is dropped, so that the day is still found (by its uses' table) until both
78
+ -- are gone, and a drop that gave up is tried again (review r10). While the uses' day is the log's, nothing: 'kept'.
79
+ CREATE FUNCTION drop_reads_day(p_day date) RETURNS text
80
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET jit = off SET lock_timeout = '100ms' AS $$
81
+ DECLARE
82
+ parent text;
83
+ name text;
84
+ BEGIN
85
+ IF EXISTS (SELECT 1 FROM pg_inherits i WHERE i.inhparent = to_regclass('public.log_uses')
86
+ AND i.inhrelid = to_regclass('public.' || quote_ident('log_uses_' || to_char(p_day, 'YYYYMMDD')))) THEN
87
+ RETURN 'kept';
88
+ END IF;
89
+ FOREACH parent IN ARRAY ARRAY['log_reads'] LOOP
90
+ name := parent || '_' || to_char(p_day, 'YYYYMMDD');
91
+ IF to_regclass('public.' || quote_ident(name)) IS NOT NULL THEN
92
+ IF NOT EXISTS (SELECT 1 FROM pg_inherits i WHERE i.inhrelid = to_regclass('public.' || quote_ident(name)) AND i.inhparent = to_regclass('public.' || parent)) THEN
93
+ RAISE EXCEPTION '% is not a day of %', name, parent;
94
+ END IF;
95
+ EXECUTE format('DROP TABLE public.%I', name);
96
+ END IF;
97
+ END LOOP;
98
+ DELETE FROM public.log_read_days WHERE day = p_day - date '1970-01-01';
99
+ RETURN 'dropped';
100
+ END
101
+ $$;--> statement-breakpoint
102
+ -- The projector's write: reads, each once (by its time and number), the projector's row put back over any other at its number (a row planted ahead would otherwise make it skip one: formal/Reads.tla, ReplaceOnConflict; two projectors write the same bytes), each member's days of them (`log_read_days`), and the cursor moved on to `p_through`, never back, with the signed pair at or below it whose floor the next read takes (`p_pair`) and its MAC (CursorMac), in one call, after the rows (IdempotentByNumber, CursorAfterRows). One projector's call at a time, the lock taken before any statement reads. None past the newest signed pair's number (ProjectSealedOnly): the projector holds the pair's signatures itself before it asks.
103
+ -- Only from where the chunk started (`p_from`, FromChecked): a projector another overtook, whose reads retention may since have dropped with their day, writes nothing, nor makes their day again: whether it wrote (review r11: 600 retired reads came back). The days its reads need, made here, once it may write.
104
+ CREATE FUNCTION project_reads(p_reads jsonb, p_from bigint, p_through bigint, p_pair bigint, p_at bigint, p_mac bytea) RETURNS boolean
105
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET jit = off AS $$
106
+ DECLARE
107
+ sealed bigint;
108
+ stands bigint;
109
+ BEGIN
110
+ PERFORM pg_advisory_xact_lock(1668244070, 166);
111
+ sealed := (SELECT max(k.upto) FROM public.log_checkpoints k);
112
+ IF sealed IS NULL OR p_through > sealed THEN
113
+ RAISE EXCEPTION 'reads through % are past the newest pair, %', p_through, sealed USING ERRCODE = 'invalid_parameter_value';
114
+ END IF;
115
+ -- The cursor is the migration's one row: gone, nothing is projected, and the pages say so (review r6).
116
+ stands := (SELECT c.through FROM public.log_reads_cursor c WHERE c.only_row);
117
+ IF stands IS NULL THEN
118
+ RAISE EXCEPTION 'the reads cursor is gone' USING ERRCODE = 'data_corrupted';
119
+ END IF;
120
+ IF stands <> p_from THEN
121
+ RETURN false;
122
+ END IF;
123
+ IF EXISTS (SELECT 1 FROM jsonb_array_elements(p_reads) r WHERE (r ->> 'number')::bigint > p_through) THEN
124
+ RAISE EXCEPTION 'a read past % is no read it projects', p_through USING ERRCODE = 'invalid_parameter_value';
125
+ END IF;
126
+ -- Each read's day there to hold it: uses of a log older than the days made ahead (review r4).
127
+ PERFORM public.make_reads_day(d) FROM (SELECT DISTINCT date '1970-01-01' + ((x ->> 'at')::bigint / 86400000)::integer AS d FROM jsonb_array_elements(p_reads) x) days
128
+ WHERE to_regclass('public.' || quote_ident('log_reads_' || to_char(d, 'YYYYMMDD'))) IS NULL;
129
+ INSERT INTO public.log_reads (number, at, credential, purpose, principal, secret, environment, project, place, source, source_environment, source_project, version, mac)
130
+ SELECT (x ->> 'number')::bigint, (x ->> 'at')::bigint, (x ->> 'credential')::uuid, (x ->> 'purpose')::smallint, x ->> 'principal', (x ->> 'secret')::uuid,
131
+ (x ->> 'environment')::uuid, (x ->> 'project')::uuid, (x ->> 'place')::uuid, (x ->> 'source')::uuid, (x ->> 'sourceEnvironment')::uuid,
132
+ (x ->> 'sourceProject')::uuid, (x ->> 'version')::uuid, decode(x ->> 'mac', 'hex')
133
+ FROM jsonb_array_elements(p_reads) x
134
+ ON CONFLICT (at, number) DO UPDATE SET credential = excluded.credential, purpose = excluded.purpose, principal = excluded.principal, secret = excluded.secret,
135
+ environment = excluded.environment, project = excluded.project, place = excluded.place, source = excluded.source, source_environment = excluded.source_environment,
136
+ source_project = excluded.source_project, version = excluded.version, mac = excluded.mac;
137
+ INSERT INTO public.log_read_days (principal, day)
138
+ SELECT DISTINCT x ->> 'principal', ((x ->> 'at')::bigint / 86400000)::integer FROM jsonb_array_elements(p_reads) x
139
+ ON CONFLICT DO NOTHING;
140
+ UPDATE public.log_reads_cursor SET through = p_through, pair = p_pair, at = p_at, mac = p_mac WHERE only_row AND through < p_through;
141
+ RETURN true;
142
+ END
143
+ $$;--> statement-breakpoint
144
+ -- A member's reads, newest first, below a point, at most `p_take`: their days that hold any, newest first, each day's index read in order until the page is full. However many days the log holds, a page reads its member's days and no other (review r6); weighing the empty days made ahead, the planner would otherwise read a busy member's day whole and sort it (measured, T166).
145
+ CREATE FUNCTION log_reads_by(p_principal text, p_at bigint, p_number bigint, p_take integer)
146
+ RETURNS TABLE (at bigint, number bigint)
147
+ LANGUAGE plpgsql STABLE SET search_path = pg_catalog, pg_temp SET jit = off SET enable_sort = off SET enable_bitmapscan = off AS $$
148
+ DECLARE
149
+ d integer;
150
+ left_over integer := p_take;
151
+ found_now integer;
152
+ BEGIN
153
+ FOR d IN SELECT k.day FROM public.log_read_days k WHERE k.principal = p_principal AND k.day <= LEAST(p_at / 86400000, 2147483647) ORDER BY k.day DESC LOOP
154
+ RETURN QUERY SELECT r.at, r.number FROM public.log_reads r
155
+ WHERE r.principal = p_principal AND r.at >= d::bigint * 86400000 AND r.at < (d::bigint + 1) * 86400000 AND (r.at, r.number) < (p_at, p_number)
156
+ ORDER BY r.at DESC, r.number DESC LIMIT left_over;
157
+ GET DIAGNOSTICS found_now = ROW_COUNT;
158
+ left_over := left_over - found_now;
159
+ EXIT WHEN left_over <= 0;
160
+ END LOOP;
161
+ END
162
+ $$;--> statement-breakpoint
163
+ REVOKE ALL ON FUNCTION make_reads_day(date), drop_reads_day(date), project_reads(jsonb, bigint, bigint, bigint, bigint, bytea),
164
+ log_reads_by(text, bigint, bigint, integer) FROM PUBLIC;--> statement-breakpoint
165
+ GRANT EXECUTE ON FUNCTION make_reads_day(date), drop_reads_day(date), project_reads(jsonb, bigint, bigint, bigint, bigint, bytea) TO coffre_vault;--> statement-breakpoint
166
+ GRANT EXECUTE ON FUNCTION log_reads_by(text, bigint, bigint, integer) TO coffre_app;--> statement-breakpoint
167
+ GRANT SELECT ON log_reads, log_read_days, log_reads_cursor TO coffre_vault, coffre_app, coffre_backup;--> statement-breakpoint
168
+ -- The pairs' places and times, for the pages to say how far behind the projection is: public, as the notes are.
169
+ GRANT SELECT ON log_checkpoints TO coffre_app;--> statement-breakpoint
170
+ -- Today's, yesterday's and the next three days, as 0035 made the uses' (the projector makes more as it goes).
171
+ SELECT make_reads_day((now() AT TIME ZONE 'UTC')::date + d) FROM generate_series(-1, 3) AS d;