@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,259 @@
1
+ -- An unarchive never seals rows edited while their key was archived (T134; docs/design/0.15-plan.md, "Unarchives restore what the log sealed"; the spec: formal/SealedObjects.tla's RestoreFromLog, RestorePinned, EndWhileArchived). An archived key is no object and its row has no seal, so nothing checked its rows until its unarchive sealed whatever they held: a version, a reference row or a source a database writer edited meanwhile. Now the vault holds an unarchive to the object its key's log last sealed (`prepare`), and `app_change` holds that seal to the log's newest sealed one under the fence (`p_restores`). And the end of a reference an archived key's kept object stands on is logged on that object (`change_input`: ended, then deleted again), so an unarchive restores it ended, never live. It replaces two functions only 0.15's change path calls, behind the switch; no release calls `app_change`, whose new argument replaces the old function.
2
+ DROP FUNCTION app_change(text, text, text, bigint, text, text, bytea, bytea, bytea);
3
+ --> statement-breakpoint
4
+ -- `app_change` as 0029 made it, but for an unarchive's `p_restores`: the seal of the object its key's log last sealed, which must still be its newest sealed one.
5
+ CREATE FUNCTION app_change(
6
+ 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, p_restores bytea
7
+ ) RETURNS bigint
8
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET jit = off AS $$
9
+ DECLARE
10
+ op jsonb := p_payload::jsonb;
11
+ change text := op ->> 'op';
12
+ target uuid := public.log_object(p_kind, p_id);
13
+ n bigint;
14
+ done integer;
15
+ held bytea;
16
+ -- An issue of a credential this very transaction wrote (its row's xmin is ours: not a savepoint's, nor a row another committed).
17
+ -- Never null, whatever is missing (a 0.14 row's call carries no payload, so no op): else the fence.
18
+ fresh boolean := coalesce(change = 'credential.issue' AND p_kind = 'credential'
19
+ AND EXISTS (SELECT 1 FROM public.credentials c WHERE c.id = p_id::uuid AND c.xmin = pg_current_xact_id()::xid), false);
20
+ BEGIN
21
+ PERFORM 1 FROM public.audit_chain_head WHERE only_row FOR UPDATE;
22
+ -- The issue lock shared (IssueOwnLock); and, but for a fresh issue, the fence exclusive after it (IssueBeforeFence).
23
+ PERFORM pg_advisory_xact_lock_shared(1668244070, 1);
24
+ IF NOT fresh THEN PERFORM pg_advisory_xact_lock(1668244070, 0); END IF;
25
+ held := CASE p_kind
26
+ WHEN 'credential' THEN (SELECT c.seal FROM public.credentials c WHERE c.id = p_id::uuid)
27
+ WHEN 'secret' THEN (SELECT x.seal FROM public.secrets x WHERE x.id = p_id::uuid)
28
+ WHEN 'environment' THEN (SELECT e.seal FROM public.environments e WHERE e.id = p_id::uuid)
29
+ END;
30
+ IF p_request IS NULL THEN
31
+ IF held IS NOT NULL OR EXISTS (SELECT 1 FROM public.log_changes c WHERE c.object = target) THEN
32
+ RAISE EXCEPTION 'the % object''s row is not its newest change''s: stale', p_kind USING ERRCODE = 'data_corrupted';
33
+ END IF;
34
+ RETURN 0;
35
+ END IF;
36
+ SELECT c.number INTO n FROM public.log_changes c WHERE c.request_id = p_request;
37
+ IF FOUND THEN
38
+ IF NOT EXISTS (
39
+ SELECT 1 FROM public.log_changes c
40
+ WHERE c.request_id = p_request AND c.object = target AND c.payload = p_payload AND c.seal IS NOT DISTINCT FROM p_seal
41
+ ) THEN
42
+ RAISE EXCEPTION 'request % names another change', p_request USING ERRCODE = 'unique_violation';
43
+ END IF;
44
+ RETURN n;
45
+ END IF;
46
+ IF NOT ((p_kind = 'credential' AND change IN ('credential.issue', 'credential.revoke', 'credential.renew', 'credential.scope'))
47
+ OR (p_kind = 'secret' AND change IN ('secret.write', 'secret.delete'))
48
+ OR (p_kind = 'environment' AND change IN ('environment.create', 'environment.rename', 'environment.archive', 'project.archive', 'environment.delete'))) THEN
49
+ RAISE EXCEPTION 'the app makes no change % to a %', coalesce(change, 'without an op'), p_kind USING ERRCODE = 'invalid_parameter_value';
50
+ END IF;
51
+ IF op -> 'ref' ->> 'kind' IS DISTINCT FROM p_kind OR op -> 'ref' ->> 'id' IS DISTINCT FROM p_id THEN
52
+ RAISE EXCEPTION 'a change names the object it is to: %, %', p_kind, p_id USING ERRCODE = 'invalid_parameter_value';
53
+ END IF;
54
+ -- ChangeChecksSeal.
55
+ IF held IS DISTINCT FROM (SELECT c.seal FROM public.log_changes c WHERE c.object = target ORDER BY c.number DESC LIMIT 1) THEN
56
+ RAISE EXCEPTION 'the % object''s row is not its newest change''s: stale', p_kind USING ERRCODE = 'data_corrupted';
57
+ END IF;
58
+ -- RestorePinned: an unarchive, a key's object made again from none, restores the one its log last sealed, still so.
59
+ IF p_restores IS NOT NULL THEN
60
+ IF p_kind <> 'secret' OR change <> 'secret.write' OR p_seal_from IS NOT NULL THEN
61
+ RAISE EXCEPTION 'only a key''s object made again from none restores one' USING ERRCODE = 'invalid_parameter_value';
62
+ END IF;
63
+ IF p_restores IS DISTINCT FROM (SELECT c.seal FROM public.log_changes c WHERE c.object = target AND c.seal IS NOT NULL ORDER BY c.number DESC LIMIT 1) THEN
64
+ RAISE EXCEPTION 'the secret object an unarchive restores is not its newest sealed one: stale' USING ERRCODE = 'data_corrupted';
65
+ END IF;
66
+ END IF;
67
+ -- ChangeCAS.
68
+ CASE p_kind
69
+ WHEN 'credential' THEN
70
+ UPDATE public.credentials SET seal = p_seal WHERE id = p_id::uuid AND seal IS NOT DISTINCT FROM p_seal_from;
71
+ WHEN 'secret' THEN
72
+ UPDATE public.secrets SET seal = p_seal WHERE id = p_id::uuid AND seal IS NOT DISTINCT FROM p_seal_from;
73
+ WHEN 'environment' THEN
74
+ UPDATE public.environments SET seal = p_seal WHERE id = p_id::uuid AND seal IS NOT DISTINCT FROM p_seal_from;
75
+ END CASE;
76
+ GET DIAGNOSTICS done = ROW_COUNT;
77
+ IF done = 0 THEN RETURN NULL; END IF;
78
+ INSERT INTO public.log_changes (number, at, request_id, object, actor, payload, seal, mac)
79
+ VALUES (nextval('public.log_number'), p_at, p_request, target, p_actor, p_payload, p_seal, p_mac)
80
+ RETURNING number INTO n;
81
+ RETURN n;
82
+ END
83
+ $$;
84
+ --> statement-breakpoint
85
+ REVOKE ALL ON FUNCTION app_change(text, text, text, bigint, text, text, bytea, bytea, bytea, bytea) FROM PUBLIC;
86
+ --> statement-breakpoint
87
+ GRANT EXECUTE ON FUNCTION app_change(text, text, text, bigint, text, text, bytea, bytea, bytea, bytea) TO coffre_app;
88
+ --> statement-breakpoint
89
+ -- `change_input` as 0029 made it, but for an archived key whose kept object stands on a reference that ends: that object written ended from no seal (its row's end already written by its id), then deleted again (`secret.delete`).
90
+ CREATE OR REPLACE FUNCTION change_input(
91
+ p_kind text, p_id text, p_request text, p_at bigint, p_actor text, p_payload text, p_value text, p_seal_from bytea, p_seal bytea, p_mac bytea
92
+ ) RETURNS bigint
93
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET jit = off AS $$
94
+ DECLARE
95
+ op jsonb := p_payload::jsonb;
96
+ v jsonb := p_value::jsonb;
97
+ change text := op ->> 'op';
98
+ target uuid := public.log_object(p_kind, p_id);
99
+ n bigint;
100
+ done integer;
101
+ member text;
102
+ held bytea;
103
+ newest bytea;
104
+ config jsonb;
105
+ BEGIN
106
+ -- 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,
107
+ -- head, fence: Migration015.tla's RowFirst, ChangeHeadFirst): the member's queue (MEMBER_QUEUE, 0x636f666d) and row
108
+ -- first, a principal's own or a credential's (0.14's vault locks them before the head, lockMembers; a change made
109
+ -- inside a 0.14 decision holds them already), then 0.14's head (its app takes it before its rows), then the fence
110
+ -- (0.14's appends take it after the head, audit_log_fence). The head held, no 0.14 write lands between this change
111
+ -- and its entry.
112
+ member := CASE p_kind
113
+ WHEN 'principal' THEN p_id
114
+ WHEN 'credential' THEN coalesce(
115
+ (SELECT c.principal FROM public.credentials c WHERE c.id = p_id::uuid),
116
+ (SELECT b.principal FROM public.service_bindings b WHERE b.id = p_id::uuid))
117
+ END;
118
+ IF member IS NOT NULL THEN
119
+ PERFORM pg_advisory_xact_lock(1668245101, hashtext(m.principal)) FROM public.vault_members m WHERE m.principal = member FOR NO KEY UPDATE;
120
+ END IF;
121
+ PERFORM 1 FROM public.audit_chain_head WHERE only_row FOR UPDATE;
122
+ -- ChangeLocks: exclusive, so that it waits for every use in flight, and every use after it waits for its commit.
123
+ PERFORM pg_advisory_xact_lock(1668244070, 0);
124
+ SELECT c.number INTO n FROM public.log_changes c WHERE c.request_id = p_request;
125
+ IF FOUND THEN
126
+ IF NOT EXISTS (
127
+ SELECT 1 FROM public.log_changes c
128
+ WHERE c.request_id = p_request AND c.object = target AND c.payload = p_payload AND c.seal IS NOT DISTINCT FROM p_seal
129
+ ) THEN
130
+ RAISE EXCEPTION 'request % names another change', p_request USING ERRCODE = 'unique_violation';
131
+ END IF;
132
+ RETURN n;
133
+ END IF;
134
+ IF NOT (change = 'genesis'
135
+ OR (p_kind = 'principal' AND change IN ('member.add', 'member.role', 'member.remove', 'member.restore', 'access.grant', 'access.revoke'))
136
+ OR (p_kind = 'environment' AND change IN ('environment.protect', 'environment.rename', 'environment.archive'))
137
+ OR (p_kind = 'secret' AND change IN ('secret.write', 'secret.delete'))
138
+ OR (p_kind = 'roles' AND change = 'role.define')
139
+ OR (p_kind = 'policy' AND change = 'bulk.limit')) THEN
140
+ RAISE EXCEPTION 'this schema knows no change % to a %', coalesce(change, 'without an op'), p_kind USING ERRCODE = 'invalid_parameter_value';
141
+ END IF;
142
+ -- The payload names its object (`ref`), so that a replay knows what each change is to: the column holds only its
143
+ -- key. The cutover's genesis may leave it out, its value naming it.
144
+ IF (change <> 'genesis' OR op ? 'ref') AND (op -> 'ref' ->> 'kind' IS DISTINCT FROM p_kind OR op -> 'ref' ->> 'id' IS DISTINCT FROM p_id) THEN
145
+ RAISE EXCEPTION 'a change names the object it is to: %, %', p_kind, p_id USING ERRCODE = 'invalid_parameter_value';
146
+ END IF;
147
+ -- A value is the object as this function writes it: an environment's, which a genesis may leave out to seal the
148
+ -- row as it is; a key's, the end of its reference (`secret.write`, the one change of a key the vault makes), and
149
+ -- none at its genesis; the roles' and the policy's, but at their genesis, whose payload holds it. The others' rows
150
+ -- are written by whoever keeps them, and carry none.
151
+ IF (CASE
152
+ WHEN p_kind = 'environment' THEN change <> 'genesis' AND v IS NULL
153
+ WHEN p_kind = 'secret' THEN (change = 'secret.write') = (v IS NULL)
154
+ WHEN p_kind IN ('roles', 'policy') THEN (change = 'genesis') = (v IS NOT NULL)
155
+ ELSE v IS NOT NULL
156
+ END) THEN
157
+ RAISE EXCEPTION 'a % change % carries the object it leaves exactly when this function writes it', p_kind, change USING ERRCODE = 'invalid_parameter_value';
158
+ END IF;
159
+ -- ChangeChecksSeal: the row's seal is its object's newest change's, or the object has neither yet (one probe, as
160
+ -- a use makes). A row deleted outright, or a seal no change wrote, is stale too.
161
+ held := CASE p_kind
162
+ WHEN 'principal' THEN (SELECT m.seal FROM public.vault_members m WHERE m.principal = p_id)
163
+ WHEN 'credential' THEN coalesce(
164
+ (SELECT c.seal FROM public.credentials c WHERE c.id = p_id::uuid),
165
+ (SELECT b.seal FROM public.service_bindings b WHERE b.id = p_id::uuid))
166
+ WHEN 'environment' THEN (SELECT e.seal FROM public.environments e WHERE e.id = p_id::uuid)
167
+ WHEN 'secret' THEN (SELECT x.seal FROM public.secrets x WHERE x.id = p_id::uuid)
168
+ WHEN 'policy' THEN (SELECT g.seal FROM public.log_config g WHERE g.name = 'policy')
169
+ WHEN 'roles' THEN (SELECT g.seal FROM public.log_config g WHERE g.name = 'roles')
170
+ END;
171
+ newest := (SELECT c.seal FROM public.log_changes c WHERE c.object = target ORDER BY c.number DESC LIMIT 1);
172
+ IF held IS DISTINCT FROM newest THEN
173
+ RAISE EXCEPTION 'the % object''s row is not its newest change''s: stale', p_kind USING ERRCODE = 'data_corrupted';
174
+ END IF;
175
+ -- ChangeCAS: each write moves the row only from the seal the vault read.
176
+ CASE p_kind
177
+ WHEN 'principal' THEN
178
+ UPDATE public.vault_members SET seal = p_seal WHERE principal = p_id AND seal IS NOT DISTINCT FROM p_seal_from;
179
+ WHEN 'credential' THEN
180
+ -- A token's row, or a trust binding's: one kind of object, ids that never meet.
181
+ UPDATE public.credentials SET seal = p_seal WHERE id = p_id::uuid AND seal IS NOT DISTINCT FROM p_seal_from;
182
+ GET DIAGNOSTICS done = ROW_COUNT;
183
+ IF done = 0 THEN
184
+ UPDATE public.service_bindings SET seal = p_seal WHERE id = p_id::uuid AND seal IS NOT DISTINCT FROM p_seal_from;
185
+ END IF;
186
+ WHEN 'environment' THEN
187
+ IF v IS NULL THEN
188
+ UPDATE public.environments SET seal = p_seal WHERE id = p_id::uuid AND seal IS NOT DISTINCT FROM p_seal_from;
189
+ ELSE
190
+ -- A deleted environment's row keeps its tombstone, `dev~deleted-…` (@coffre/core/schemas's isTombstone),
191
+ -- and its object the slug before it (objects.ts, environmentObject): an environment deleted stays so,
192
+ -- its slug is never written again, and none is deleted here; an object's slug holds no tombstone.
193
+ UPDATE public.environments e
194
+ SET slug = CASE WHEN strpos(e.slug, '~') > 0 THEN e.slug ELSE v ->> 'slug' END,
195
+ protected = (v ->> 'protected')::boolean,
196
+ archived_at = CASE WHEN (v ->> 'archived')::boolean THEN coalesce(e.archived_at, to_timestamp(p_at / 1000.0)) END,
197
+ seal = p_seal
198
+ WHERE e.id = p_id::uuid AND e.seal IS NOT DISTINCT FROM p_seal_from
199
+ AND v ->> 'kind' = 'environment' AND v ->> 'id' = p_id
200
+ AND e.project_id = (v ->> 'projectId')::uuid
201
+ AND e.parent_id IS NOT DISTINCT FROM (v ->> 'parentId')::uuid
202
+ AND e.owner IS NOT DISTINCT FROM v ->> 'owner'
203
+ AND (strpos(e.slug, '~') > 0) = (v ->> 'deleted')::boolean
204
+ -- Its project's archival is the project's row's, which the app writes, and none once deleted (objects.ts): held,
205
+ -- never written here.
206
+ AND (v ->> 'projectArchived')::boolean = (strpos(e.slug, '~') = 0 AND EXISTS (SELECT 1 FROM public.projects p WHERE p.id = e.project_id AND p.archived_at IS NOT NULL))
207
+ AND strpos(v ->> 'slug', '~') = 0
208
+ AND (NOT (v ->> 'deleted')::boolean OR split_part(e.slug, '~', 1) = v ->> 'slug');
209
+ END IF;
210
+ WHEN 'secret' THEN
211
+ IF v IS NULL THEN
212
+ UPDATE public.secrets SET seal = p_seal WHERE id = p_id::uuid AND seal IS NOT DISTINCT FROM p_seal_from;
213
+ ELSE
214
+ -- A reference's end, in the vault's decision that appends 0.14's `reference.end` (EndInRow, EndWithEntry:
215
+ -- formal/SealedObjects.tla): the holder, a key with no version of its own, left with no source. Live, from
216
+ -- its seal, its newest reference row ended. That row must stand: its seal, which the check above held to
217
+ -- the newest change, says the holder reads its source; a row ended or gone under it is not the log's.
218
+ -- Archived, from none (EndWhileArchived): the object its unarchive would restore, ended, then deleted
219
+ -- again by the next call (`secret.delete`); its row was ended by its id (`end_reference`).
220
+ UPDATE public.secrets x SET seal = p_seal
221
+ WHERE x.id = p_id::uuid AND x.seal IS NOT DISTINCT FROM p_seal_from AND (x.archived_at IS NULL) = (p_seal_from IS NOT NULL)
222
+ AND x.current_version_id IS NULL
223
+ AND v ->> 'kind' = 'secret' AND v ->> 'id' = p_id AND x.project_id = (v ->> 'projectId')::uuid AND x.environment_id = (v ->> 'environmentId')::uuid
224
+ AND v ->> 'version' = '' AND v ->> 'nature' = 'secret' AND v -> 'key' = 'null'::jsonb AND v -> 'digest' = 'null'::jsonb AND v -> 'source' = 'null'::jsonb
225
+ AND v -> 'reference' = 'null'::jsonb;
226
+ GET DIAGNOSTICS done = ROW_COUNT;
227
+ IF done > 0 AND p_seal_from IS NOT NULL THEN
228
+ UPDATE public.secret_references y SET ended_at = to_timestamp(p_at / 1000.0)
229
+ WHERE y.id = (SELECT z.id FROM public.secret_references z WHERE z.secret_id = p_id::uuid ORDER BY z.created_seq DESC LIMIT 1) AND y.ended_at IS NULL;
230
+ GET DIAGNOSTICS done = ROW_COUNT;
231
+ IF done = 0 THEN
232
+ RAISE EXCEPTION 'the secret object''s reference has no standing row: stale' USING ERRCODE = 'data_corrupted';
233
+ END IF;
234
+ END IF;
235
+ END IF;
236
+ WHEN 'policy', 'roles' THEN
237
+ config := CASE change WHEN 'genesis' THEN op -> 'value' ELSE v END;
238
+ IF p_id <> 'instance' OR config IS NULL OR jsonb_typeof(config) <> 'object' OR (change = 'genesis' AND p_seal_from IS NOT NULL) THEN
239
+ RAISE EXCEPTION 'the % object is one, made once from its value and changed from its seal', p_kind USING ERRCODE = 'invalid_parameter_value';
240
+ END IF;
241
+ -- Made by its first change (genesis, or the first role defined, the first cap set); moved after from its seal.
242
+ IF p_seal_from IS NULL THEN
243
+ INSERT INTO public.log_config (name, value, seal) VALUES (p_kind, config::text, p_seal) ON CONFLICT (name) DO NOTHING;
244
+ ELSE
245
+ UPDATE public.log_config SET value = config::text, seal = p_seal WHERE log_config.name = p_kind AND seal = p_seal_from;
246
+ END IF;
247
+ ELSE
248
+ RAISE EXCEPTION 'no object kind %', p_kind USING ERRCODE = 'invalid_parameter_value';
249
+ END CASE;
250
+ -- A row gone, a row whose seal is no longer the one read, an environment that is not what the value says it
251
+ -- stays, or an object made already: nothing was done.
252
+ GET DIAGNOSTICS done = ROW_COUNT;
253
+ IF done = 0 THEN RETURN NULL; END IF;
254
+ INSERT INTO public.log_changes (number, at, request_id, object, actor, payload, seal, mac)
255
+ VALUES (nextval('public.log_number'), p_at, p_request, target, p_actor, p_payload, p_seal, p_mac)
256
+ RETURNING number INTO n;
257
+ RETURN n;
258
+ END
259
+ $$;
@@ -0,0 +1,65 @@
1
+ -- The app's reads before a use, in one call (T135; docs/design/0.15-plan.md, "As built: T135"): a request's credential, the newest policy change, the place it reads and its keys' rows, each what the app's own statements read (`credentialRow`, `resolvePath`, `environmentSecrets` in packages/server/src/db/queries.ts), as one JSON value. A function, so that each backend plans its statements once: planned anew for every request, the app's three statements cost 2.35 ms of Postgres CPU a request on a 2 vCPU Postgres, their plans most of it; this, 0.5 ms (measured, ibid.). One snapshot (STABLE): the place and the keys are read with the credential. It decides nothing: the app checks each row as it checks its own statements' (the MAC, what is live, what is inherited). Only 0.15's switch calls it: 0.14's code serves on it as before.
2
+ --
3
+ -- Planned once per backend (`force_generic_plan`), as the hot login's statements are: the plpgsql default plans each call again while a custom plan looks cheaper, which here was every call. Every read is a probe of a unique key, or an environment's keys by their index: `enable_seqscan = off` keeps a generic plan from reading a small table whole, whose pages grow with every row the instance gains (R48; proxy-use-cost.test.ts). `jit = off`, as the hot path's `use_credential` has it: a generic plan's estimate, a disabled scan's above all, can pass `jit_above_cost`, and compiling it costs a hundred times the read.
4
+ --
5
+ -- Each part is asked for or not: `p_token_hash` the credential (null: none); `p_project` and `p_environment` the place, by slugs, standing ones only (null: none); `p_keys` its keys, those it names, each once (null: every one). A personal config (`dev@me`), a key's own path and a tombstone are the app's own statements' to read; so are a fork's keys, and none is read for a token that names no credential. `bytea_output` pinned: the app reads each bytea as `\x`-hex.
6
+ CREATE FUNCTION app_read(p_token_hash bytea, p_project text, p_environment text, p_keys text[]) RETURNS text
7
+ LANGUAGE plpgsql STABLE SET search_path = pg_catalog, pg_temp SET plan_cache_mode = force_generic_plan SET enable_seqscan = off SET jit = off SET bytea_output = 'hex' AS $$
8
+ DECLARE
9
+ v_credential json;
10
+ v_identity json;
11
+ v_policy text;
12
+ v_project json;
13
+ v_environment json;
14
+ v_parent text;
15
+ v_project_id uuid;
16
+ v_environment_id uuid;
17
+ v_parent_id uuid;
18
+ v_live boolean;
19
+ v_rows json;
20
+ BEGIN
21
+ SELECT to_json(c), to_json(i), pol.seq::text, to_json(p), to_json(e), par.slug, p.id, e.id, e.parent_id, p.archived_at IS NULL AND e.archived_at IS NULL
22
+ INTO v_credential, v_identity, v_policy, v_project, v_environment, v_parent, v_project_id, v_environment_id, v_parent_id, v_live
23
+ FROM (SELECT 1) one
24
+ LEFT JOIN public.credentials c ON c.token_hash = p_token_hash
25
+ LEFT JOIN public.identities i ON i.id = c.identity_id
26
+ -- The newest policy change (`newestSeqQuery`), read with the credential, as `credentialRow` reads it.
27
+ LEFT JOIN LATERAL (
28
+ 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
29
+ ) pol ON p_token_hash IS NOT NULL
30
+ LEFT JOIN public.projects p ON p.slug = p_project AND p.slug NOT LIKE '%~%'
31
+ LEFT JOIN public.environments e ON e.project_id = p.id AND e.slug = p_environment AND e.slug NOT LIKE '%~%'
32
+ LEFT JOIN public.environments par ON par.id = e.parent_id;
33
+ -- Its keys as `environmentSecrets` reads them, each its row and its current version, in key order: none of an
34
+ -- archived project's or environment's. The named ones (distinct: the app's to say) one probe each, by the key's
35
+ -- index and the version's; two statements, as one plan serves only one shape. Not read (null) for a token that
36
+ -- names no credential, which is refused on that alone, so that an unknown one costs its probe and no more; nor of
37
+ -- a fork, whose layers the app reads together, in one snapshot (`readableValues`).
38
+ IF v_environment_id IS NOT NULL AND v_live AND (p_token_hash IS NULL OR v_credential IS NOT NULL) AND v_parent_id IS NULL THEN
39
+ IF p_keys IS NULL THEN
40
+ SELECT json_agg(json_build_object('secret', to_json(s), 'version', to_json(v)) ORDER BY s.key) INTO v_rows
41
+ FROM public.secrets s LEFT JOIN public.secret_versions v ON v.id = s.current_version_id
42
+ WHERE s.project_id = v_project_id AND s.environment_id = v_environment_id;
43
+ ELSE
44
+ SELECT json_agg(json_build_object('secret', to_json(s), 'version', to_json(v)) ORDER BY s.key) INTO v_rows
45
+ FROM unnest(p_keys) k (key)
46
+ -- LIMIT 1, a key being unique in its environment, keeps the lateral a probe by `k.key`: flattened, the planner
47
+ -- estimates an environment's keys as one row, of so many environments, and reads them all to join (review r4).
48
+ CROSS JOIN LATERAL (
49
+ SELECT * FROM public.secrets x WHERE x.project_id = v_project_id AND x.environment_id = v_environment_id AND x.key = k.key LIMIT 1
50
+ ) s
51
+ LEFT JOIN LATERAL (SELECT * FROM public.secret_versions y WHERE y.id = s.current_version_id) v ON true;
52
+ END IF;
53
+ END IF;
54
+ RETURN json_build_object(
55
+ 'credential', v_credential, 'identity', v_identity, 'policy', v_policy,
56
+ 'project', v_project, 'environment', v_environment, 'parent', v_parent,
57
+ 'secrets', CASE WHEN v_environment_id IS NULL OR (p_token_hash IS NOT NULL AND v_credential IS NULL) OR v_parent_id IS NOT NULL THEN NULL
58
+ WHEN v_live THEN coalesce(v_rows, '[]') ELSE '[]' END
59
+ )::text;
60
+ END;
61
+ $$;
62
+ --> statement-breakpoint
63
+ REVOKE ALL ON FUNCTION app_read(bytea, text, text, text[]) FROM PUBLIC;
64
+ --> statement-breakpoint
65
+ GRANT EXECUTE ON FUNCTION app_read(bytea, text, text, text[]) TO coffre_app;
@@ -0,0 +1,4 @@
1
+ -- An index for the reads of an environment's live keys that do not name its project (T152): the places tree's counts in every `me`, `liveKeysIn`, `distinctSecretCounts`. `secrets_lookup_idx` leads with the project, so such a read walked it end to end, or the table, every project's keys and every archived one in it. Partial, so that the archived keys an environment piles up are no part of it: a read of the live ones costs what they do. It only adds an index: 0.14's code serves on it as before.
2
+ --
3
+ -- A plain CREATE INDEX, not CONCURRENTLY, which cannot run in the transaction the migrator wraps a migration in: it blocks writes to `secrets` while it builds, a table of tens of thousands of rows in well under a second.
4
+ CREATE INDEX "secrets_live_idx" ON "secrets" USING btree ("environment_id") WHERE "archived_at" IS NULL;