@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,159 @@
1
+ -- The instance a binding was made on (T149, PR 3b; docs/design/credential-lifecycle.md, section 4b): its public URL when the vault sealed it, the audience its runs' tokens name, which its object's seal covers. Kept in its row, `origin`, so that the app names it as sealed whatever the instance's URL is now: a binding made on another URL is still the vault's to end, with what it issued, and signs nobody in through tokens for this one. Null on a binding made before this column (PR 3a's, unreleased; 0.14's, which the log never held), which the app names on its public URL. It adds a nullable column, which the previous release never reads, and redefines `change_bindings` to write it: only 0.15's vault calls it, and that vault is unreleased, as 0037 redefined 0034's `sign_in`.
2
+ ALTER TABLE "service_bindings" ADD COLUMN "origin" text;
3
+ --> statement-breakpoint
4
+ DROP FUNCTION change_bindings(bigint, text, boolean, text, integer, bigint, integer, bigint, uuid, bytea, bigint, uuid, integer, bigint, uuid[], text[], text, integer, uuid[], uuid[], text[], uuid, bytea, text, text, text, text, text, text, text, text, text, bigint, bytea, uuid[], bytea[], bytea[], bytea[], bytea[], uuid[], uuid[], bytea[], bytea[], bytea[], bytea[], bigint, text, text[], uuid[], text[], bytea[], bytea[]);
5
+ --> statement-breakpoint
6
+ CREATE FUNCTION change_bindings(
7
+ p_at bigint, p_actor text,
8
+ p_make boolean, p_principal text, p_generation integer, p_principal_seq bigint, p_actor_generation integer, p_actor_seq bigint,
9
+ p_via uuid, p_via_seal bytea, p_via_end bigint, p_connection uuid, p_connection_generation integer,
10
+ p_policy_seq bigint, p_protected uuid[], p_slugs text[], p_app_key text, p_max integer,
11
+ p_live uuid[], p_read_keys uuid[], p_read_names text[],
12
+ 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,
13
+ p_label text, p_created_by text, p_created_at bigint, p_seal bytea,
14
+ p_ends uuid[], p_end_mac_from bytea[], p_end_seal_from bytea[], p_end_auth_mac bytea[], p_end_seal bytea[],
15
+ 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[],
16
+ p_revoked_at bigint, p_revoked_by text,
17
+ p_change_request text[], p_change_object uuid[], p_change_payload text[], p_change_seal bytea[], p_change_mac bytea[]
18
+ ) RETURNS json
19
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET jit = off SET lock_timeout = '1s' AS $$
20
+ DECLARE
21
+ n bigint;
22
+ -- A value, not `clock_timestamp()` in a predicate: volatile there, it bounds no index.
23
+ now_ts timestamptz := clock_timestamp();
24
+ ended timestamptz := timestamptz 'epoch' + p_revoked_at * interval '1 millisecond';
25
+ BEGIN
26
+ IF p_make = (p_id IS NULL) OR (NOT p_make AND cardinality(p_ends) = 0) OR cardinality(p_creds) <> cardinality(p_cred_binding)
27
+ OR p_make = (p_live IS NULL) OR cardinality(p_read_keys) <> cardinality(p_read_names)
28
+ 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)
29
+ + (SELECT count(*) FROM unnest(p_cred_seal) AS s WHERE s IS NOT NULL) THEN
30
+ RAISE EXCEPTION 'a change of bindings makes one, ends some, or both, with a change for each object' USING ERRCODE = 'invalid_parameter_value';
31
+ END IF;
32
+ IF p_make THEN
33
+ -- IssuerLocked, IssuerCAS, as `issue_token`, by principal: the account's row first (`token:` sorts before `user:`),
34
+ -- its update lock, so that two binds of it take turns over its count of bindings (a sign-in through one of its
35
+ -- bindings waits for the statement too); then the maker's, shared. Then both compared.
36
+ 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;
37
+ PERFORM 1 FROM public.vault_members m WHERE m.principal = p_principal FOR NO KEY UPDATE;
38
+ PERFORM 1 FROM public.vault_members m WHERE m.principal = p_actor FOR SHARE;
39
+ 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)
40
+ 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
41
+ RETURN json_build_object('state', 'moved');
42
+ END IF;
43
+ IF p_via IS NOT NULL THEN
44
+ PERFORM 1 FROM public.credentials c WHERE c.id = p_via FOR SHARE;
45
+ IF NOT EXISTS (SELECT 1 FROM public.credentials c
46
+ WHERE c.id = p_via AND c.principal = p_actor AND c.revoked_at IS NULL AND c.seal = p_via_seal
47
+ AND floor(extract(epoch FROM least(c.expires_at, coalesce(c.not_after, c.expires_at))) * 1000)::bigint = p_via_end)
48
+ -- The clock once the locks are had, not when the statement began: one that ended while it waited has ended.
49
+ OR p_via_end <= (extract(epoch FROM clock_timestamp()) * 1000)::bigint
50
+ 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
51
+ RETURN json_build_object('state', 'ended');
52
+ END IF;
53
+ END IF;
54
+ -- The MCP connection it came through, still connected at the caller's generation (as `credentialStill`): shared,
55
+ -- so a disconnect waits for this, or is seen (review r3).
56
+ IF p_connection IS NOT NULL THEN
57
+ 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;
58
+ IF NOT FOUND THEN RETURN json_build_object('state', 'ended'); END IF;
59
+ END IF;
60
+ -- An id the log holds already is no binding to make, even once its row is gone: its object stands (review r3).
61
+ IF EXISTS (SELECT 1 FROM public.log_changes l WHERE l.object = public.log_object('binding', p_id::text)) THEN
62
+ RETURN json_build_object('state', 'stale');
63
+ END IF;
64
+ END IF;
65
+ -- The bindings it ends, then the credentials they issued: each row locked, by id, then held to its MAC and seal as
66
+ -- read. Every live credential they issued must be named, so one that a sign-in made meanwhile is read again (`moved`),
67
+ -- never left live.
68
+ PERFORM 1 FROM public.service_bindings b WHERE b.id = ANY(p_ends) ORDER BY b.id FOR UPDATE;
69
+ IF (SELECT count(*) FROM unnest(p_ends, p_end_mac_from, p_end_seal_from) AS e(id, mac, seal)
70
+ 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)
71
+ <> cardinality(p_ends) THEN
72
+ RETURN json_build_object('state', 'moved');
73
+ END IF;
74
+ -- A credential 0.14 made, or one written around the vault, has no seal: ended with its binding all the same, its row
75
+ -- revoked with no change, the log holding no object of it.
76
+ PERFORM 1 FROM public.credentials c WHERE c.id = ANY(p_creds) ORDER BY c.id FOR UPDATE;
77
+ IF (SELECT count(*) FROM unnest(p_creds, p_cred_binding, p_cred_mac_from, p_cred_seal_from) AS x(id, binding, mac, seal)
78
+ JOIN public.credentials c ON c.id = x.id AND x.binding = ANY(p_ends) AND c.created_by = 'binding:' || x.binding::text
79
+ AND c.revoked_at IS NULL AND c.auth_mac = x.mac AND c.seal IS NOT DISTINCT FROM x.seal)
80
+ <> cardinality(p_creds)
81
+ OR EXISTS (SELECT 1 FROM unnest(p_ends) AS e(id) JOIN public.credentials c
82
+ 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
83
+ WHERE NOT (c.id = ANY(p_creds))) THEN
84
+ RETURN json_build_object('state', 'moved');
85
+ END IF;
86
+ IF p_make THEN
87
+ -- The account's live bindings as the app read them, which it weighed this one beside (an issuer's keys moved, the
88
+ -- bindings it replaces): another made or ended since is read again. Under the account's row's lock, which every
89
+ -- bind takes, so two binds take turns. At most `p_max`, those it ends left out.
90
+ 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)
91
+ IS DISTINCT FROM ARRAY(SELECT l FROM unnest(p_live) AS l ORDER BY l) THEN
92
+ RETURN json_build_object('state', 'moved');
93
+ END IF;
94
+ IF cardinality(p_live) - (SELECT count(*) FROM unnest(p_live) AS l WHERE l = ANY(p_ends)) >= p_max THEN
95
+ RETURN json_build_object('state', 'full');
96
+ END IF;
97
+ END IF;
98
+ -- IssuePolicyHeld: the issue lock, shared; then, under it, the policy and the slugs this change was weighed on, and the numbers.
99
+ PERFORM pg_advisory_xact_lock_shared(1668244070, 1);
100
+ 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)
101
+ IS DISTINCT FROM p_policy_seq
102
+ OR EXISTS (
103
+ SELECT 1 FROM unnest(p_protected, p_slugs) AS p(id, slug)
104
+ WHERE (SELECT e.slug FROM public.environments e JOIN public.projects q ON q.id = e.project_id
105
+ WHERE e.id = p.id AND strpos(e.slug, '~') = 0 AND strpos(q.slug, '~') = 0) IS DISTINCT FROM p.slug
106
+ ) THEN
107
+ RETURN json_build_object('state', 'policy');
108
+ END IF;
109
+ 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
110
+ RETURN json_build_object('state', 'retired');
111
+ END IF;
112
+ -- What it ends is a use's input: the fence exclusive (RevokeTakesFence), after the issue lock (PolicyIssueFirst), for
113
+ -- the statement's work only. A bind that ends nothing takes none.
114
+ IF cardinality(p_ends) > 0 THEN PERFORM pg_advisory_xact_lock(1668244070, 0); END IF;
115
+ -- The keys a binding it makes reads, by the names they were asked by: still those names (a rename meanwhile names
116
+ -- another). Compared, not locked (a key's writer takes the fence before its row: a share lock here, held into the
117
+ -- fence, would be a cycle; review r3), and once the fence is had, which a rename takes too: one that committed while
118
+ -- this waited is seen (review r4). With no fence, one that commits after this is a rename after the bind.
119
+ 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
120
+ RETURN json_build_object('state', 'moved');
121
+ END IF;
122
+ -- ChangeChecksSeal: each row it ends is its object's newest change's; one put back is stale. A row with no seal is one
123
+ -- 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
124
+ -- log's binding is never ended with no change of it (review r2).
125
+ IF EXISTS (SELECT 1 FROM unnest(p_ends, p_end_seal_from) AS e(id, seal)
126
+ 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))
127
+ OR EXISTS (SELECT 1 FROM unnest(p_creds, p_cred_seal_from) AS x(id, seal)
128
+ 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
129
+ RETURN json_build_object('state', 'stale');
130
+ END IF;
131
+ IF p_make THEN
132
+ -- Again, now that no lock is left to wait for: a make of the same id that committed meanwhile, its row deleted since,
133
+ -- is seen; one not yet committed holds the id's key, which this insert then waits for and fails on (review r4).
134
+ IF EXISTS (SELECT 1 FROM public.log_changes l WHERE l.object = public.log_object('binding', p_id::text)) THEN
135
+ RETURN json_build_object('state', 'stale');
136
+ END IF;
137
+ 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)
138
+ 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,
139
+ timestamptz 'epoch' + p_created_at * interval '1 millisecond', p_seal);
140
+ END IF;
141
+ UPDATE public.service_bindings b SET revoked_at = ended, revoked_by = p_revoked_by, auth_mac = e.mac, seal = e.seal
142
+ FROM unnest(p_ends, p_end_auth_mac, p_end_seal) AS e(id, mac, seal) WHERE b.id = e.id;
143
+ UPDATE public.credentials c SET revoked_at = ended, revoked_by = p_revoked_by, auth_mac = x.mac, seal = x.seal
144
+ FROM unnest(p_creds, p_cred_auth_mac, p_cred_seal) AS x(id, mac, seal) WHERE c.id = x.id;
145
+ -- Each object's change, numbered in the order named: the binding made, those ended, their credentials.
146
+ FOR i IN 1 .. cardinality(p_change_request) LOOP
147
+ INSERT INTO public.log_changes (number, at, request_id, object, actor, payload, seal, mac)
148
+ 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])
149
+ RETURNING number INTO n;
150
+ END LOOP;
151
+ -- Only rows the log never held, ended: no change, nothing to seal.
152
+ RETURN json_build_object('number', coalesce(n::text, '0'));
153
+ END
154
+ $$;
155
+
156
+ --> statement-breakpoint
157
+ REVOKE ALL ON FUNCTION change_bindings(bigint, text, boolean, text, integer, bigint, integer, bigint, uuid, bytea, bigint, uuid, integer, bigint, uuid[], text[], text, integer, uuid[], uuid[], text[], uuid, bytea, text, text, text, text, text, text, text, text, text, text, bigint, bytea, uuid[], bytea[], bytea[], bytea[], bytea[], uuid[], uuid[], bytea[], bytea[], bytea[], bytea[], bigint, text, text[], uuid[], text[], bytea[], bytea[]) FROM PUBLIC;
158
+ --> statement-breakpoint
159
+ GRANT EXECUTE ON FUNCTION change_bindings(bigint, text, boolean, text, integer, bigint, integer, bigint, uuid, bytea, bigint, uuid, integer, bigint, uuid[], text[], text, integer, uuid[], uuid[], text[], uuid, bytea, text, text, text, text, text, text, text, text, text, text, bigint, bytea, uuid[], bytea[], bytea[], bytea[], bytea[], uuid[], uuid[], bytea[], bytea[], bytea[], bytea[], bigint, text, text[], uuid[], text[], bytea[], bytea[]) TO coffre_vault;
@@ -0,0 +1,132 @@
1
+ -- 0.15's cutover from 0.14 (docs/design/0.15-plan.md, PR 9; formal/Migration015.tla): the first 0.15 vault start seals every object 0.14 left as 0.14 accepts it (`genesis`, through `change_input`), and writes the log's first `cutover` change, naming 0.14's last head. These are its writes the vault's login cannot make, and its reads of the rows only the app's login reads, each a definer function that holds itself to what a cutover does: only before the cutover's change, on rows no change of 0.15's holds. Only functions: 0.14's code serves on it as before.
2
+ --> statement-breakpoint
3
+ -- The cutover's change, once: under the fence, exclusive, which the cutover holds from before its marker's check (its locks: every member's row, 0.14's head, the issue lock, the fence; T132). It names 0.14's last head, which must be the chain's head now (`audit_chain_head`): the cutover holds it, and its own 0.14 entry, `log.cutover`, is that head. A retry by its request id finds it; any other cutover is refused.
4
+ CREATE FUNCTION log_cutover(p_request text, p_at bigint, p_actor text, p_payload text, p_mac bytea) RETURNS bigint
5
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET jit = off AS $$
6
+ DECLARE
7
+ op jsonb := p_payload::jsonb;
8
+ n bigint;
9
+ BEGIN
10
+ IF op ->> 'op' IS DISTINCT FROM 'cutover' OR (op #>> '{anchor,seq}') !~ '^[0-9]{1,19}$' OR (op #>> '{anchor,hash}') !~ '^[0-9a-f]{64}$' THEN
11
+ RAISE EXCEPTION 'not a cutover change: %', p_payload USING ERRCODE = 'invalid_parameter_value';
12
+ END IF;
13
+ PERFORM pg_advisory_xact_lock(1668244070, 0);
14
+ SELECT c.number INTO n FROM public.log_changes c WHERE c.request_id = p_request AND c.object = public.log_object('log', 'cutover') AND c.payload = p_payload;
15
+ IF n IS NOT NULL THEN RETURN n; END IF;
16
+ IF EXISTS (SELECT 1 FROM public.log_changes c WHERE c.object = public.log_object('log', 'cutover')) THEN
17
+ RAISE EXCEPTION 'the log is cut over from 0.14 already' USING ERRCODE = 'unique_violation';
18
+ END IF;
19
+ IF NOT EXISTS (
20
+ SELECT 1 FROM public.audit_chain_head h
21
+ WHERE h.only_row AND h.next_seq = (op #>> '{anchor,seq}')::bigint + 1 AND h.head_hash = decode(op #>> '{anchor,hash}', 'hex')
22
+ ) THEN
23
+ RAISE EXCEPTION 'a cutover names 0.14''s head as it stands' USING ERRCODE = 'invalid_parameter_value';
24
+ END IF;
25
+ INSERT INTO public.log_changes (number, at, request_id, object, actor, payload, seal, mac)
26
+ VALUES (nextval('public.log_number'), p_at, p_request, public.log_object('log', 'cutover'), p_actor, p_payload, NULL, p_mac)
27
+ RETURNING number INTO n;
28
+ RETURN n;
29
+ END
30
+ $$;
31
+ --> statement-breakpoint
32
+ -- The places 0.14 deleted, made as 0.15 leaves them (the plan's "Places 0.14 deleted", T130), before their objects' genesis: each standing environment of a project whose slug is a tombstone gets `<slug>~deleted` (`deletedWithProject`), and each key under a place deleted that is not archived is archived, its versions erased already. Only before the cutover, and only on rows no change of 0.15's holds; each one named must be such, or nothing is done.
33
+ CREATE FUNCTION cutover_places(p_at bigint, p_environments uuid[], p_secrets uuid[]) RETURNS void
34
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET jit = off AS $$
35
+ DECLARE
36
+ done integer;
37
+ BEGIN
38
+ IF p_at IS NULL OR p_at < 0 THEN
39
+ RAISE EXCEPTION 'the cutover writes at a time, ms since the epoch' USING ERRCODE = 'invalid_parameter_value';
40
+ END IF;
41
+ IF EXISTS (SELECT 1 FROM public.log_changes c WHERE c.object = public.log_object('log', 'cutover')) THEN
42
+ RAISE EXCEPTION 'the log is cut over from 0.14 already' USING ERRCODE = 'invalid_parameter_value';
43
+ END IF;
44
+ UPDATE public.environments e SET slug = e.slug || '~deleted'
45
+ WHERE e.id = ANY (p_environments) AND strpos(e.slug, '~') = 0 AND e.seal IS NULL
46
+ AND NOT EXISTS (SELECT 1 FROM public.log_changes c WHERE c.object = public.log_object('environment', e.id::text))
47
+ AND EXISTS (SELECT 1 FROM public.projects p WHERE p.id = e.project_id AND strpos(p.slug, '~') > 0);
48
+ GET DIAGNOSTICS done = ROW_COUNT;
49
+ IF done <> coalesce(cardinality(p_environments), 0) THEN
50
+ RAISE EXCEPTION 'an environment named is no standing one of a deleted project, 0.14''s alone' USING ERRCODE = 'invalid_parameter_value';
51
+ END IF;
52
+ UPDATE public.secrets x SET archived_at = to_timestamp(p_at / 1000.0)
53
+ WHERE x.id = ANY (p_secrets) AND x.archived_at IS NULL AND x.seal IS NULL
54
+ AND NOT EXISTS (SELECT 1 FROM public.log_changes c WHERE c.object = public.log_object('secret', x.id::text))
55
+ AND EXISTS (
56
+ SELECT 1 FROM public.environments e JOIN public.projects p ON p.id = e.project_id
57
+ WHERE e.id = x.environment_id AND (strpos(e.slug, '~') > 0 OR strpos(p.slug, '~') > 0)
58
+ );
59
+ GET DIAGNOSTICS done = ROW_COUNT;
60
+ IF done <> coalesce(cardinality(p_secrets), 0) THEN
61
+ RAISE EXCEPTION 'a key named is no live one under a deleted place, 0.14''s alone' USING ERRCODE = 'invalid_parameter_value';
62
+ END IF;
63
+ END
64
+ $$;
65
+ --> statement-breakpoint
66
+ -- The credentials and bindings the app vouched for, as the cutover reads them under its locks to hold them to that: their objects' columns, their seals, and whether the log has any change of them. The vault's login reads neither table (0036's `issuing_credential` for one).
67
+ CREATE FUNCTION cutover_credentials(p_ids uuid[]) RETURNS TABLE (
68
+ id uuid, principal text, kind text, scope text, revoked boolean, expires_at bigint, not_after bigint, generation integer, seal bytea, logged boolean
69
+ )
70
+ LANGUAGE sql STABLE SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET jit = off AS $$
71
+ SELECT c.id, c.principal, c.kind, c.scope, c.revoked_at IS NOT NULL, floor(extract(epoch FROM c.expires_at) * 1000)::bigint,
72
+ floor(extract(epoch FROM c.not_after) * 1000)::bigint, c.generation, c.seal,
73
+ EXISTS (SELECT 1 FROM public.log_changes l WHERE l.object = public.log_object('credential', c.id::text))
74
+ FROM public.credentials c WHERE c.id = ANY (p_ids)
75
+ $$;
76
+ --> statement-breakpoint
77
+ CREATE FUNCTION cutover_bindings(p_ids uuid[]) RETURNS TABLE (
78
+ id uuid, principal text, generation integer, profile text, issuer text, origin text, jwks_uri text, claims text, scope text, policy text, revoked boolean, seal bytea, logged boolean
79
+ )
80
+ LANGUAGE sql STABLE SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET jit = off AS $$
81
+ SELECT b.id, b.principal, b.generation, b.profile, b.issuer, b.origin, b.jwks_uri, b.claims, b.scope, b.policy, b.revoked_at IS NOT NULL, b.seal,
82
+ EXISTS (SELECT 1 FROM public.log_changes l WHERE l.object = public.log_object('binding', b.id::text))
83
+ FROM public.service_bindings b WHERE b.id = ANY (p_ids)
84
+ $$;
85
+ --> statement-breakpoint
86
+ -- A binding's genesis (T160): 0.14's row, which the app vouched for and the vault held to that, sealed as it is, from no seal, the object the payload's value, under the fence; and the instance it was made on, `origin` (0040), written with its seal: the deployment's public URL, as the app vouched for it. 0.14 writes no `origin`, and its MAC covers none: a row with one is no row of 0.14's, and is not sealed. Only before the cutover's change; never a binding the log holds (PR 3a's `change_bindings` makes and ends those).
87
+ CREATE 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
88
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET jit = off AS $$
89
+ DECLARE
90
+ op jsonb := p_payload::jsonb;
91
+ target uuid := public.log_object('binding', p_id::text);
92
+ n bigint;
93
+ done integer;
94
+ BEGIN
95
+ 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
96
+ 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
97
+ RAISE EXCEPTION 'not a binding''s genesis: %', p_payload USING ERRCODE = 'invalid_parameter_value';
98
+ END IF;
99
+ PERFORM 1 FROM public.audit_chain_head WHERE only_row FOR UPDATE;
100
+ PERFORM pg_advisory_xact_lock(1668244070, 0);
101
+ SELECT c.number INTO n FROM public.log_changes c WHERE c.request_id = p_request;
102
+ IF FOUND THEN
103
+ 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
104
+ RAISE EXCEPTION 'request % names another change', p_request USING ERRCODE = 'unique_violation';
105
+ END IF;
106
+ RETURN n;
107
+ END IF;
108
+ IF EXISTS (SELECT 1 FROM public.log_changes c WHERE c.object = public.log_object('log', 'cutover')) THEN
109
+ RAISE EXCEPTION 'the log is cut over from 0.14 already: a binding is the vault''s to make' USING ERRCODE = 'invalid_parameter_value';
110
+ END IF;
111
+ IF EXISTS (SELECT 1 FROM public.log_changes c WHERE c.object = target) THEN
112
+ RETURN NULL;
113
+ END IF;
114
+ UPDATE public.service_bindings b SET seal = p_seal, origin = op #>> '{value,origin}'
115
+ WHERE b.id = p_id AND b.seal IS NULL AND b.revoked_at IS NULL AND b.origin IS NULL
116
+ AND b.principal = op #>> '{value,principal}' AND b.generation = (op #>> '{value,generation}')::integer
117
+ AND b.profile = op #>> '{value,profile}' AND b.issuer = op #>> '{value,issuer}' AND b.jwks_uri = op #>> '{value,jwksUri}'
118
+ AND b.claims = op #>> '{value,claims}' AND b.policy IS NOT DISTINCT FROM op #>> '{value,policy}';
119
+ GET DIAGNOSTICS done = ROW_COUNT;
120
+ IF done = 0 THEN RETURN NULL; END IF;
121
+ INSERT INTO public.log_changes (number, at, request_id, object, actor, payload, seal, mac)
122
+ VALUES (nextval('public.log_number'), p_at, p_request, target, p_actor, p_payload, p_seal, p_mac)
123
+ RETURNING number INTO n;
124
+ RETURN n;
125
+ END
126
+ $$;
127
+ --> statement-breakpoint
128
+ REVOKE ALL ON FUNCTION log_cutover(text, bigint, text, text, bytea), cutover_places(bigint, uuid[], uuid[]), cutover_credentials(uuid[]), cutover_bindings(uuid[]),
129
+ seal_binding(uuid, text, bigint, text, text, bytea, bytea) FROM PUBLIC;
130
+ --> statement-breakpoint
131
+ GRANT EXECUTE ON FUNCTION log_cutover(text, bigint, text, text, bytea), cutover_places(bigint, uuid[], uuid[]), cutover_credentials(uuid[]), cutover_bindings(uuid[]),
132
+ seal_binding(uuid, text, bigint, text, text, bytea, bytea) TO coffre_vault;
@@ -0,0 +1,7 @@
1
+ -- The audit pages list the sign-ins through trust bindings (T156): with 0.15's log, a sign-in writes no 0.14 `token.exchange` entry, and its change is the record (D147), so the app reads the changes, as it reads the vault's members and grants. Two indexes over the sign-ins alone, every service's and one's, newest first by when the vault asked (`at`, then the number, which comes after any wait), so that a page reads as many as it shows however many other changes the log holds. Only a sign-in's payload holds a `signIn` object (`SIGNED_IN` in @coffre/server's queries repeats the condition). Additive: the previous release reads nothing here.
2
+ --> statement-breakpoint
3
+ CREATE INDEX "log_changes_sign_in_idx" ON "log_changes" USING btree ("at","number") WHERE strpos("log_changes"."payload", '"signIn":{') > 0;
4
+ --> statement-breakpoint
5
+ CREATE INDEX "log_changes_sign_in_actor_idx" ON "log_changes" USING btree ("actor","at","number") WHERE strpos("log_changes"."payload", '"signIn":{') > 0;
6
+ --> statement-breakpoint
7
+ GRANT SELECT ON log_changes TO coffre_app;
@@ -0,0 +1,4 @@
1
+ -- The audit pages list the vault's token issues, revokes and sign-outs (T164): with 0.15's log, none writes 0.14's `token.create`, `token.revoke` or `sign_out` entry, and its change is the record (D147), which the app reads as it reads the sign-ins (0042). Two indexes over those changes alone, every member's and one's, ordered as the sign-ins' (`at`, then the number), so that a page reads as many as it shows, and a page that hides them counts no more than its stretch holds, however many other changes the log holds. Each is known by how its payload starts, an issue's or a revoke's, and by the record it holds: an `issue` object, or a reason of `revoke` or `sign_out` (`RECORDS` in @coffre/db's change-records.ts, which the reads repeat). Additive: the previous release reads nothing here.
2
+ --> statement-breakpoint
3
+ CREATE INDEX "log_changes_issue_revoke_idx" ON "log_changes" USING btree ("at","number") WHERE ((strpos("log_changes"."payload", '{"caller":"') = 1 AND strpos("log_changes"."payload", '"issue":{"by":"') > 0) OR (strpos("log_changes"."payload", '{"op":"credential.revoke",') = 1 AND strpos("log_changes"."payload", '"reason":"revoke"') > 0) OR (strpos("log_changes"."payload", '{"op":"credential.revoke",') = 1 AND strpos("log_changes"."payload", '"reason":"sign_out"') > 0));--> statement-breakpoint
4
+ CREATE INDEX "log_changes_issue_revoke_actor_idx" ON "log_changes" USING btree ("actor","at","number") WHERE ((strpos("log_changes"."payload", '{"caller":"') = 1 AND strpos("log_changes"."payload", '"issue":{"by":"') > 0) OR (strpos("log_changes"."payload", '{"op":"credential.revoke",') = 1 AND strpos("log_changes"."payload", '"reason":"revoke"') > 0) OR (strpos("log_changes"."payload", '{"op":"credential.revoke",') = 1 AND strpos("log_changes"."payload", '"reason":"sign_out"') > 0));