@serfab/cadre-core 1.16.0 → 1.18.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 (268) hide show
  1. package/README.md +4 -3
  2. package/dist/bootstrap-peer-store-file.js +1 -1
  3. package/dist/bootstrap-peer-store.d.ts +1 -1
  4. package/dist/bootstrap-peer-store.js +4 -4
  5. package/dist/cadre-invite-protocol.d.ts +81 -142
  6. package/dist/cadre-invite-protocol.js +168 -458
  7. package/dist/cadre-invite-protocol.js.map +1 -1
  8. package/dist/cadre-join-request.d.ts +15 -2
  9. package/dist/cadre-join-request.js +22 -3
  10. package/dist/cadre-join-request.js.map +1 -1
  11. package/dist/cadre-node.d.ts +1089 -475
  12. package/dist/cadre-node.js +2170 -1037
  13. package/dist/cadre-node.js.map +1 -1
  14. package/dist/control-authorization.d.ts +36 -14
  15. package/dist/control-authorization.js +20 -3
  16. package/dist/control-authorization.js.map +1 -1
  17. package/dist/control-cohort.d.ts +88 -32
  18. package/dist/control-cohort.js +91 -33
  19. package/dist/control-cohort.js.map +1 -1
  20. package/dist/control-database.d.ts +856 -318
  21. package/dist/control-database.js +1422 -701
  22. package/dist/control-database.js.map +1 -1
  23. package/dist/control-formation-recorder.d.ts +46 -43
  24. package/dist/control-formation-recorder.js +77 -78
  25. package/dist/control-formation-recorder.js.map +1 -1
  26. package/dist/control-protocol-guard.d.ts +11 -4
  27. package/dist/control-protocol-guard.js +20 -5
  28. package/dist/control-protocol-guard.js.map +1 -1
  29. package/dist/control-schema-version.d.ts +97 -0
  30. package/dist/control-schema-version.js +225 -0
  31. package/dist/control-schema-version.js.map +1 -0
  32. package/dist/control-schema.d.ts +45 -5
  33. package/dist/control-schema.js +262 -78
  34. package/dist/control-schema.js.map +1 -1
  35. package/dist/control-write-retry.d.ts +45 -10
  36. package/dist/control-write-retry.js +92 -22
  37. package/dist/control-write-retry.js.map +1 -1
  38. package/dist/delegate-admission.d.ts +13 -0
  39. package/dist/delegate-admission.js +27 -0
  40. package/dist/delegate-admission.js.map +1 -1
  41. package/dist/ed25519-key.d.ts +10 -7
  42. package/dist/ed25519-key.js +23 -20
  43. package/dist/ed25519-key.js.map +1 -1
  44. package/dist/edge-node/config.d.ts +122 -0
  45. package/dist/edge-node/config.js +199 -0
  46. package/dist/edge-node/config.js.map +1 -0
  47. package/dist/edge-node/dissolve.d.ts +95 -0
  48. package/dist/edge-node/dissolve.js +184 -0
  49. package/dist/edge-node/dissolve.js.map +1 -0
  50. package/dist/edge-node/edge-join.d.ts +44 -0
  51. package/dist/edge-node/edge-join.js +227 -0
  52. package/dist/edge-node/edge-join.js.map +1 -0
  53. package/dist/edge-node/edge-node.d.ts +295 -0
  54. package/dist/edge-node/edge-node.js +704 -0
  55. package/dist/edge-node/edge-node.js.map +1 -0
  56. package/dist/edge-node/index.d.ts +18 -0
  57. package/dist/edge-node/index.js +18 -0
  58. package/dist/edge-node/index.js.map +1 -0
  59. package/dist/edge-node/join-credential.d.ts +11 -0
  60. package/dist/edge-node/join-credential.js +11 -0
  61. package/dist/edge-node/join-credential.js.map +1 -0
  62. package/dist/edge-node/lazy-raw-storage.d.ts +40 -0
  63. package/dist/edge-node/lazy-raw-storage.js +134 -0
  64. package/dist/edge-node/lazy-raw-storage.js.map +1 -0
  65. package/dist/edge-node/node-local.d.ts +60 -0
  66. package/dist/edge-node/node-local.js +58 -0
  67. package/dist/edge-node/node-local.js.map +1 -0
  68. package/dist/edge-node/options.d.ts +83 -0
  69. package/dist/edge-node/options.js +110 -0
  70. package/dist/edge-node/options.js.map +1 -0
  71. package/dist/edge-node/owner-grant.d.ts +41 -0
  72. package/dist/edge-node/owner-grant.js +38 -0
  73. package/dist/edge-node/owner-grant.js.map +1 -0
  74. package/dist/edge-node/platform.d.ts +140 -0
  75. package/dist/edge-node/platform.js +12 -0
  76. package/dist/edge-node/platform.js.map +1 -0
  77. package/dist/edge-node/strand-move.d.ts +115 -0
  78. package/dist/edge-node/strand-move.js +223 -0
  79. package/dist/edge-node/strand-move.js.map +1 -0
  80. package/dist/edge-node/strands.d.ts +28 -0
  81. package/dist/edge-node/strands.js +60 -0
  82. package/dist/edge-node/strands.js.map +1 -0
  83. package/dist/enrolled-machine-store.d.ts +3 -3
  84. package/dist/enrolled-machine-store.js +3 -3
  85. package/dist/formation-token.d.ts +48 -0
  86. package/dist/formation-token.js +26 -0
  87. package/dist/formation-token.js.map +1 -0
  88. package/dist/index.d.ts +27 -17
  89. package/dist/index.js +39 -17
  90. package/dist/index.js.map +1 -1
  91. package/dist/joined-strand-store.d.ts +29 -12
  92. package/dist/joined-strand-store.js +66 -27
  93. package/dist/joined-strand-store.js.map +1 -1
  94. package/dist/key-store.d.ts +9 -4
  95. package/dist/key-store.js +9 -4
  96. package/dist/key-store.js.map +1 -1
  97. package/dist/link-budget.d.ts +9 -7
  98. package/dist/link-budget.js +9 -7
  99. package/dist/link-budget.js.map +1 -1
  100. package/dist/machine-removal.d.ts +19 -39
  101. package/dist/machine-removal.js +24 -98
  102. package/dist/machine-removal.js.map +1 -1
  103. package/dist/member-redemption.d.ts +256 -0
  104. package/dist/member-redemption.js +490 -0
  105. package/dist/member-redemption.js.map +1 -0
  106. package/dist/membership-view.d.ts +91 -0
  107. package/dist/membership-view.js +110 -0
  108. package/dist/membership-view.js.map +1 -0
  109. package/dist/owner-anchor-sync.d.ts +4 -4
  110. package/dist/owner-anchor-sync.js +16 -15
  111. package/dist/owner-anchor-sync.js.map +1 -1
  112. package/dist/owner-grant-protocol.d.ts +251 -0
  113. package/dist/owner-grant-protocol.js +344 -0
  114. package/dist/owner-grant-protocol.js.map +1 -0
  115. package/dist/owner-key-export.d.ts +70 -0
  116. package/dist/owner-key-export.js +279 -0
  117. package/dist/owner-key-export.js.map +1 -0
  118. package/dist/owner-key-flows.d.ts +55 -0
  119. package/dist/owner-key-flows.js +60 -0
  120. package/dist/owner-key-flows.js.map +1 -0
  121. package/dist/owner-key-store.d.ts +31 -0
  122. package/dist/owner-key-store.js +81 -0
  123. package/dist/owner-key-store.js.map +1 -0
  124. package/dist/owner-keys.d.ts +78 -0
  125. package/dist/owner-keys.js +133 -0
  126. package/dist/owner-keys.js.map +1 -0
  127. package/dist/owner-signer.d.ts +54 -0
  128. package/dist/owner-signer.js +51 -0
  129. package/dist/owner-signer.js.map +1 -0
  130. package/dist/peer-addr-book.d.ts +10 -1
  131. package/dist/peer-addr-book.js +4 -3
  132. package/dist/peer-addr-book.js.map +1 -1
  133. package/dist/peer-authorization.d.ts +83 -18
  134. package/dist/peer-authorization.js +123 -53
  135. package/dist/peer-authorization.js.map +1 -1
  136. package/dist/peer-join-backfill.d.ts +89 -20
  137. package/dist/peer-join-backfill.js +265 -108
  138. package/dist/peer-join-backfill.js.map +1 -1
  139. package/dist/pending-join-runner.d.ts +48 -18
  140. package/dist/pending-join-runner.js +74 -56
  141. package/dist/pending-join-runner.js.map +1 -1
  142. package/dist/provider/rent-node.js +1 -1
  143. package/dist/provider/rent-node.js.map +1 -1
  144. package/dist/seed-bootstrap.d.ts +162 -133
  145. package/dist/seed-bootstrap.js +300 -283
  146. package/dist/seed-bootstrap.js.map +1 -1
  147. package/dist/seed-trust-policy.js +4 -4
  148. package/dist/strand-addr-protocol.d.ts +4 -1
  149. package/dist/strand-addr-protocol.js +8 -2
  150. package/dist/strand-addr-protocol.js.map +1 -1
  151. package/dist/strand-cohort.d.ts +3 -0
  152. package/dist/strand-cohort.js.map +1 -1
  153. package/dist/strand-database.d.ts +24 -2
  154. package/dist/strand-database.js +29 -6
  155. package/dist/strand-database.js.map +1 -1
  156. package/dist/strand-first-sync-gate.d.ts +176 -88
  157. package/dist/strand-first-sync-gate.js +288 -121
  158. package/dist/strand-first-sync-gate.js.map +1 -1
  159. package/dist/strand-formation-deadlines.d.ts +9 -9
  160. package/dist/strand-formation-deadlines.js +9 -9
  161. package/dist/strand-formation-manager.d.ts +23 -17
  162. package/dist/strand-formation-manager.js +46 -44
  163. package/dist/strand-formation-manager.js.map +1 -1
  164. package/dist/strand-formation-protocol.d.ts +5 -8
  165. package/dist/strand-formation-protocol.js +101 -16
  166. package/dist/strand-formation-protocol.js.map +1 -1
  167. package/dist/strand-instance-manager.d.ts +27 -18
  168. package/dist/strand-instance-manager.js +119 -50
  169. package/dist/strand-instance-manager.js.map +1 -1
  170. package/dist/strand-membership-reconciler.d.ts +16 -13
  171. package/dist/strand-membership-reconciler.js +30 -22
  172. package/dist/strand-membership-reconciler.js.map +1 -1
  173. package/dist/strand-membership-writer.d.ts +45 -22
  174. package/dist/strand-membership-writer.js +226 -131
  175. package/dist/strand-membership-writer.js.map +1 -1
  176. package/dist/strand-move.d.ts +6 -5
  177. package/dist/strand-move.js +36 -32
  178. package/dist/strand-move.js.map +1 -1
  179. package/dist/strand-peer-redial.js +2 -3
  180. package/dist/strand-peer-redial.js.map +1 -1
  181. package/dist/strand-revocation-enforcer.d.ts +134 -60
  182. package/dist/strand-revocation-enforcer.js +144 -71
  183. package/dist/strand-revocation-enforcer.js.map +1 -1
  184. package/dist/strand-solicitation.d.ts +50 -39
  185. package/dist/strand-solicitation.js +10 -3
  186. package/dist/strand-solicitation.js.map +1 -1
  187. package/dist/strand-watcher.d.ts +80 -13
  188. package/dist/strand-watcher.js +152 -48
  189. package/dist/strand-watcher.js.map +1 -1
  190. package/dist/table-change-watch.d.ts +63 -0
  191. package/dist/table-change-watch.js +209 -0
  192. package/dist/table-change-watch.js.map +1 -0
  193. package/dist/timeout-scheduler.d.ts +4 -3
  194. package/dist/timeout-scheduler.js +4 -3
  195. package/dist/timeout-scheduler.js.map +1 -1
  196. package/dist/types.d.ts +374 -150
  197. package/dist/types.js +4 -4
  198. package/dist/types.js.map +1 -1
  199. package/package.json +17 -8
  200. package/src/bootstrap-peer-store-file.ts +44 -44
  201. package/src/bootstrap-peer-store.ts +214 -214
  202. package/src/cadre-invite-protocol.ts +740 -1052
  203. package/src/cadre-join-request.ts +24 -3
  204. package/src/cadre-node.ts +2402 -1079
  205. package/src/control-authorization.ts +47 -14
  206. package/src/control-cohort.ts +204 -128
  207. package/src/control-database.ts +1749 -820
  208. package/src/control-formation-recorder.ts +359 -353
  209. package/src/control-protocol-guard.ts +23 -6
  210. package/src/control-schema-version.ts +301 -0
  211. package/src/control-schema.ts +273 -78
  212. package/src/control-write-retry.ts +641 -567
  213. package/src/delegate-admission.ts +351 -314
  214. package/src/ed25519-key.ts +174 -171
  215. package/src/edge-node/config.ts +277 -0
  216. package/src/edge-node/dissolve.ts +251 -0
  217. package/src/edge-node/edge-join.ts +319 -0
  218. package/src/edge-node/edge-node.ts +1045 -0
  219. package/src/edge-node/index.ts +59 -0
  220. package/src/edge-node/join-credential.ts +16 -0
  221. package/src/edge-node/lazy-raw-storage.ts +178 -0
  222. package/src/edge-node/node-local.ts +84 -0
  223. package/src/edge-node/options.ts +179 -0
  224. package/src/edge-node/owner-grant.ts +63 -0
  225. package/src/edge-node/platform.ts +151 -0
  226. package/src/edge-node/strand-move.ts +270 -0
  227. package/src/edge-node/strands.ts +72 -0
  228. package/src/enrolled-machine-store.ts +3 -3
  229. package/src/formation-token.ts +63 -0
  230. package/src/index.ts +899 -789
  231. package/src/joined-strand-store.ts +73 -28
  232. package/src/key-store.ts +10 -4
  233. package/src/link-budget.ts +9 -7
  234. package/src/machine-removal.ts +26 -111
  235. package/src/member-redemption.ts +649 -0
  236. package/src/membership-view.ts +144 -0
  237. package/src/owner-anchor-sync.ts +19 -18
  238. package/src/owner-grant-protocol.ts +547 -0
  239. package/src/owner-key-export.ts +348 -0
  240. package/src/owner-key-flows.ts +109 -0
  241. package/src/owner-key-store.ts +85 -0
  242. package/src/owner-keys.ts +178 -0
  243. package/src/owner-signer.ts +94 -0
  244. package/src/peer-addr-book.ts +4 -3
  245. package/src/peer-authorization.ts +558 -488
  246. package/src/peer-join-backfill.ts +971 -761
  247. package/src/pending-join-runner.ts +76 -53
  248. package/src/provider/rent-node.ts +1 -1
  249. package/src/seed-bootstrap.ts +343 -349
  250. package/src/seed-trust-policy.ts +4 -4
  251. package/src/strand-addr-protocol.ts +547 -538
  252. package/src/strand-cohort.ts +4 -0
  253. package/src/strand-database.ts +352 -319
  254. package/src/strand-first-sync-gate.ts +334 -133
  255. package/src/strand-formation-deadlines.ts +227 -227
  256. package/src/strand-formation-manager.ts +731 -725
  257. package/src/strand-formation-protocol.ts +122 -24
  258. package/src/strand-instance-manager.ts +2092 -2026
  259. package/src/strand-membership-reconciler.ts +33 -25
  260. package/src/strand-membership-writer.ts +2093 -1974
  261. package/src/strand-move.ts +50 -38
  262. package/src/strand-peer-redial.ts +2 -3
  263. package/src/strand-revocation-enforcer.ts +740 -654
  264. package/src/strand-solicitation.ts +61 -34
  265. package/src/strand-watcher.ts +175 -56
  266. package/src/table-change-watch.ts +285 -0
  267. package/src/timeout-scheduler.ts +29 -28
  268. package/src/types.ts +391 -152
@@ -1,15 +1,39 @@
1
+ import { Parser } from '@quereus/quereus';
1
2
  /**
2
- * Embedded control schema for cross-platform compatibility.
3
+ * The version of the declared control schema, carried in the schema text as the
4
+ * {@link CONTROL_SCHEMA_VERSION_TAG} tag on the `OwnerKey` table. A warm start compares
5
+ * it with the version the store records before applying anything, and the apply then
6
+ * migrates the store to this version and records it (`control-schema-version.ts`).
3
7
  *
4
- * This is the authoritative runtime copy of the CadreControl authorization
5
- * schema. It is duplicated from `schemas/control.qsql` so that React Native and
6
- * other filesystem-less environments get the schema without a runtime file read.
8
+ * When it changes, what a new version may change, and the fixture each version keeps:
9
+ * docs/architecture.md → "Control schema versions and migrations", enforced by
10
+ * `control-schema-versions.spec.ts`.
11
+ */
12
+ export const CONTROL_SCHEMA_VERSION = 3;
13
+ /**
14
+ * The first control schema version whose fixture is kept, and the oldest a store can be
15
+ * migrated from. A store that records a version below it (written by 1.16.0 and earlier,
16
+ * which recorded none) is refused with reason `before-migrations`; docs/architecture.md
17
+ * says why it is not migrated.
18
+ */
19
+ export const CONTROL_SCHEMA_BASELINE_VERSION = 3;
20
+ /** The `OwnerKey` table tag that records {@link CONTROL_SCHEMA_VERSION} in the schema and in every store. */
21
+ export const CONTROL_SCHEMA_VERSION_TAG = 'sereus.controlSchemaVersion';
22
+ /**
23
+ * The `declare schema CadreControl { … }` statement, with its header comment.
24
+ *
25
+ * Together with {@link APPLY_CONTROL_SCHEMA} this is the authoritative runtime copy of
26
+ * the CadreControl authorization schema ({@link CONTROL_SCHEMA}). It is duplicated from
27
+ * `schemas/control.qsql` so that React Native and other filesystem-less environments
28
+ * get the schema without a runtime file read. Held apart from the apply because
29
+ * `ControlDatabase.loadSchema` declares, checks the pending changes, and only then
30
+ * applies.
7
31
  *
8
32
  * The two copies MUST stay identical — `control-schema-drift.spec.ts` fails the
9
33
  * build if they drift. Any edit here must be mirrored in `schemas/control.qsql`
10
34
  * and vice versa.
11
35
  */
12
- export const CONTROL_SCHEMA = `-- A Sereus party's cadre (its nodes) and their participation in strands (networks).
36
+ export const CONTROL_SCHEMA_DECLARATION = `-- A Sereus party's cadre (its nodes) and their participation in strands (networks).
13
37
  --
14
38
  -- Every write is authorized by a check: an owner signs an action-tagged digest of the row
15
39
  -- (cadre-core control-authorization.ts), or a peer signs its own record. Each digest leads
@@ -21,13 +45,14 @@ export const CONTROL_SCHEMA = `-- A Sereus party's cadre (its nodes) and their p
21
45
  -- two digests scoped by the invitation token instead (FormationUsage.Authorized).
22
46
  --
23
47
  -- Rows of the guarded tables (OwnerKey, ValidationKey, Strand, StrandPartyKey, JoinedStrand,
24
- -- JoinRequest, CadrePeer, DeviceToken, CadreInvite) carry a one-off StampId bound into their
25
- -- add and remove digests. \`unique\` holds over live rows only, so a delete retires the stamp
26
- -- into Revocation in the same transaction (RevocationRecorded) and an insert refuses a retired
27
- -- stamp (NotRevoked): the never-expiring add approval cannot re-seat a removed row. A
28
- -- legitimate re-add mints a fresh stamp and a fresh signature. CadreInvite is the one guarded
29
- -- table that is never deleted: its tombstone withdraws the invitation while the row stays,
30
- -- because the rows it admitted prove their membership through it.
48
+ -- JoinRequest, CadrePeer, DeviceToken, CadreInvite, OwnerGrant) carry a one-off StampId bound
49
+ -- into their add and remove digests. \`unique\` holds over live rows only, so a delete retires
50
+ -- the stamp into Revocation in the same transaction (RevocationRecorded) and an insert refuses
51
+ -- a retired stamp (NotRevoked): the never-expiring add approval cannot re-seat a removed row.
52
+ -- A legitimate re-add mints a fresh stamp and a fresh signature. CadreInvite and OwnerGrant
53
+ -- are the two guarded tables that are never deleted: a tombstone there withdraws the
54
+ -- invitation or grant while the row stays, because the rows it admitted prove themselves
55
+ -- through it.
31
56
  --
32
57
  -- A check whose expression evaluates to null passes (SQL three-valued logic), so every
33
58
  -- comparison over a nullable column that is meant to pin a value is written null-safe
@@ -47,12 +72,16 @@ export const CONTROL_SCHEMA = `-- A Sereus party's cadre (its nodes) and their p
47
72
  declare schema CadreControl {
48
73
  -- A key that can authorize control changes. A row is seated one of three ways, told apart
49
74
  -- by the vouch columns: the founding row (all three null), an owner-signed add (VouchOwner
50
- -- and VouchSig pinned to the insert context, VouchUsage null), or an owner-granting
51
- -- invitation redeemed by consent (VouchOwner = the invitation's IssuerKey, VouchSig null,
52
- -- VouchUsage = the CadreInviteUsage row that admitted it). A reader judges the stored proof
53
- -- against its node-local anchor, never against this replicated table.
75
+ -- and VouchSig pinned to the insert context, VouchUsage null), or an owner grant redeemed
76
+ -- by consent (VouchOwner = the grant's IssuerKey, VouchSig null, VouchUsage = the
77
+ -- OwnerGrantUsage row that seated it). A machine invitation (CadreInvite) admits a machine
78
+ -- and never seats a row here: a machine's identity key is never an owner key. A reader
79
+ -- judges the stored proof against its node-local anchor, never against this replicated
80
+ -- table.
54
81
  table OwnerKey (
55
82
  Key text primary key,
83
+ Label text null, -- the user's name for the key ("phone", "paper backup"); bound into the
84
+ -- signed add, so a relayed approval cannot seat the key under another name
56
85
  StampId text unique,
57
86
  VouchOwner text null,
58
87
  VouchSig text null,
@@ -60,6 +89,10 @@ declare schema CadreControl {
60
89
  constraint NotRevoked check on insert (
61
90
  not exists (select 1 from Revocation R where R.TableName = 'OwnerKey' and R.StampId = new.StampId)
62
91
  ),
92
+ -- Never empty: the add digest binds coalesce(Label, ''), so an empty label would sign as none.
93
+ constraint LabelLength check (
94
+ Label is null or length(Label) between 1 and 64
95
+ ),
63
96
  -- The tombstone must name the removed row, not just its stamp (Revocation.RowKey).
64
97
  constraint RevocationRecorded check on delete (
65
98
  exists (select 1 from Revocation R where R.TableName = 'OwnerKey' and R.StampId = old.StampId and R.RowKey = old.Key)
@@ -81,29 +114,34 @@ declare schema CadreControl {
81
114
  -- deliberately raw, the one owner read here that ignores Revocation: a machine holding only
82
115
  -- a retired owner row must still be refused a second genesis, or a removed device could
83
116
  -- re-found the party on its own copy. An owner cannot sign its own removal.
84
- -- The consent branch seats an owner with no signature at all: a CadreInviteUsage row
85
- -- written in the same transaction names this exact row (Key and stamp), and the
86
- -- invitation it redeemed grants ownership and was issued by the stored VouchOwner. The
87
- -- invitation is read from committed.CadreInvite, so an invitation cannot be seated and
88
- -- redeemed in one transaction; the usage row carries the liveness conditions.
117
+ -- The consent branch seats an owner with no signature at all: an OwnerGrantUsage row
118
+ -- written in the same transaction names this exact row (Key and stamp), and the grant
119
+ -- it redeemed was issued by the stored VouchOwner and carries the row's label (the
120
+ -- issuer's choice, bound into the grant's signature). The grant is read from its
121
+ -- committed table, so it cannot be seated and redeemed in one transaction; the usage
122
+ -- row carries the liveness conditions.
89
123
  constraint Authorized check on insert, delete (
90
124
  (old.Key is null and (select count(1) from committed.OwnerKey) = 0
91
125
  and new.VouchOwner is null and new.VouchSig is null and new.VouchUsage is null)
92
- or (old.Key is null and exists (select 1 from committed.OwnerKey A where A.Key = context.OwnerKey and not exists (select 1 from Revocation R where R.TableName = 'OwnerKey' and R.StampId = A.StampId) and verify(digest('CadreControl.OwnerKey', 'add', party_id(), new.Key, new.StampId), context.Signature, A.Key, 'ed25519'))
126
+ or (old.Key is null and exists (select 1 from committed.OwnerKey A where A.Key = context.OwnerKey and not exists (select 1 from Revocation R where R.TableName = 'OwnerKey' and R.StampId = A.StampId) and verify(digest('CadreControl.OwnerKey', 'add', party_id(), new.Key, coalesce(new.Label, ''), new.StampId), context.Signature, A.Key, 'ed25519'))
93
127
  and coalesce(new.VouchOwner, '') = context.OwnerKey
94
128
  and coalesce(new.VouchSig, '') = context.Signature
95
129
  and new.VouchUsage is null)
96
130
  or (old.Key is null and new.VouchSig is null and new.VouchUsage is not null
97
131
  and exists (
98
- select 1 from CadreInviteUsage U
132
+ select 1 from OwnerGrantUsage U
99
133
  where U.UsageStampId = new.VouchUsage
100
- and U.PeerKey = new.Key
101
- and U.OwnerStampId = new.StampId
102
- and exists (select 1 from committed.CadreInvite I where I.Key = U.InviteKey and I.GrantsOwner = 1 and I.IssuerKey = new.VouchOwner)
134
+ and U.SeatedKey = new.Key
135
+ and U.SeatedStampId = new.StampId
136
+ and exists (select 1 from committed.OwnerGrant G
137
+ where G.Key = U.GrantKey and G.IssuerKey = new.VouchOwner
138
+ and coalesce(G.Label, '') = coalesce(new.Label, ''))
103
139
  ))
104
140
  or (new.Key is null and exists (select 1 from committed.OwnerKey A where A.Key = context.OwnerKey and A.Key <> old.Key and not exists (select 1 from Revocation R where R.TableName = 'OwnerKey' and R.StampId = A.StampId) and verify(digest('CadreControl.OwnerKey', 'remove', party_id(), old.Key, old.StampId), context.Signature, A.Key, 'ed25519')))
105
141
  )
106
- ) with context (OwnerKey text null, Signature text null);
142
+ -- The tag is the version of this whole schema (cadre-core CONTROL_SCHEMA_VERSION): every edit
143
+ -- to the schema bumps it, and a warm start reads the store's copy before applying anything.
144
+ ) with tags ("${CONTROL_SCHEMA_VERSION_TAG}" = ${CONTROL_SCHEMA_VERSION}) with context (OwnerKey text null, Signature text null);
107
145
 
108
146
  -- A key that can validate a strand formation disclosure (FormationUsage.Authorized).
109
147
  table ValidationKey (
@@ -136,9 +174,12 @@ declare schema CadreControl {
136
174
  Id text primary key, -- UUID
137
175
  MemberPrivateKey text null unique, -- the strand's shared read secret; closed strands only
138
176
  Type text, -- 'o' open | 'c' closed: open gates writes in the sApp, only closed gates reads
139
- FounderOwnerKey text null, -- owner key of the machine that published the row, the founding machine
140
- -- (cadre-node.ts launchStrand); null on a consent-seated strand, whose
141
- -- insert carries no signature. Provenance, not content (strandRowMismatches).
177
+ FounderOwnerKey text null, -- the owner key that signed the insert; null on a consent-seated strand,
178
+ -- whose insert carries no signature. Provenance, not content (strandRowMismatches).
179
+ FounderPeerId text null, -- peer id of the machine that runs the founder bootstrap (cadre-node.ts
180
+ -- launchStrand), bound into the signed add; null on a consent-seated strand.
181
+ -- Not pinned to a CadrePeer row: a solo founder publishes before its own
182
+ -- row exists, and only an owner can name it. Provenance, as FounderOwnerKey.
142
183
  StampId text unique,
143
184
  constraint NotRevoked check on insert (
144
185
  not exists (select 1 from Revocation R where R.TableName = 'Strand' and R.StampId = new.StampId)
@@ -149,9 +190,10 @@ declare schema CadreControl {
149
190
  -- The consent branch below carries no signature, so an update rule would let anyone rewrite
150
191
  -- a consent-seated strand.
151
192
  constraint NoUpdate check on update (false),
152
- -- Owner-signed, with FounderOwnerKey pinned to the verified signer; or consent-seated by a
153
- -- FormationUsage row naming this exact row (id and stamp), written in the same transaction
154
- -- (ControlDatabase.redeemInvitation). Binding the stamp is what makes a removal stick:
193
+ -- Owner-signed, with FounderOwnerKey pinned to the verified signer and FounderPeerId bound
194
+ -- into the signature; or consent-seated by a FormationUsage row naming this exact row (id
195
+ -- and stamp), written in the same transaction (ControlDatabase.redeemInvitation). Binding
196
+ -- the stamp is what makes a removal stick:
155
197
  -- the usage row outlives the strand, and keyed on the id alone it would re-authorize an
156
198
  -- unsigned re-seat under a fresh stamp. The consent branch is narrow, each clause closing
157
199
  -- one door:
@@ -164,12 +206,14 @@ declare schema CadreControl {
164
206
  -- A standalone tombstone (no accompanying delete, Revocation.Authorized) lets an owner
165
207
  -- foreclose consent-seating of an id that never existed: owner-only, and ids are random.
166
208
  constraint AuthorizedInsert check on insert (
167
- (exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and not exists (select 1 from Revocation R where R.TableName = 'OwnerKey' and R.StampId = A.StampId) and verify(digest('CadreControl.Strand', 'add', party_id(), new.Id, new.Type, coalesce(new.MemberPrivateKey, ''), new.StampId), context.Signature, A.Key, 'ed25519'))
168
- and new.FounderOwnerKey = context.OwnerKey)
209
+ (exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and not exists (select 1 from Revocation R where R.TableName = 'OwnerKey' and R.StampId = A.StampId) and verify(digest('CadreControl.Strand', 'add', party_id(), new.Id, new.Type, coalesce(new.MemberPrivateKey, ''), new.FounderPeerId, new.StampId), context.Signature, A.Key, 'ed25519'))
210
+ and new.FounderOwnerKey = context.OwnerKey
211
+ and new.FounderPeerId is not null)
169
212
  or (
170
213
  new.Type = 'o'
171
214
  and new.MemberPrivateKey is null
172
215
  and new.FounderOwnerKey is null
216
+ and new.FounderPeerId is null
173
217
  and exists (
174
218
  select 1 from FormationUsage FU
175
219
  where FU.StrandId = new.Id
@@ -619,15 +663,17 @@ declare schema CadreControl {
619
663
  ) with context (Now datetime, ValidationKey text null, ValidationSignature text null);
620
664
 
621
665
  -- Serves the per-token reads (the cap count above; cadre-core's countFormationUsage,
622
- -- isTokenUsed and hasOutstandingFormationInvite) on an append-only table. Its cross-machine
666
+ -- readToken and hasOutstandingFormationInvite) on an append-only table. Its cross-machine
623
667
  -- convergence is guarded by the integration scenario strand-formation-concurrent-redemption.
624
668
  index FormationUsageByToken on FormationUsage (Token);
625
669
 
626
- -- An owner-signed invitation to join this cadre, redeemable on the owner's behalf by any
627
- -- member machine (ControlDatabase.redeemCadreInvite). The invitation is a keypair, not a
670
+ -- An owner-signed invitation to join this cadre as a machine, redeemable on the owner's
671
+ -- behalf by any member machine (ControlDatabase.redeemCadreInvite). It admits a machine
672
+ -- and grants nothing else; ownership is an OwnerGrant. The invitation is a keypair, not a
628
673
  -- bearer token: the row holds the public half and the holder proves possession by signing
629
- -- with the private half (CadreInviteUsage.InvitePossessed), so the member that redeems it
630
- -- can verify but never redeem it for itself. Rows are never deleted: a device admitted
674
+ -- with the private half (CadreInviteUsage.InvitePossessed), so a member that only relays
675
+ -- it can neither admit a machine of its own choosing with it nor spend its uses. Rows are
676
+ -- never deleted: a device admitted
631
677
  -- through one carries no owner signature of its own, and its membership is the chain
632
678
  -- invitation -> usage -> peer row, verified at read time, so the row must outlive every
633
679
  -- device it admitted. Withdrawing an invitation is an owner-signed Revocation tombstone
@@ -640,7 +686,6 @@ declare schema CadreControl {
640
686
  table CadreInvite (
641
687
  Key text primary key, -- the invitation's ed25519 public key (base64url)
642
688
  PeerId text null, -- the device this invitation is for; null: whoever redeems first designates it
643
- GrantsOwner integer check (GrantsOwner = 0 or GrantsOwner = 1), -- 1: redemption also seats an OwnerKey row
644
689
  ExpiresAt datetime null,
645
690
  TotalUses int null check (TotalUses >= 0),
646
691
  IssuerKey text, -- the owner that signed the row (== insert context.OwnerKey). A reader checks it
@@ -661,7 +706,6 @@ declare schema CadreControl {
661
706
  'CadreControl.CadreInvite', 'add', party_id(),
662
707
  new.Key,
663
708
  coalesce(new.PeerId, ''),
664
- cast(new.GrantsOwner as text),
665
709
  coalesce(cast(new.ExpiresAt as text), ''),
666
710
  coalesce(cast(new.TotalUses as text), ''),
667
711
  new.StampId
@@ -676,8 +720,7 @@ declare schema CadreControl {
676
720
  ) with context (OwnerKey text, Signature text);
677
721
 
678
722
  -- Append-only record of cadre invitation redemptions, one row per redemption: the proof a
679
- -- CadrePeer row (and, for an owner-granting invitation, an OwnerKey row) was admitted by
680
- -- consent rather than by an owner signature.
723
+ -- CadrePeer row was admitted by consent rather than by an owner signature.
681
724
  table CadreInviteUsage (
682
725
  UsageStampId text primary key, -- nonce minted by the redeeming device and signed into both signatures
683
726
  -- below. The primary key: this table never deletes, so the key alone
@@ -691,22 +734,20 @@ declare schema CadreControl {
691
734
  PeerStampId text, -- the CadrePeer.StampId this usage seats: one row incarnation, not a peer id
692
735
  -- forever. Minted by the writer inside the redeeming transaction and not
693
736
  -- signed (FormationUsage.StrandStampId).
694
- OwnerStampId text null, -- the OwnerKey.StampId this usage seats when the invitation grants ownership
695
737
  InviteSig text, -- by the invitation's private key over the 'redeem' digest: proof of possession
696
738
  PeerSig text, -- by PeerKey over the 'consent' digest: the device agreed to join
697
739
  constraint InsertOnly check on update, delete (false),
698
740
  -- The invitation is read from the pre-transaction snapshot, so an invitation cannot be
699
- -- seated and redeemed in one transaction (the OwnerKey consent branch rests on that).
741
+ -- seated and redeemed in one transaction (the CadrePeer consent branch rests on that).
700
742
  -- NOTE: accepted tradeoff — the use cap counts committed usage, so two redeemers racing
701
- -- for a single-use invitation on different machines may both be admitted until merge,
702
- -- and after merge both rows stand (FormationUsage.Authorized), for owner grants too.
703
- -- Serializing would need cross-machine coordination the control plane does not have;
704
- -- the mitigations are the defaults the redemption protocol sets (untargeted owner
705
- -- invitations single-use with a short expiry), the targeted form for owner grants, and
706
- -- the owner seeing the extra owner and removing it. Revisit if the control plane gains
707
- -- a cross-machine sequencer. An issuer removed since issuing takes its outstanding
708
- -- invitations with it (the committed.OwnerKey clause, which a tombstone alone
709
- -- satisfies); devices it already admitted stay members.
743
+ -- for a single-use machine invitation on different machines may both be admitted until
744
+ -- merge, and after merge both rows stand (FormationUsage.Authorized). Serializing would
745
+ -- need cross-machine coordination the control plane does not have; the mitigations are
746
+ -- the single-use default, the targeted form, and the owner seeing the extra machine and
747
+ -- removing it. Revisit if the control plane gains a cross-machine sequencer. The owner
748
+ -- grant's half of this tradeoff is at OwnerGrantUsage.Authorized. An issuer removed
749
+ -- since issuing takes its outstanding invitations with it (the committed.OwnerKey
750
+ -- clause, which a tombstone alone satisfies); devices it already admitted stay members.
710
751
  constraint Authorized check on insert (
711
752
  exists (
712
753
  select 1 from committed.CadreInvite I
@@ -731,33 +772,142 @@ declare schema CadreControl {
731
772
  ),
732
773
  -- Deferred, so the same-transaction peer insert counts: a usage row can never be held in
733
774
  -- reserve to seat a row later (FormationUsage.StrandExists). PeerKey must be the key the
734
- -- peer row stores: a redemption that only completes an already-admitted device with its
735
- -- OwnerKey row (no peer insert to pin it) cannot name a key other than that device's.
775
+ -- peer row stores, so the usage names the admitted machine's own key and no other.
736
776
  constraint PeerExists check (exists (
737
777
  select 1 from CadrePeer P where P.PeerId = new.PeerId and P.StampId = new.PeerStampId and P.PublicKey = new.PeerKey
738
- )),
739
- constraint OwnerExists check (
740
- new.OwnerStampId is null
741
- or exists (select 1 from OwnerKey K where K.Key = new.PeerKey and K.StampId = new.OwnerStampId)
742
- )
778
+ ))
743
779
  ) with context (Now datetime);
744
780
 
745
781
  -- Serves the cap count above and the liveness reads (cadre-core countCadreInviteUsage,
746
782
  -- hasLiveCadreInvite) on an append-only table, as FormationUsageByToken does.
747
783
  index CadreInviteUsageByInvite on CadreInviteUsage (InviteKey);
748
784
 
785
+ -- An owner-signed grant of ownership to whoever holds the grant's private half: it seats an
786
+ -- OwnerKey row for a key the holder generates or already holds, and admits no machine (a
787
+ -- machine is admitted by a CadreInvite; peer identity and ownership are unrelated, so the
788
+ -- row names no peer id). The grant is a keypair as the invitation is: the row holds the
789
+ -- public half, the holder proves possession by signing with the private half
790
+ -- (OwnerGrantUsage.GrantPossessed), and the member machine that writes the redemption can
791
+ -- verify that signature but never produce it, so it cannot seat an owner for itself. Rows
792
+ -- are never deleted: a key seated through one carries no owner signature of its own, and
793
+ -- its proof is the chain grant -> usage -> OwnerKey row, verified at read time, so the row
794
+ -- must outlive every key it seated. Withdrawal is an owner-signed Revocation tombstone over
795
+ -- the row's stamp with the row kept (Revocation.RowIsGone); liveness is read as no
796
+ -- tombstone, unexpired, uses left, issuer still an owner (OwnerGrantUsage.Authorized).
797
+ -- NOTE: expired and withdrawn rows accumulate for the life of the party, as CadreInvite's.
798
+ -- The label is the issuer's: it is bound into the grant's signature and the seated OwnerKey
799
+ -- row must carry it (the grant branch of OwnerKey.Authorized), so a key seated on a fresh
800
+ -- device during recovery needs no naming prompt, and nothing the holder writes names it.
801
+ table OwnerGrant (
802
+ Key text primary key, -- the grant's ed25519 public key (base64url)
803
+ Label text null, -- the label the seated OwnerKey row carries, chosen by the issuer
804
+ ExpiresAt datetime null, -- null: never. The writer defaults to a short expiry; a lifetime with
805
+ -- no end is the issuer's explicit choice (a backup grant)
806
+ TotalUses int null check (TotalUses >= 0),
807
+ IssuerKey text, -- the owner that signed the row (== insert context.OwnerKey); a reader checks
808
+ -- it against its node-local anchor, never against the replicated OwnerKey table
809
+ IssuerSig text, -- that owner's 'add' signature (== insert context.Signature), stored so any
810
+ -- reader can re-verify the row from the holder's copy or the replicated one
811
+ StampId text unique,
812
+ -- A withdrawn grant cannot be re-seated from the signed copy its holder keeps, on any
813
+ -- node that holds the tombstone (schema header for the node that does not).
814
+ constraint NotRevoked check on insert (
815
+ not exists (select 1 from Revocation R where R.TableName = 'OwnerGrant' and R.StampId = new.StampId)
816
+ ),
817
+ -- Never empty, as OwnerKey.LabelLength: the add digest binds coalesce(Label, '').
818
+ constraint LabelLength check (
819
+ Label is null or length(Label) between 1 and 64
820
+ ),
821
+ -- The owner signs the whole row, nullable columns as '' (cadre-core ownerGrantRowFields),
822
+ -- and the stored issuer pair must be the pair verified here, as CadreInvite pins its issuer.
823
+ constraint AuthorizedInsert check on insert (
824
+ exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and not exists (select 1 from Revocation R where R.TableName = 'OwnerKey' and R.StampId = A.StampId) and verify(
825
+ digest(
826
+ 'CadreControl.OwnerGrant', 'add', party_id(),
827
+ new.Key,
828
+ coalesce(new.Label, ''),
829
+ coalesce(cast(new.ExpiresAt as text), ''),
830
+ coalesce(cast(new.TotalUses as text), ''),
831
+ new.StampId
832
+ ),
833
+ context.Signature, A.Key, 'ed25519'))
834
+ and coalesce(new.IssuerKey, '') = context.OwnerKey
835
+ and coalesce(new.IssuerSig, '') = context.Signature
836
+ ),
837
+ constraint NoUpdate check on update (false),
838
+ -- Never deleted (table comment). Withdrawal is the tombstone alone.
839
+ constraint NoDelete check on delete (false)
840
+ ) with context (OwnerKey text, Signature text);
841
+
842
+ -- Append-only record of owner-grant redemptions, one row per redemption: the proof an
843
+ -- OwnerKey row was seated by consent rather than by an owner signature.
844
+ table OwnerGrantUsage (
845
+ UsageStampId text primary key, -- nonce minted by the holder and signed into both signatures below. The
846
+ -- primary key: this table never deletes, so the key alone makes a nonce
847
+ -- permanent and there is no NotRevoked pair here (CadreInviteUsage).
848
+ GrantKey text, -- the OwnerGrant redeemed
849
+ SeatedKey text, -- the owner key seated (base64url ed25519): the holder's, fresh or already held
850
+ SeatedStampId text, -- the OwnerKey.StampId this usage seats: one row incarnation, not a key
851
+ -- forever. Minted by the writer inside the redeeming transaction and not
852
+ -- signed (CadreInviteUsage.PeerStampId).
853
+ GrantSig text, -- by the grant's private key over the 'redeem' digest: proof of possession
854
+ OwnerSig text, -- by SeatedKey over the 'consent' digest: the holder holds the key it seats
855
+ constraint InsertOnly check on update, delete (false),
856
+ -- The grant is read from the pre-transaction snapshot, so a grant cannot be seated and
857
+ -- redeemed in one transaction (the grant branch of OwnerKey.Authorized rests on that).
858
+ -- NOTE: accepted tradeoff — the use cap counts committed usage, so two holders of one
859
+ -- single-use grant redeeming on different machines may both be seated until merge, and
860
+ -- after merge both rows stand (CadreInviteUsage.Authorized). Serializing would need
861
+ -- cross-machine coordination the control plane does not have; the mitigations are the
862
+ -- defaults the writer sets (single use and a 15-minute expiry, a longer lifetime only
863
+ -- when the user chooses one) and the owner seeing the extra key in the owner-key list
864
+ -- and removing it. Revisit if the control plane gains a cross-machine sequencer. An
865
+ -- issuer removed since issuing takes its outstanding grants with it (the
866
+ -- committed.OwnerKey clause, which a tombstone alone satisfies); keys it already
867
+ -- seated stay owners.
868
+ constraint Authorized check on insert (
869
+ exists (
870
+ select 1 from committed.OwnerGrant G
871
+ where G.Key = new.GrantKey
872
+ and not exists (select 1 from Revocation R where R.TableName = 'OwnerGrant' and R.StampId = G.StampId)
873
+ and (G.ExpiresAt is null or G.ExpiresAt > context.Now)
874
+ and (G.TotalUses is null or G.TotalUses > (select count(1) from committed.OwnerGrantUsage U where U.GrantKey = new.GrantKey))
875
+ and exists (select 1 from committed.OwnerKey K where K.Key = G.IssuerKey and not exists (select 1 from Revocation R where R.TableName = 'OwnerKey' and R.StampId = K.StampId))
876
+ )
877
+ ),
878
+ -- Verified against the grant key itself: the member writing the row can check this
879
+ -- signature but, holding only the public half, cannot produce it.
880
+ constraint GrantPossessed check on insert (
881
+ verify(digest('CadreControl.OwnerGrantUsage', 'redeem', party_id(), new.GrantKey, new.UsageStampId, new.SeatedKey),
882
+ new.GrantSig, new.GrantKey, 'ed25519')
883
+ ),
884
+ -- The holder proves it holds the key being seated (CadreInviteUsage.PeerConsented).
885
+ constraint OwnerConsented check on insert (
886
+ verify(digest('CadreControl.OwnerGrantUsage', 'consent', party_id(), new.GrantKey, new.UsageStampId, new.SeatedKey),
887
+ new.OwnerSig, new.SeatedKey, 'ed25519')
888
+ ),
889
+ -- Deferred, so the same-transaction OwnerKey insert counts: a usage row can never be
890
+ -- held in reserve to seat a row later (CadreInviteUsage.PeerExists).
891
+ constraint SeatedExists check (
892
+ exists (select 1 from OwnerKey K where K.Key = new.SeatedKey and K.StampId = new.SeatedStampId)
893
+ )
894
+ ) with context (Now datetime);
895
+
896
+ -- Serves the cap count above and the liveness reads (cadre-core countOwnerGrantUsage,
897
+ -- listOwnerGrantStatuses) on an append-only table, as CadreInviteUsageByInvite does.
898
+ index OwnerGrantUsageByGrant on OwnerGrantUsage (GrantKey);
899
+
749
900
  -- Append-only retirement record for the stamps of removed rows of every guarded table, with
750
901
  -- the removed row's key. Every append is owner-signed, so the growth surface is the owner
751
- -- keys. One singleton marker row, ('Revocation', 'ledger', 'opened'), retires nothing: an
752
- -- owner's connected reconcile pass files it (ControlDatabase.openRevocationLedger) so this
753
- -- table is never a never-written block, which the storage layer would consult the cohort
754
- -- for on every guarded insert and membership lookup. Readers filter on their own guarded
755
- -- TableName, and queryRevocations skips it. A 'CadreInvite' row withdraws an invitation
756
- -- whose row stays (CadreInvite table comment).
902
+ -- keys. A party founded before genesis created every control collection may hold one marker
903
+ -- row, ('Revocation', 'ledger', 'opened'), which retires nothing: an owner filed it so this
904
+ -- table had a collection. Nothing files it now. Readers filter on their own guarded
905
+ -- TableName, and queryRevocations skips it. A 'CadreInvite' or 'OwnerGrant' row withdraws
906
+ -- an invitation or a grant whose row stays (the table comments).
757
907
  table Revocation (
758
908
  TableName text, -- 'OwnerKey' | 'ValidationKey' | 'Strand' | 'StrandPartyKey' | 'JoinedStrand' |
759
- -- 'JoinRequest' | 'CadrePeer' | 'DeviceToken' | 'CadreInvite', or 'Revocation'
760
- -- for the marker
909
+ -- 'JoinRequest' | 'CadrePeer' | 'DeviceToken' | 'CadreInvite' | 'OwnerGrant', or
910
+ -- 'Revocation' for an older party's marker
761
911
  RowKey text, -- primary key of the removed row
762
912
  StampId text, -- the retired stamp
763
913
  ReissuedAt integer default 0, -- bumped by an owner-signed re-issue so a tombstone committed while the
@@ -785,9 +935,11 @@ declare schema CadreControl {
785
935
  -- counts). Keyed on the stamp, not RowKey: a node that converges on a re-admission before
786
936
  -- the earlier removal's tombstone must still accept that tombstone. Retiring a stamp that
787
937
  -- never existed is harmless: stamps carry 128 random bits (generateStampId). For
788
- -- 'CadreInvite' there is no row-gone test: a tombstone there is a withdrawal, and the
789
- -- row stays (CadreInvite table comment). The last branch admits the marker row and
790
- -- nothing else under 'Revocation'.
938
+ -- 'CadreInvite' and 'OwnerGrant' there is no row-gone test: a tombstone there is a
939
+ -- withdrawal, and the row stays (the table comments). The last branch admits the marker
940
+ -- row an older party may hold, and nothing else under 'Revocation'; nothing files it now.
941
+ -- NOTE: drop this branch, REVOCATION_LEDGER_MARKER and the readers' skip together with the
942
+ -- next CONTROL_SCHEMA_VERSION change that already needs a migration.
791
943
  constraint RowIsGone check on insert (
792
944
  (new.TableName = 'OwnerKey' and not exists (select 1 from OwnerKey K where K.StampId = new.StampId))
793
945
  or (new.TableName = 'CadrePeer' and not exists (select 1 from CadrePeer P where P.StampId = new.StampId))
@@ -798,6 +950,7 @@ declare schema CadreControl {
798
950
  or (new.TableName = 'JoinRequest' and not exists (select 1 from JoinRequest J where J.StampId = new.StampId))
799
951
  or (new.TableName = 'DeviceToken' and not exists (select 1 from DeviceToken D where D.StampId = new.StampId))
800
952
  or (new.TableName = 'CadreInvite')
953
+ or (new.TableName = 'OwnerGrant')
801
954
  or (new.TableName = 'Revocation' and new.RowKey = 'ledger' and new.StampId = 'opened')
802
955
  ),
803
956
  -- Owner-signed over the whole row under its own domain tag, so the delete's 'remove'
@@ -805,9 +958,8 @@ declare schema CadreControl {
805
958
  -- writer could evict a peer party-wide and block its re-admission. RowKey is not checked
806
959
  -- against the retired row here (that would reject a tombstone re-issued later, or filed on
807
960
  -- a node that never held the row); each guarded table's RevocationRecorded binds it
808
- -- instead. Reads live OwnerKey like the sibling tables. The marker is signed under this
809
- -- rule too, over ('Revocation', 'ledger', 'opened'). The stored signer pair must be the
810
- -- pair verified here.
961
+ -- instead. Reads live OwnerKey like the sibling tables. An older party's marker was signed
962
+ -- under this rule too. The stored signer pair must be the pair verified here.
811
963
  constraint Authorized check on insert (
812
964
  exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and not exists (select 1 from Revocation R where R.TableName = 'OwnerKey' and R.StampId = A.StampId) and verify(digest('CadreControl.Revocation', 'remove', party_id(), new.TableName, new.RowKey, new.StampId), context.Signature, A.Key, 'ed25519'))
813
965
  and coalesce(new.SignerKey, '') = context.OwnerKey
@@ -822,8 +974,40 @@ declare schema CadreControl {
822
974
  context.Signature, A.Key, 'ed25519'))
823
975
  )
824
976
  ) with context (OwnerKey text, Signature text);
825
- }
977
+ }`;
978
+ /** The name the control schema is declared under; Quereus compares schema names without regard to case. */
979
+ export const CONTROL_SCHEMA_NAME = 'CadreControl';
980
+ /** The statement that applies {@link CONTROL_SCHEMA_DECLARATION}, run only after the store has been checked against it. */
981
+ export const APPLY_CONTROL_SCHEMA = `apply schema ${CONTROL_SCHEMA_NAME};`;
982
+ /** The whole control schema as `schemas/control.qsql` holds it: the declaration, then the apply. */
983
+ export const CONTROL_SCHEMA = `${CONTROL_SCHEMA_DECLARATION}
826
984
 
827
- apply schema CadreControl;
985
+ ${APPLY_CONTROL_SCHEMA}
828
986
  `;
987
+ /**
988
+ * The declaration statement of a control schema file (`ControlDatabaseConfig.schemaPath`),
989
+ * cut from the file's own text.
990
+ *
991
+ * Refuses a file that is anything other than one `declare schema CadreControl { … }`
992
+ * followed by one plain `apply schema CadreControl` — the shape of {@link CONTROL_SCHEMA}.
993
+ * `loadSchema` runs its own {@link APPLY_CONTROL_SCHEMA} once the store has been checked,
994
+ * so any other statement in the file, or an option on its apply, would be dropped or run
995
+ * unchecked.
996
+ */
997
+ export function controlSchemaFileDeclaration(source) {
998
+ const statements = new Parser().parseAll(source);
999
+ const [declaration, apply] = statements;
1000
+ if (statements.length !== 2
1001
+ || declaration.type !== 'declareSchema' || !isControlSchemaName(declaration.schemaName)
1002
+ || apply.type !== 'applySchema' || !isControlSchemaName(apply.schemaName)
1003
+ || apply.options !== undefined || apply.toVersion !== undefined || apply.withSeed === true
1004
+ || declaration.loc === undefined) {
1005
+ throw new Error(`A control schema file must hold exactly \`declare schema ${CONTROL_SCHEMA_NAME} { … }\` ` +
1006
+ `followed by \`${APPLY_CONTROL_SCHEMA}\`, the shape of schemas/control.qsql.`);
1007
+ }
1008
+ return source.slice(declaration.loc.start.offset, declaration.loc.end.offset);
1009
+ }
1010
+ function isControlSchemaName(name) {
1011
+ return name?.toLowerCase() === CONTROL_SCHEMA_NAME.toLowerCase();
1012
+ }
829
1013
  //# sourceMappingURL=control-schema.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"control-schema.js","sourceRoot":"","sources":["../src/control-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgzB7B,CAAC"}
1
+ {"version":3,"file":"control-schema.js","sourceRoot":"","sources":["../src/control-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE1C;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC;AAExC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,+BAA+B,GAAG,CAAC,CAAC;AAEjD,6GAA6G;AAC7G,MAAM,CAAC,MAAM,0BAA0B,GAAG,6BAA6B,CAAC;AAExE;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oBA4GtB,0BAA0B,OAAO,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAi0BzE,CAAC;AAEH,2GAA2G;AAC3G,MAAM,CAAC,MAAM,mBAAmB,GAAG,cAAc,CAAC;AAElD,2HAA2H;AAC3H,MAAM,CAAC,MAAM,oBAAoB,GAAG,gBAAgB,mBAAmB,GAAG,CAAC;AAE3E,oGAAoG;AACpG,MAAM,CAAC,MAAM,cAAc,GAAG,GAAG,0BAA0B;;EAEzD,oBAAoB;CACrB,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,UAAU,4BAA4B,CAAC,MAAc;IAC1D,MAAM,UAAU,GAAG,IAAI,MAAM,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACjD,MAAM,CAAC,WAAW,EAAE,KAAK,CAAC,GAAG,UAAU,CAAC;IACxC,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;WACvB,WAAW,CAAC,IAAI,KAAK,eAAe,IAAI,CAAC,mBAAmB,CAAC,WAAW,CAAC,UAAU,CAAC;WACpF,KAAK,CAAC,IAAI,KAAK,aAAa,IAAI,CAAC,mBAAmB,CAAC,KAAK,CAAC,UAAU,CAAC;WACtE,KAAK,CAAC,OAAO,KAAK,SAAS,IAAI,KAAK,CAAC,SAAS,KAAK,SAAS,IAAI,KAAK,CAAC,QAAQ,KAAK,IAAI;WACvF,WAAW,CAAC,GAAG,KAAK,SAAS,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CACd,4DAA4D,mBAAmB,WAAW;YAC1F,iBAAiB,oBAAoB,wCAAwC,CAC7E,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;AAC/E,CAAC;AAED,SAAS,mBAAmB,CAAC,IAAwB;IACpD,OAAO,IAAI,EAAE,WAAW,EAAE,KAAK,mBAAmB,CAAC,WAAW,EAAE,CAAC;AAClE,CAAC"}
@@ -28,12 +28,13 @@ import type { ControlRetryOptions } from './control-retry.js';
28
28
  * before any matcher runs. The one half-landed write that IS re-presented is a torn write the
29
29
  * library marks final — not saved and unable to land ({@link isFinalTornWrite}).
30
30
  *
31
- * TWO policies ship from here, and the split is deliberate: the default
31
+ * THREE policies ship from here, and the split is deliberate: the default
32
32
  * ({@link isRetriableControlWriteFailure}, {@link CONTROL_WRITE_ATTEMPTS}) sits under every
33
- * control write, and {@link SCHEMA_INIT_RETRY_POLICY} is opted into by
34
- * `ControlDatabase.loadSchema` alone. The widened one absorbs one extra failure class whose
35
- * re-run safety holds for schema DDL and NOT for writes in general; anything added to the default
36
- * list must be safe for all ~19 callers.
33
+ * control write; {@link SCHEMA_INIT_RETRY_POLICY} is opted into by
34
+ * `ControlDatabase.applyControlSchema` alone; {@link LOST_UPDATE_RETRY_POLICY} is opted into by
35
+ * the `CadrePeer` writers whose bodies re-read the row and decide again. Each widened one absorbs
36
+ * one extra failure class whose re-run safety holds for its opted-in bodies and NOT for writes in
37
+ * general; anything added to the default list must be safe for all ~19 callers.
37
38
  *
38
39
  * Lives outside `control-database.ts` so a spec can drive the classifier against real engine
39
40
  * errors without importing the whole database class. The loop itself lives in
@@ -212,12 +213,20 @@ export declare function isUncommittedTransactorAggregate(message: string): boole
212
213
  export declare function isRetriableControlWriteFailure(error: unknown): boolean;
213
214
  /**
214
215
  * {@link isRetriableControlWriteFailure} widened by exactly one class — the self-coordination
215
- * grace refusal ({@link RETRIABLE_SCHEMA_INIT_MATCHERS}) — for `ControlDatabase.loadSchema`.
216
+ * grace refusal ({@link RETRIABLE_SCHEMA_INIT_MATCHERS}) — for `ControlDatabase.applyControlSchema`.
216
217
  *
217
218
  * Opt-in per call site, via {@link SCHEMA_INIT_RETRY_POLICY}. Every other control write keeps the
218
219
  * default classifier untouched.
219
220
  */
220
221
  export declare function isRetriableSchemaInitFailure(error: unknown): boolean;
222
+ /**
223
+ * {@link isRetriableControlWriteFailure} widened by exactly one class — a lost update
224
+ * ({@link isLostUpdate}), claimed by type — for the `CadrePeer` writers that re-read the row inside
225
+ * their locked body. Opt-in per call site, via {@link LOST_UPDATE_RETRY_POLICY}; both vetoes still
226
+ * run first, so a lost update carried inside a partial commit, a spent sync budget or a non-final
227
+ * torn write stays declined.
228
+ */
229
+ export declare function isRetriableLostUpdateFailure(error: unknown): boolean;
221
230
  /**
222
231
  * Policy and pacing overrides for {@link retryControlWrite}. Production callers pass nothing
223
232
  * (or a named policy such as {@link SCHEMA_INIT_RETRY_POLICY}); specs inject a recorded
@@ -230,7 +239,7 @@ export declare function isRetriableSchemaInitFailure(error: unknown): boolean;
230
239
  */
231
240
  export type ControlWriteRetryOptions = ControlRetryOptions;
232
241
  /**
233
- * The one non-default retry policy: `ControlDatabase.loadSchema`'s distributed DDL, which
242
+ * The schema-init policy: `ControlDatabase.applyControlSchema`'s distributed DDL, which
234
243
  * additionally absorbs optimystic's self-coordination grace refusal and gets more attempts over a
235
244
  * longer backoff to do it (see {@link SCHEMA_INIT_ATTEMPTS} and
236
245
  * {@link SCHEMA_INIT_RETRY_DELAYS_MS}). Still bounded by the shared
@@ -238,12 +247,38 @@ export type ControlWriteRetryOptions = ControlRetryOptions;
238
247
  *
239
248
  * NOTE: exactly one call site opts in today, and the re-run safety this policy assumes is that
240
249
  * site's, not a general property — `apply schema` is a diff, and a failed apply is unwound whole
241
- * and verified against the pre-apply catalog (full argument at the `loadSchema` call site in
242
- * `control-database.ts`). A second opt-in must re-derive that argument for its own write body
250
+ * and verified against the pre-apply catalog (full argument at the `applyControlSchema` call site
251
+ * in `control-database.ts`). A second opt-in must re-derive that argument for its own write body
243
252
  * first; if this policy ever grows a third consumer, rename it for what the callers share rather
244
- * than widening it by default.
253
+ * than widening it by default. {@link LOST_UPDATE_RETRY_POLICY} is a separate policy for a
254
+ * separate class, not a second consumer of this one.
245
255
  */
246
256
  export declare const SCHEMA_INIT_RETRY_POLICY: Readonly<ControlWriteRetryOptions>;
257
+ /**
258
+ * The lost-update policy: the default classifier plus a lost update claimed by type
259
+ * ({@link isRetriableLostUpdateFailure}), over the default attempts, backoff and budget. A re-run
260
+ * here is a RE-DECISION, not a re-presentation: the body reads the row again under the lock and
261
+ * signs over what it finds, so the write that lost to a rival is replaced by one made over the
262
+ * rival's result. The budget bounds it as it bounds every write — a losing attempt whose read
263
+ * phase waited out one per-peer read deadline (~7.5 s measured in the degraded-cohort scenario's
264
+ * trio boot) gets exactly one retry, which is the ceiling to expect; one that waited out two
265
+ * (14.7 s, measured once in 18 of that scenario's boots, on the member that cannot reach one
266
+ * sibling) gets none and is abandoned on the budget, and the lost publish is then the reconcile
267
+ * pass's or the heartbeat's to redo, as every abandoned self-publish is.
268
+ *
269
+ * NOTE: the re-run safety this policy assumes is each opted-in body's, not a general property. A
270
+ * body may opt in only when its read of the row, its decision and its signature ALL run inside
271
+ * the locked body, so that the re-run presents a write made over the row as it now stands rather
272
+ * than the first attempt's signed record (which the rival may have stamped past, or which may
273
+ * overwrite what the rival wrote). Three `CadrePeer` writers opt in, through
274
+ * `ControlDatabase.mutateCadrePeer`'s policy parameter or `lockedWithRetry` directly:
275
+ * `updateSelfPeerRecord` (re-reads and re-signs the node's own record), `reauthorizeCadrePeer`
276
+ * (re-reads the stamp and decides again whether the row is still the owner's to re-touch) and
277
+ * the two removals `deleteCadrePeer` / `removeMachine` (stamp read and signature already inside
278
+ * the body, and a self-publish never changes the stamp the `'remove'` digest binds). A new opt-in
279
+ * must re-derive that argument for its own body first.
280
+ */
281
+ export declare const LOST_UPDATE_RETRY_POLICY: Readonly<ControlWriteRetryOptions>;
247
282
  /**
248
283
  * Run `attempt` up to {@link CONTROL_WRITE_ATTEMPTS} times (or `options.attempts`), retrying only
249
284
  * failures the policy's classifier — {@link isRetriableControlWriteFailure} by default —