@serfab/cadre-core 1.13.0 → 1.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 (230) hide show
  1. package/README.md +293 -274
  2. package/dist/cadre-invite-protocol.d.ts +383 -0
  3. package/dist/cadre-invite-protocol.js +774 -0
  4. package/dist/cadre-invite-protocol.js.map +1 -0
  5. package/dist/cadre-join-request.d.ts +42 -0
  6. package/dist/cadre-join-request.js +144 -0
  7. package/dist/cadre-join-request.js.map +1 -0
  8. package/dist/cadre-node.d.ts +849 -210
  9. package/dist/cadre-node.js +1574 -374
  10. package/dist/cadre-node.js.map +1 -1
  11. package/dist/claim-proof.d.ts +45 -0
  12. package/dist/claim-proof.js +132 -0
  13. package/dist/claim-proof.js.map +1 -0
  14. package/dist/control-authorization.d.ts +80 -19
  15. package/dist/control-authorization.js +55 -13
  16. package/dist/control-authorization.js.map +1 -1
  17. package/dist/control-cohort.js +1 -1
  18. package/dist/control-cohort.js.map +1 -1
  19. package/dist/control-database.d.ts +605 -108
  20. package/dist/control-database.js +1299 -280
  21. package/dist/control-database.js.map +1 -1
  22. package/dist/control-protocol-guard.d.ts +142 -0
  23. package/dist/control-protocol-guard.js +293 -0
  24. package/dist/control-protocol-guard.js.map +1 -0
  25. package/dist/control-schema.d.ts +1 -1
  26. package/dist/control-schema.js +817 -1067
  27. package/dist/control-schema.js.map +1 -1
  28. package/dist/control-stream.d.ts +4 -0
  29. package/dist/control-stream.js +22 -7
  30. package/dist/control-stream.js.map +1 -1
  31. package/dist/delegate-admission.d.ts +1 -1
  32. package/dist/delegate-admission.js +1 -1
  33. package/dist/device-token.d.ts +11 -9
  34. package/dist/device-token.js +14 -12
  35. package/dist/device-token.js.map +1 -1
  36. package/dist/ed25519-key.d.ts +22 -9
  37. package/dist/ed25519-key.js +47 -10
  38. package/dist/ed25519-key.js.map +1 -1
  39. package/dist/fetch-impl.d.ts +12 -0
  40. package/dist/fetch-impl.js +23 -0
  41. package/dist/fetch-impl.js.map +1 -0
  42. package/dist/file-durable-slot.js +2 -1
  43. package/dist/file-durable-slot.js.map +1 -1
  44. package/dist/file-safe-name.d.ts +25 -0
  45. package/dist/file-safe-name.js +29 -0
  46. package/dist/file-safe-name.js.map +1 -0
  47. package/dist/formation-approval.js +5 -11
  48. package/dist/formation-approval.js.map +1 -1
  49. package/dist/fs-atomic.d.ts +0 -19
  50. package/dist/fs-atomic.js +3 -24
  51. package/dist/fs-atomic.js.map +1 -1
  52. package/dist/index.d.ts +22 -10
  53. package/dist/index.js +54 -11
  54. package/dist/index.js.map +1 -1
  55. package/dist/invitation-bootstrap.d.ts +50 -12
  56. package/dist/invitation-bootstrap.js +76 -13
  57. package/dist/invitation-bootstrap.js.map +1 -1
  58. package/dist/joined-strand-store.d.ts +7 -0
  59. package/dist/joined-strand-store.js +9 -0
  60. package/dist/joined-strand-store.js.map +1 -1
  61. package/dist/key-store-file.js +2 -1
  62. package/dist/key-store-file.js.map +1 -1
  63. package/dist/link-budget.d.ts +25 -33
  64. package/dist/link-budget.js +25 -35
  65. package/dist/link-budget.js.map +1 -1
  66. package/dist/machine-removal.d.ts +78 -0
  67. package/dist/machine-removal.js +166 -0
  68. package/dist/machine-removal.js.map +1 -0
  69. package/dist/membership-connection-gater.d.ts +191 -151
  70. package/dist/membership-connection-gater.js +236 -173
  71. package/dist/membership-connection-gater.js.map +1 -1
  72. package/dist/node-claim-payload.d.ts +74 -0
  73. package/dist/node-claim-payload.js +224 -0
  74. package/dist/node-claim-payload.js.map +1 -0
  75. package/dist/node-default-transports.d.ts +27 -0
  76. package/dist/node-default-transports.js +35 -0
  77. package/dist/node-default-transports.js.map +1 -0
  78. package/dist/node-transports.d.ts +30 -0
  79. package/dist/node-transports.js +40 -0
  80. package/dist/node-transports.js.map +1 -0
  81. package/dist/owner-anchor-sync.d.ts +22 -0
  82. package/dist/owner-anchor-sync.js +143 -0
  83. package/dist/owner-anchor-sync.js.map +1 -0
  84. package/dist/peer-authorization.d.ts +113 -18
  85. package/dist/peer-authorization.js +225 -29
  86. package/dist/peer-authorization.js.map +1 -1
  87. package/dist/peer-dial.d.ts +143 -27
  88. package/dist/peer-dial.js +276 -31
  89. package/dist/peer-dial.js.map +1 -1
  90. package/dist/peer-join-backfill.d.ts +26 -2
  91. package/dist/peer-join-backfill.js +60 -12
  92. package/dist/peer-join-backfill.js.map +1 -1
  93. package/dist/peer-record.d.ts +17 -52
  94. package/dist/peer-record.js +20 -58
  95. package/dist/peer-record.js.map +1 -1
  96. package/dist/pending-join-runner.d.ts +66 -58
  97. package/dist/pending-join-runner.js +136 -145
  98. package/dist/pending-join-runner.js.map +1 -1
  99. package/dist/primary-lan-address.d.ts +8 -0
  100. package/dist/primary-lan-address.js +46 -0
  101. package/dist/primary-lan-address.js.map +1 -0
  102. package/dist/provider/client.d.ts +131 -0
  103. package/dist/provider/client.js +226 -0
  104. package/dist/provider/client.js.map +1 -0
  105. package/dist/provider/descriptor.d.ts +41 -0
  106. package/dist/provider/descriptor.js +103 -0
  107. package/dist/provider/descriptor.js.map +1 -0
  108. package/dist/provider/index.d.ts +9 -0
  109. package/dist/provider/index.js +10 -0
  110. package/dist/provider/index.js.map +1 -0
  111. package/dist/provider/oauth.d.ts +125 -0
  112. package/dist/provider/oauth.js +199 -0
  113. package/dist/provider/oauth.js.map +1 -0
  114. package/dist/provider/rent-node.d.ts +101 -0
  115. package/dist/provider/rent-node.js +220 -0
  116. package/dist/provider/rent-node.js.map +1 -0
  117. package/dist/push-fanout.d.ts +8 -2
  118. package/dist/push-fanout.js +8 -2
  119. package/dist/push-fanout.js.map +1 -1
  120. package/dist/relay-addrs.d.ts +9 -4
  121. package/dist/relay-addrs.js +9 -4
  122. package/dist/relay-addrs.js.map +1 -1
  123. package/dist/relay-reservation.d.ts +51 -16
  124. package/dist/relay-reservation.js +107 -24
  125. package/dist/relay-reservation.js.map +1 -1
  126. package/dist/seed-bootstrap.d.ts +165 -123
  127. package/dist/seed-bootstrap.js +313 -275
  128. package/dist/seed-bootstrap.js.map +1 -1
  129. package/dist/seed-trust-policy.d.ts +98 -37
  130. package/dist/seed-trust-policy.js +188 -2
  131. package/dist/seed-trust-policy.js.map +1 -1
  132. package/dist/strand-addr-protocol.d.ts +24 -26
  133. package/dist/strand-addr-protocol.js +19 -26
  134. package/dist/strand-addr-protocol.js.map +1 -1
  135. package/dist/strand-commit-durability.d.ts +54 -0
  136. package/dist/strand-commit-durability.js +74 -0
  137. package/dist/strand-commit-durability.js.map +1 -0
  138. package/dist/strand-database.d.ts +23 -8
  139. package/dist/strand-database.js +41 -13
  140. package/dist/strand-database.js.map +1 -1
  141. package/dist/strand-first-sync-gate.d.ts +7 -1
  142. package/dist/strand-first-sync-gate.js +7 -1
  143. package/dist/strand-first-sync-gate.js.map +1 -1
  144. package/dist/strand-formation-protocol.js +5 -2
  145. package/dist/strand-formation-protocol.js.map +1 -1
  146. package/dist/strand-fret-addrs.d.ts +19 -1
  147. package/dist/strand-fret-addrs.js +27 -9
  148. package/dist/strand-fret-addrs.js.map +1 -1
  149. package/dist/strand-instance-manager.d.ts +81 -19
  150. package/dist/strand-instance-manager.js +216 -105
  151. package/dist/strand-instance-manager.js.map +1 -1
  152. package/dist/strand-move.d.ts +124 -0
  153. package/dist/strand-move.js +438 -0
  154. package/dist/strand-move.js.map +1 -0
  155. package/dist/strand-network-config.js +7 -6
  156. package/dist/strand-network-config.js.map +1 -1
  157. package/dist/strand-peer-redial.d.ts +113 -0
  158. package/dist/strand-peer-redial.js +236 -0
  159. package/dist/strand-peer-redial.js.map +1 -0
  160. package/dist/strand-revocation-enforcer.d.ts +11 -11
  161. package/dist/strand-revocation-enforcer.js +11 -11
  162. package/dist/strand-wake-protocol.d.ts +13 -16
  163. package/dist/strand-wake-protocol.js +23 -27
  164. package/dist/strand-wake-protocol.js.map +1 -1
  165. package/dist/trusted-owner-store.d.ts +35 -5
  166. package/dist/trusted-owner-store.js +50 -8
  167. package/dist/trusted-owner-store.js.map +1 -1
  168. package/dist/types.d.ts +512 -89
  169. package/dist/types.js +10 -0
  170. package/dist/types.js.map +1 -1
  171. package/package.json +19 -10
  172. package/src/cadre-invite-protocol.ts +1052 -0
  173. package/src/cadre-join-request.ts +167 -0
  174. package/src/cadre-node.ts +1703 -386
  175. package/src/claim-proof.ts +147 -0
  176. package/src/control-authorization.ts +203 -117
  177. package/src/control-cohort.ts +1 -1
  178. package/src/control-database.ts +4663 -3453
  179. package/src/control-protocol-guard.ts +348 -0
  180. package/src/control-schema.ts +828 -1078
  181. package/src/control-stream.ts +26 -7
  182. package/src/delegate-admission.ts +1 -1
  183. package/src/device-token.ts +14 -10
  184. package/src/ed25519-key.ts +47 -10
  185. package/src/fetch-impl.ts +23 -0
  186. package/src/file-durable-slot.ts +64 -63
  187. package/src/file-safe-name.ts +32 -0
  188. package/src/formation-approval.ts +604 -610
  189. package/src/fs-atomic.ts +92 -117
  190. package/src/index.ts +789 -648
  191. package/src/invitation-bootstrap.ts +91 -13
  192. package/src/joined-strand-store.ts +508 -498
  193. package/src/key-store-file.ts +142 -141
  194. package/src/link-budget.ts +25 -36
  195. package/src/machine-removal.ts +216 -0
  196. package/src/membership-connection-gater.ts +259 -191
  197. package/src/node-claim-payload.ts +259 -0
  198. package/src/node-default-transports.ts +38 -0
  199. package/src/node-transports.ts +49 -0
  200. package/src/owner-anchor-sync.ts +171 -0
  201. package/src/peer-authorization.ts +258 -29
  202. package/src/peer-dial.ts +604 -315
  203. package/src/peer-join-backfill.ts +62 -11
  204. package/src/peer-record.ts +159 -197
  205. package/src/pending-join-runner.ts +729 -735
  206. package/src/primary-lan-address.ts +47 -0
  207. package/src/provider/client.ts +326 -0
  208. package/src/provider/descriptor.ts +129 -0
  209. package/src/provider/index.ts +59 -0
  210. package/src/provider/oauth.ts +285 -0
  211. package/src/provider/rent-node.ts +275 -0
  212. package/src/push-fanout.ts +8 -2
  213. package/src/relay-addrs.ts +9 -4
  214. package/src/relay-reservation.ts +1211 -1121
  215. package/src/seed-bootstrap.ts +1502 -1448
  216. package/src/seed-trust-policy.ts +353 -140
  217. package/src/strand-addr-protocol.ts +26 -35
  218. package/src/strand-commit-durability.ts +116 -0
  219. package/src/strand-database.ts +46 -14
  220. package/src/strand-first-sync-gate.ts +7 -1
  221. package/src/strand-formation-protocol.ts +1039 -1036
  222. package/src/strand-fret-addrs.ts +179 -145
  223. package/src/strand-instance-manager.ts +2026 -1883
  224. package/src/strand-move.ts +585 -0
  225. package/src/strand-network-config.ts +7 -6
  226. package/src/strand-peer-redial.ts +307 -0
  227. package/src/strand-revocation-enforcer.ts +11 -11
  228. package/src/strand-wake-protocol.ts +23 -30
  229. package/src/trusted-owner-store.ts +242 -171
  230. package/src/types.ts +2687 -2242
@@ -9,1071 +9,821 @@
9
9
  * build if they drift. Any edit here must be mirrored in `schemas/control.qsql`
10
10
  * and vice versa.
11
11
  */
12
- export const CONTROL_SCHEMA = `-- This manages a Sereus party's cadre, or set of nodes, and their participation in strands (networks)
13
- declare schema CadreControl {
14
- -- A key that can authorize various control changes
15
- table OwnerKey (
16
- Key text primary key,
17
- StampId text not null unique, -- per-row authorization nonce, bound into the signed digests below.
18
- -- \`unique\` holds over LIVE rows only; a removed row's stamp is retired
19
- -- permanently into Revocation (NotRevoked below), so the never-expiring
20
- -- enrollment signature cannot resurrect a removed owner.
21
- -- A removed row's StampId is retired into Revocation, and this refuses any insert
22
- -- naming a retired stamp: the approval that seated this row can never re-seat it after
23
- -- removal. \`unique\` alone did not do this — it only holds over LIVE rows, so a delete
24
- -- freed the stamp and the original never-expiring signature verified again. A
25
- -- legitimate re-add mints a FRESH StampId and a fresh signature, so it is unaffected.
26
- constraint NotRevoked check on insert (
27
- not exists (select 1 from Revocation R where R.TableName = 'OwnerKey' and R.StampId = new.StampId)
28
- ),
29
- -- Retirement is MANDATORY: a delete must carry the matching Revocation row in the same
30
- -- transaction, or the stamp would free up again. Deferred (subquery), so the sibling
31
- -- insert is visible at commit regardless of statement order. The tombstone must also
32
- -- name the removed ROW, not just its stamp — see Revocation.RowKey.
33
- constraint RevocationRecorded check on delete (
34
- exists (select 1 from Revocation R where R.TableName = 'OwnerKey' and R.StampId = old.StampId and R.RowKey = old.Key)
35
- ),
36
- -- A party must never lose its last owner key: every other CadreControl table's
37
- -- CHECK requires an OwnerKey row, so an owner-less control database can never
38
- -- authorize anything again — emptying this table is a permanent denial of the
39
- -- party's control plane. Deferred (subquery), so the count is the POST-delete count.
40
- -- NOTE: this is a per-transaction check against locally visible rows. Two
41
- -- partitioned nodes that concurrently remove different owners can each see a
42
- -- survivor and still converge to zero; if partitioned rotation ever becomes a real
43
- -- workflow, the floor needs a cross-node guard, not a local count.
44
- constraint MinOneOwner check on delete (
45
- (select count(1) from OwnerKey) >= 1
46
- ),
47
- -- Enrollment is insert + delete only; rotation is add-then-remove. No writer
48
- -- updates an OwnerKey row, and a self-rotation branch would double as a sole-owner
49
- -- takeover: a deferred post-image count is also true of an update that re-points
50
- -- the only row at an attacker key. Mirrors Strand.Manager.NoUpdate.
51
- constraint NoUpdate check on update (false),
52
- -- NOTE: the domain tag scopes an approval to a table and an action, not to a PARTY. Two
53
- -- parties that share an owner key would accept each other's approvals. Fine today (each
54
- -- party has its own owner key); if shared-owner multi-party ever ships, bind a party
55
- -- identity row into these digests.
56
- constraint Authorized check on insert, delete (
57
- -- Every authorizer is read from the PRE-transaction snapshot (committed.*):
58
- -- this CHECK auto-defers to commit (it has a subquery), by which point the row
59
- -- being inserted — and any sibling inserted alongside it — is live, so a plain
60
- -- from-OwnerKey would let a row authorize itself, or two strangers seat each
61
- -- other. committed.* excludes both, stating the rule directly: the authorizer
62
- -- must have existed BEFORE this transaction.
63
-
64
- -- Every signed digest below (and in every other CadreControl table) leads with two
65
- -- fixed literals — a 'CadreControl.<Table>' domain tag and an 'add'/'remove'/
66
- -- 'vouch'/'publish' action tag (cadre-core control-authorization.ts) — so an
67
- -- approval verifies ONLY against the one rule it was minted for. Without them,
68
- -- e.g. a ValidationKey enrollment approval covered the identical bytes as an
69
- -- OwnerKey enrollment: a narrow grant that doubled as full ownership.
70
-
71
- -- Bootstrap: the FOUNDING transaction — one whose pre-transaction owner set is
72
- -- empty — needs no authorization, and every row it inserts rides this branch
73
- -- (whoever writes it already owns the party outright, so a co-founder row grants
74
- -- nothing a single row would not). Gated on the PRE-transaction count, so a
75
- -- same-transaction swap of the sole owner (delete the founder, insert the
76
- -- attacker — post-image count still 1) cannot ride it.
77
- (old.Key is null and (select count(1) from committed.OwnerKey) = 0)
78
-
79
- -- or a pre-existing owner authorizes by signing over THIS row (Key, StampId)
80
- or (old.Key is null and exists (select 1 from committed.OwnerKey A where A.Key = context.OwnerKey and verify(digest('CadreControl.OwnerKey', 'add', new.Key, new.StampId), context.Signature, A.Key, 'ed25519')))
81
-
82
- -- or a removal authorized by ANOTHER pre-existing owner over the DISTINCT
83
- -- 'remove'-tagged digest bound to the stored row's StampId, so an enrollment
84
- -- signature can never be replayed as a removal (cf. CadrePeer.AuthorizedDelete).
85
- -- A.Key <> old.Key: an owner cannot sign its own removal (no self-resignation).
86
- or (new.Key is null and exists (select 1 from committed.OwnerKey A where A.Key = context.OwnerKey and A.Key <> old.Key and verify(digest('CadreControl.OwnerKey', 'remove', old.Key, old.StampId), context.Signature, A.Key, 'ed25519')))
87
- )
88
- ) with context (OwnerKey text null, Signature text null);
89
-
90
- -- A key that can validate a strand formation disclosure
91
- table ValidationKey (
92
- Key text primary key,
93
- StampId text not null unique, -- single-use authorization nonce (anti-replay).
94
- -- \`unique\` holds over LIVE rows only; a removed row's stamp is retired
95
- -- permanently into Revocation (NotRevoked below), so the never-expiring
96
- -- enrollment signature cannot resurrect a removed key.
97
- -- A removed row's StampId is retired into Revocation, and this refuses any insert naming
98
- -- a retired stamp: the approval that seated this row can never re-seat it after removal.
99
- -- Same rationale as OwnerKey.NotRevoked, stated in full there.
100
- -- NOTE: this table gained a consumer — FormationUsage.Authorized reads it to gate redemption of
101
- -- a ValidationUrl invite — so the convergence gap noted on Strand.StampId now has teeth here: on
102
- -- a node that has not yet seen the tombstone, a replayed enrollment re-seats the key and it can
103
- -- approve redemptions there until the Revocation row arrives. Still no per-request READ surface to
104
- -- filter (the gate is a write-time CHECK), so there is nothing to add today; if one appears, filter
105
- -- retired stamps there.
106
- constraint NotRevoked check on insert (
107
- not exists (select 1 from Revocation R where R.TableName = 'ValidationKey' and R.StampId = new.StampId)
108
- ),
109
- -- Retirement is MANDATORY: a delete must carry the matching Revocation row in the same
110
- -- transaction, or the stamp would free up again. Deferred (subquery), so the sibling
111
- -- insert is visible at commit regardless of statement order. The tombstone must also
112
- -- name the removed ROW, not just its stamp — see Revocation.RowKey.
113
- constraint RevocationRecorded check on delete (
114
- exists (select 1 from Revocation R where R.TableName = 'ValidationKey' and R.StampId = old.StampId and R.RowKey = old.Key)
115
- ),
116
- -- Enrollment is insert + delete only; rotation is add-then-remove. No writer in the repo
117
- -- updates a ValidationKey row. Required because the AuthorizedInsert / AuthorizedDelete
118
- -- pair below covers neither update — and a bare \`check\`, which is what this rule used to
119
- -- be, covers insert+update but NOT delete. Mirrors OwnerKey.NoUpdate.
120
- constraint NoUpdate check on update (false),
121
- constraint AuthorizedInsert check on insert (
122
- -- Owners authorize by signing over THIS row (Key, StampId); single-use via unique StampId
123
- exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and verify(digest('CadreControl.ValidationKey', 'add', new.Key, new.StampId), context.Signature, A.Key, 'ed25519'))
124
- ),
125
- constraint AuthorizedDelete check on delete (
126
- -- Delete is authorized by a signature over the DISTINCT 'remove'-tagged digest bound to
127
- -- the STORED row (Key, StampId), so an enrollment approval — which never expires — can
128
- -- never be replayed as a removal. Same shape as CadrePeer.AuthorizedDelete.
129
- exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and verify(digest('CadreControl.ValidationKey', 'remove', old.Key, old.StampId), context.Signature, A.Key, 'ed25519'))
130
-
131
- -- or REAP: a COMMITTED tombstone already retires this exact row incarnation, so a node
132
- -- that was offline at removal time may delete the stale row locally with no owner key.
133
- -- Why committed.* and why the stamp must be bound: stated in full on
134
- -- CadrePeer.AuthorizedDelete.
135
- or exists (select 1 from committed.Revocation R where R.TableName = 'ValidationKey' and R.RowKey = old.Key and R.StampId = old.StampId)
136
- )
137
- ) with context (OwnerKey text, Signature text);
138
-
139
- -- A network of members sharing an sApp database, each contributing peer nodes (their cadre) to the overall cohort
140
- -- Cadre peers should participate in each of these strands
141
- table Strand (
142
- Id text primary key, -- UUID
143
- MemberPrivateKey text null unique, -- Our private key as a member of this strand
144
- Type text, -- Types: 'o' = Open, 'c' = Closed -- Open can still control writes in the sApp, but only Closed controls reads
145
- FounderOwnerKey text null, -- ed25519 (base64url) owner key of the MACHINE that published this row
146
- -- (== insert context.OwnerKey; in the reference model each machine's
147
- -- owner key is the key behind its PeerId, so this names the founding
148
- -- machine — the one machine that runs the strand's one-time founder
149
- -- bootstrap). cadre-core derives "am I the founder?" at launch by
150
- -- comparing it to the node's own owner key (cadre-node.ts
151
- -- launchStrand). Null on a consent-seated strand — the consent branch
152
- -- of AuthorizedInsert carries no signature, so there is no
153
- -- trustworthy signer to record (see the NOTE on that branch below).
154
- -- Provenance, not content: cadre-node.ts strandRowMismatches
155
- -- deliberately excludes it from the identical-content comparison.
156
- StampId text not null unique, -- single-use authorization nonce (anti-replay).
157
- -- \`unique\` holds over LIVE rows only; a removed row's stamp is retired
158
- -- permanently into Revocation (NotRevoked below).
159
- -- NOTE: NotRevoked is a per-transaction check against LOCALLY VISIBLE rows, so a node that
160
- -- has not yet converged on the Revocation row can still accept a replayed add and end up
161
- -- holding the resurrected strand alongside its tombstone. CadrePeer has a read-side
162
- -- mitigation for this class (ControlDatabase.queryCadrePeers drops rows whose stamp is
163
- -- retired, so every membership reader inherits it); Strand and ValidationKey deliberately
164
- -- do NOT, because neither has a
165
- -- per-request authorization surface today. If one ever gains one, filter it there.
166
- constraint NotRevoked check on insert (
167
- not exists (select 1 from Revocation R where R.TableName = 'Strand' and R.StampId = new.StampId)
168
- ),
169
- -- Retirement is MANDATORY: a delete must carry the matching Revocation row in the same
170
- -- transaction, or the stamp would free up again. Deferred (subquery), so the sibling
171
- -- insert is visible at commit regardless of statement order. The tombstone must also
172
- -- name the removed ROW, not just its stamp — see Revocation.RowKey. That is what makes
173
- -- the consent branch of AuthorizedInsert below trustworthy: a removal cannot file a
174
- -- tombstone that omits or misnames the id it retires.
175
- constraint RevocationRecorded check on delete (
176
- exists (select 1 from Revocation R where R.TableName = 'Strand' and R.StampId = old.StampId and R.RowKey = old.Id)
177
- ),
178
- -- Strand rows are insert + delete only. Required: the consent branch of AuthorizedInsert
179
- -- below authorizes by the EXISTENCE of a FormationUsage row naming (new.Id, new.StampId)
180
- -- and carries no signature, so while this rule was a bare \`check\` (insert+update) it also
181
- -- said "anyone may REWRITE any consent-formed strand, unsigned" — flipping Type to 'o' and
182
- -- nulling MemberPrivateKey destroys the party's own membership key for that network in place.
183
- -- Mirrors OwnerKey.NoUpdate / FormationInvite.Immutable.
184
- constraint NoUpdate check on update (false),
185
- constraint AuthorizedInsert check on insert (
186
- -- Authorized by an owner signing over THIS row (Id, Type, MemberPrivateKey, StampId); single-use via unique StampId.
187
- -- FounderOwnerKey persists WHO published the row, so the founding machine can later
188
- -- recognise its own strand (founder derivation, cadre-node.ts launchStrand). The
189
- -- equality pins the stored column to the VERIFIED signer — same shape as
190
- -- CadrePeer.VouchOwner — so a writer cannot record a founder it is not. Deliberately
191
- -- NOT added to the signed digest: the equality already binds it to the signature's
192
- -- context.OwnerKey, and widening the digest would churn every existing
193
- -- insertStrand field-order contract for nothing.
194
- (exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and verify(digest('CadreControl.Strand', 'add', new.Id, new.Type, coalesce(new.MemberPrivateKey, ''), new.StampId), context.Signature, A.Key, 'ed25519'))
195
- and new.FounderOwnerKey = context.OwnerKey)
196
-
197
- -- or authorized WITHOUT a signature by a redemption record naming THIS EXACT ROW: a
198
- -- FormationUsage row carrying both this strand's Id and its one-off StampId
199
- -- (control-database.ts:redeemInvitation writes the pair in one transaction; both
200
- -- CHECKs defer, so each sees the other's row at commit).
201
- --
202
- -- Matching the STAMP, not the id alone, is what makes a removal stick. FormationUsage
203
- -- is append-only, so a redemption record outlives the strand it formed forever; keyed
204
- -- on the id alone it re-authorized ANY later insert of that id, so after a legitimate
205
- -- owner-signed, tombstoned removal any writer could re-seat the strand unsigned with a
206
- -- fresh StampId (sidestepping NotRevoked, which retires only the removed row's stamp)
207
- -- and an ATTACKER-CHOSEN MemberPrivateKey — the party's own secret for that network.
208
- -- Bound to the stamp, the removal retires that stamp into Revocation and NotRevoked
209
- -- closes this consent record's branch permanently.
210
- --
211
- -- Beyond the stamp match, the branch is deliberately NARROW — each clause closes one door:
212
- -- * Type = 'o' / MemberPrivateKey null — a consent-seated strand looks exactly like
213
- -- what the unbound redemption path writes (open, keyless). A closed strand carrying
214
- -- a caller-chosen member key (the party's own read-gating secret for that network)
215
- -- has no legitimate producer. MemberKeyClosedOnly already implies the second clause
216
- -- from the first today; both are stated so this rule does not silently depend on
217
- -- another constraint staying as it is.
218
- -- * the inner FI.StrandId is null — only an UNBOUND invite may seat a strand at all.
219
- -- A bound invite's host strand is owner-provisioned up front; letting the invite
220
- -- consent-seat that id would allow an open, keyless downgrade of the real (possibly
221
- -- closed, key-bearing) host row on a node where it has not converged yet. The bound
222
- -- path records usage only (resolveStrand reports an absent host as missing and the
223
- -- formation manager rejects cleanly).
224
- -- * the not-exists over OTHER stamps — a strand id may be consent-seated once, EVER.
225
- -- FormationUsage is append-only, so the first redemption's record survives a later
226
- -- owner-signed removal and permanently forecloses a second consent-seating of the
227
- -- id (a spare use of the same token minted a new stamp and rode this branch to
228
- -- re-seat the id with an attacker-chosen row). Re-joining a removed id is
229
- -- owner-gated: the owner re-seats it signed (fresh stamp, fresh signature) and
230
- -- issues a BOUND invite the returning party records consent against — the id is
231
- -- not blacklisted, only its unsigned re-seat is. This cannot be weaponised to
232
- -- pre-block a legitimate id: StrandExists refuses a usage row naming a strand that
233
- -- does not exist, so no usage row can be pre-planted for an unseated id — and ids
234
- -- are 128 random bytes anyway.
235
- -- * the not-exists over Revocation — a strand id may be consent-seated once EVER
236
- -- and never after ANY removal of that id, however it was seated. The usage-row
237
- -- rule above binds only ids that ever carried a consent record; an id seated
238
- -- purely owner-signed (insertStrand, no usage row) leaves nothing behind on
239
- -- removal, so its removal needs its own trace. Every removal files a Revocation
240
- -- row naming the removed row's key (RowKey), and this clause refuses any id
241
- -- that has ever been tombstoned. RevocationRecorded above makes that trace
242
- -- mandatory AND correctly named, so no removal can slip past this clause. The
243
- -- usage-row clause is arguably subsumed by this one (a usage row implies the
244
- -- strand row existed, and its removal must file a tombstone), but it reaches
245
- -- the same conclusion without depending on Revocation at all; both are stated,
246
- -- per the style note above. The owner-signed branch is deliberately unaffected,
247
- -- so re-join stays owner-gated exactly as described.
248
- -- What is NOT schema-verified is a STANDALONE tombstone (one with no
249
- -- accompanying delete — see Revocation.Authorized): an owner can file one
250
- -- naming an id that never existed and thereby permanently foreclose
251
- -- consent-seating of that id — owner-only, the owner already controls invite
252
- -- issuance, and unbound ids are 128 random bytes minted at redemption, so no
253
- -- escalation (same reasoning as the pre-plant note above).
254
- -- NOTE: like NotRevoked above, this is a write-time check against LOCALLY
255
- -- VISIBLE rows — a node that has not yet converged on the Revocation row still
256
- -- accepts the re-seat. Fails in the safe direction: the tombstone wins after
257
- -- merge for the STAMP, and the resurrected row coexists with it (same class as
258
- -- the StampId note above).
259
- or (
260
- new.Type = 'o'
261
- and new.MemberPrivateKey is null
262
- -- NOTE: a consent-seated strand records NO founder machine — this branch
263
- -- carries no signature, so there is no trustworthy signer to persist.
264
- -- Consequence: the restart-before-founding orphan shape (a relaunch cannot
265
- -- derive founder-ness from the row) persists for consent-formed strands
266
- -- only; the responder that provisions one must keep passing an explicit
267
- -- founder flag at launch. Revisit if consent-formed strands ever hit the
268
- -- empty-Header symptom in practice: FormationUsage names the redeeming
269
- -- peer (PeerKey), which is the candidate derivation source.
270
- and new.FounderOwnerKey is null
271
- and exists (
272
- select 1 from FormationUsage FU
273
- where FU.StrandId = new.Id
274
- and FU.StrandStampId = new.StampId
275
- and exists (select 1 from FormationInvite FI where FI.Token = FU.Token and FI.StrandId is null)
276
- )
277
- and not exists (
278
- select 1 from FormationUsage FU2
279
- where FU2.StrandId = new.Id and FU2.StrandStampId <> new.StampId
280
- )
281
- and not exists (
282
- select 1 from Revocation R
283
- where R.TableName = 'Strand' and R.RowKey = new.Id
284
- )
285
- )
286
- ),
287
- constraint AuthorizedDelete check on delete (
288
- -- Delete is authorized by a signature over the DISTINCT 'remove'-tagged digest bound to
289
- -- the STORED row (Id, StampId), so the add approval — which never expires — can never be
290
- -- replayed as a removal. Note the consent branch above is deliberately NOT mirrored
291
- -- here: an invitation authorizes forming a strand, never destroying one.
292
- --
293
- -- DELIBERATELY no REAP branch (cf. CadrePeer.AuthorizedDelete): this row carries
294
- -- MemberPrivateKey — the party's own membership secret for that network, stored nowhere
295
- -- else — so this is the one guarded delete whose effect is unrecoverable, and a stale
296
- -- row is already inert for the purpose a tombstone serves (the consent branch of
297
- -- AuthorizedInsert above refuses any id ever tombstoned). Do not "fix" the asymmetry;
298
- -- tickets/backlog/debt-strand-tombstone-reap.md owns any future change.
299
- exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and verify(digest('CadreControl.Strand', 'remove', old.Id, old.StampId), context.Signature, A.Key, 'ed25519'))
300
- ),
301
- constraint MemberKeyClosedOnly check (
302
- -- An open strand ('o') has no membership gate, so it must not carry a
303
- -- member key. A non-null MemberPrivateKey requires a closed strand ('c').
304
- new.MemberPrivateKey is null or new.Type = 'c'
305
- )
306
- ) with context (OwnerKey text, Signature text);
307
-
308
- -- ONE party's own membership identity for one strand: the private key whose public
309
- -- half seats this party's \`Strand.Member\` / \`Strand.Manager\` rows. Deliberately a
310
- -- SEPARATE key from Strand.MemberPrivateKey — that column is the strand-wide read
311
- -- secret formation hands to EVERY joining party, so an identity derived from it is
312
- -- one every joiner can forge (gotchoices/sereus#4). A side table rather than a
313
- -- Strand column because a JOINER holds no control Strand row at all (reference apps
314
- -- attach formed strands from an in-memory row) yet must persist its own party key
315
- -- too; keyed by strand id it serves founder and joiner uniformly, and it leaves the
316
- -- heavily-audited Strand insert digest untouched. Deliberately NO closed-strands-only
317
- -- cross-table check: the joiner writes this row with no local Strand row to check
318
- -- against. Party-private in the same sense as MemberPrivateKey — replicated in
319
- -- plaintext to every machine the party owns (docs/strands.md → "Closed-Strand Member
320
- -- Key Handling"; that replication is what lets any of the party's machines sign
321
- -- membership writes) — but unlike MemberPrivateKey it is NEVER put on the formation
322
- -- wire.
323
- table StrandPartyKey (
324
- Id text primary key, -- the strand id this key is for
325
- PrivateKey text not null, -- THIS party's ed25519 strand member private key, base64
326
- -- protobuf — the same encoding as Strand.MemberPrivateKey;
327
- -- decode with cadre-core's strandMemberKeyPair.
328
- StampId text not null unique, -- single-use authorization nonce (anti-replay).
329
- -- \`unique\` holds over LIVE rows only; a removed row's stamp is
330
- -- retired permanently into Revocation (NotRevoked below).
331
- -- A removed row's StampId is retired into Revocation, and this refuses any insert
332
- -- naming a retired stamp: the approval that seated this row can never re-seat it
333
- -- after removal. Same rationale as OwnerKey.NotRevoked, stated in full there.
334
- constraint NotRevoked check on insert (
335
- not exists (select 1 from Revocation R where R.TableName = 'StrandPartyKey' and R.StampId = new.StampId)
336
- ),
337
- -- Retirement is MANDATORY: a delete must carry the matching Revocation row in the same
338
- -- transaction, or the stamp would free up again. Deferred (subquery), so the sibling
339
- -- insert is visible at commit regardless of statement order. The tombstone must also
340
- -- name the removed ROW, not just its stamp — see Revocation.RowKey.
341
- constraint RevocationRecorded check on delete (
342
- exists (select 1 from Revocation R where R.TableName = 'StrandPartyKey' and R.StampId = old.StampId and R.RowKey = old.Id)
343
- ),
344
- -- Insert + delete only; a rotation is remove-then-insert (fresh stamp, fresh
345
- -- signature). Required because AuthorizedInsert / AuthorizedDelete below cover
346
- -- neither update — an in-place rewrite would swap the party's identity key with
347
- -- no signature at all. Mirrors ValidationKey.NoUpdate.
348
- constraint NoUpdate check on update (false),
349
- constraint AuthorizedInsert check on insert (
350
- -- Owners authorize by signing over THIS row (Id, PrivateKey, StampId);
351
- -- single-use via unique StampId. Binding PrivateKey means a captured approval
352
- -- can only ever reproduce the exact key it approved, never seat an
353
- -- attacker-chosen identity under the party's name.
354
- exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and verify(digest('CadreControl.StrandPartyKey', 'add', new.Id, new.PrivateKey, new.StampId), context.Signature, A.Key, 'ed25519'))
355
- ),
356
- constraint AuthorizedDelete check on delete (
357
- -- Delete is authorized by a signature over the DISTINCT 'remove'-tagged digest
358
- -- bound to the STORED row (Id, StampId), so the enrollment approval — which
359
- -- never expires — can never be replayed as a removal.
360
- -- DELIBERATELY no REAP branch (cf. ValidationKey.AuthorizedDelete): like
361
- -- Strand.MemberPrivateKey, PrivateKey is a secret stored nowhere else, so this
362
- -- delete's effect is unrecoverable and stays strictly owner-signed. Same
363
- -- reasoning as Strand.AuthorizedDelete.
364
- exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and verify(digest('CadreControl.StrandPartyKey', 'remove', old.Id, old.StampId), context.Signature, A.Key, 'ed25519'))
365
- )
366
- ) with context (OwnerKey text, Signature text);
367
-
368
- -- A strand THIS party joined from ANOTHER party. The founding party's control database
369
- -- holds its Strand row, so without this table only the machine that joined would know of
370
- -- it. Every machine of the party reads it (the strand watcher offers each row like a
371
- -- Strand row), so a replica host keeps a copy and the party's other machines can attach.
372
- -- A separate table rather than a Strand row with a provenance marker: Strand means "this
373
- -- party's own strand" to every reader of it (founder derivation, FormationUsage.StrandExists,
374
- -- unpublish, the consent branch of Strand.AuthorizedInsert), and each would need a
375
- -- provenance check. MemberPrivateKey is the closed strand's shared read secret the
376
- -- formation delivered — replicated in plaintext to every machine the party owns, under the
377
- -- same accepted risk as Strand.MemberPrivateKey (docs/strands.md → "Closed-Strand Member
378
- -- Key Handling"). Unlike that column it is not held only here: the founding party and
379
- -- every member hold it, so a removed row is recoverable by re-forming, which is why this
380
- -- table (unlike Strand) carries a REAP branch.
381
- table JoinedStrand (
382
- Id text primary key, -- the joined strand's id (the founder's Strand.Id)
383
- Type text not null, -- 'o' | 'c', as Strand.Type
384
- MemberPrivateKey text null, -- closed strand's shared read secret; null for open
385
- StampId text not null unique, -- single-use authorization nonce (anti-replay).
386
- -- \`unique\` holds over LIVE rows only; a removed row's stamp is
387
- -- retired permanently into Revocation (NotRevoked below).
388
- -- The approval that seated a removed row can never re-seat it. Same rationale as
389
- -- OwnerKey.NotRevoked, stated in full there.
390
- constraint NotRevoked check on insert (
391
- not exists (select 1 from Revocation R where R.TableName = 'JoinedStrand' and R.StampId = new.StampId)
392
- ),
393
- -- A delete must file its correctly named tombstone in the same transaction. Same
394
- -- rationale as StrandPartyKey.RevocationRecorded.
395
- constraint RevocationRecorded check on delete (
396
- exists (select 1 from Revocation R where R.TableName = 'JoinedStrand' and R.StampId = old.StampId and R.RowKey = old.Id)
397
- ),
398
- -- Insert + delete only: neither Authorized rule below covers update, so an in-place
399
- -- rewrite would swap the read secret unsigned. Mirrors StrandPartyKey.NoUpdate.
400
- constraint NoUpdate check on update (false),
401
- -- Readers cast Type to 'o' | 'c' (ControlDatabase.queryJoinedStrands).
402
- constraint KnownType check (new.Type = 'o' or new.Type = 'c'),
403
- -- An open strand has no read gate, so no read secret. Same rule as Strand.MemberKeyClosedOnly.
404
- constraint MemberKeyClosedOnly check (new.MemberPrivateKey is null or new.Type = 'c'),
405
- constraint AuthorizedInsert check on insert (
406
- -- Owners authorize by signing over THIS row (Id, Type, MemberPrivateKey, StampId),
407
- -- MemberPrivateKey signing as '' when null, so a captured approval can only reproduce
408
- -- the exact secret it approved. Owner-signed, deliberately NOT self-signable by an
409
- -- enrolled CadrePeer: every always-on machine of the party downloads the strands this
410
- -- table names, so a non-owner machine could otherwise make them all host an arbitrary
411
- -- strand. A machine that is not an owner keeps its joins machine-local.
412
- exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and verify(digest('CadreControl.JoinedStrand', 'add', new.Id, new.Type, coalesce(new.MemberPrivateKey, ''), new.StampId), context.Signature, A.Key, 'ed25519'))
413
- ),
414
- constraint AuthorizedDelete check on delete (
415
- -- 'remove'-tagged digest over the STORED row (Id, StampId), so the add approval can
416
- -- never be replayed as a removal.
417
- exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and verify(digest('CadreControl.JoinedStrand', 'remove', old.Id, old.StampId), context.Signature, A.Key, 'ed25519'))
418
-
419
- -- or REAP: a COMMITTED tombstone already retires this exact row incarnation.
420
- -- Why committed.* and why the stamp is bound: stated in full on
421
- -- CadrePeer.AuthorizedDelete. Allowed here, unlike Strand / StrandPartyKey,
422
- -- because MemberPrivateKey is recoverable (see the table comment).
423
- or exists (select 1 from committed.Revocation R where R.TableName = 'JoinedStrand' and R.RowKey = old.Id and R.StampId = old.StampId)
424
- )
425
- ) with context (OwnerKey text, Signature text);
426
-
427
- -- A join THIS party asked for, by redeeming another party's invitation, that it has not
428
- -- finished yet, or has finished and not yet dismissed. Party-wide rather than kept on the
429
- -- device that asked, so the attempt outlives that device and any owner machine of the party
430
- -- can carry it on and record how it ended. Invitation is a bearer credential and
431
- -- MembershipInvite a single-use one; both are replicated in plaintext to every machine the
432
- -- party owns, under the same accepted risk as JoinedStrand.MemberPrivateKey (docs/strands.md
433
- -- → "Closed-Strand Member Key Handling").
434
- -- Every write is a whole-row insert or delete: recording an outcome deletes the row (with its
435
- -- tombstone) and inserts its successor under a fresh stamp in one transaction
436
- -- (ControlDatabase.replacePendingJoin). Which outcome may follow which is CadreNode policy,
437
- -- not a rule here, because every writer is an owner.
438
- -- Carries a REAP branch, like JoinedStrand, because nothing here is stored only here: the
439
- -- inviting party holds the FormationInvite row, the user holds the shared link, and the
440
- -- membership invitation is a copy of what the inviter issued.
441
- table PendingJoin (
442
- Id text primary key, -- base64url sha256 of the invitation token (cadre-core's
443
- -- pendingJoinId): one row per invitation, and the Revocation
444
- -- tombstone that names a removed row never holds the token.
445
- Invitation text not null, -- CadreNode.encodeInvitation: token, sAppId, expiration and
446
- -- bootstrap addresses. A few kilobytes at most, since
447
- -- createOpenInvitation caps the addresses an invitation carries.
448
- Disclosure text not null, -- canonicalJson of the disclosure the requester gave; the
449
- -- formation sets partyId itself on each attempt
450
- RequestedAt integer not null, -- epoch ms
451
- ExpiresAt integer not null, -- epoch ms; no attempt starts at or after it
452
- Outcome text null, -- null while pending | 'joined' | 'failed'
453
- OutcomeAt integer null, -- epoch ms the outcome was recorded
454
- StrandId text null, -- 'joined': the strand the formation returned
455
- MembershipInvite text null, -- 'joined', closed strand only: JSON {inviteKey, invitePrivateKey},
456
- -- the StrandMembershipInvite the formation delivered
457
- FailureCode text null, -- 'failed': a FormationRejectionCode, 'expired' or 'local'
458
- FailureReason text null, -- 'failed': human-readable text
459
- StampId text not null unique, -- single-use authorization nonce (anti-replay).
460
- -- \`unique\` holds over LIVE rows only; a removed row's stamp is
461
- -- retired permanently into Revocation (NotRevoked below).
462
- -- The approval that seated a removed row can never re-seat it, so a dismissed or replaced
463
- -- row cannot be brought back. Same rationale as OwnerKey.NotRevoked, stated in full there.
464
- constraint NotRevoked check on insert (
465
- not exists (select 1 from Revocation R where R.TableName = 'PendingJoin' and R.StampId = new.StampId)
466
- ),
467
- -- A delete must file its correctly named tombstone in the same transaction. Same
468
- -- rationale as StrandPartyKey.RevocationRecorded.
469
- constraint RevocationRecorded check on delete (
470
- exists (select 1 from Revocation R where R.TableName = 'PendingJoin' and R.StampId = old.StampId and R.RowKey = old.Id)
471
- ),
472
- -- Insert + delete only: neither Authorized rule below covers update, so an in-place
473
- -- rewrite would change the outcome unsigned. Mirrors StrandPartyKey.NoUpdate.
474
- constraint NoUpdate check on update (false),
475
- -- Readers cast Outcome to null | 'joined' | 'failed' (ControlDatabase.queryPendingJoins).
476
- constraint KnownOutcome check (new.Outcome is null or new.Outcome = 'joined' or new.Outcome = 'failed'),
477
- -- Each outcome carries exactly its own columns, so a reader can trust the shape it reads.
478
- constraint OutcomeShape check (
479
- (new.Outcome is null
480
- and new.OutcomeAt is null and new.StrandId is null and new.MembershipInvite is null
481
- and new.FailureCode is null and new.FailureReason is null)
482
- or (new.Outcome = 'joined'
483
- and new.OutcomeAt is not null and new.StrandId is not null
484
- and new.FailureCode is null and new.FailureReason is null)
485
- or (new.Outcome = 'failed'
486
- and new.OutcomeAt is not null and new.FailureCode is not null
487
- and new.StrandId is null and new.MembershipInvite is null)
488
- ),
489
- constraint AuthorizedInsert check on insert (
490
- -- Owners authorize by signing over THIS row, every column ending in StampId; nullable
491
- -- columns sign as '' and integers as their text, so a captured approval can only
492
- -- reproduce the exact row it approved. Owner-signed only: whether a machine that is
493
- -- not an owner may record an outcome is an open decision
494
- -- (tickets/blocked/decide-non-owner-machine-completes-a-pending-join.md).
495
- exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and verify(digest('CadreControl.PendingJoin', 'add',
496
- new.Id, new.Invitation, new.Disclosure, cast(new.RequestedAt as text), cast(new.ExpiresAt as text),
497
- coalesce(new.Outcome, ''), coalesce(cast(new.OutcomeAt as text), ''), coalesce(new.StrandId, ''),
498
- coalesce(new.MembershipInvite, ''), coalesce(new.FailureCode, ''), coalesce(new.FailureReason, ''),
499
- new.StampId), context.Signature, A.Key, 'ed25519'))
500
- ),
501
- constraint AuthorizedDelete check on delete (
502
- -- 'remove'-tagged digest over the STORED row (Id, StampId), so the add approval can
503
- -- never be replayed as a removal.
504
- exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and verify(digest('CadreControl.PendingJoin', 'remove', old.Id, old.StampId), context.Signature, A.Key, 'ed25519'))
505
-
506
- -- or REAP: a COMMITTED tombstone already retires this exact row incarnation.
507
- -- Why committed.* and why the stamp is bound: stated in full on
508
- -- CadrePeer.AuthorizedDelete. Allowed for the reason the table comment gives.
509
- or exists (select 1 from committed.Revocation R where R.TableName = 'PendingJoin' and R.RowKey = old.Id and R.StampId = old.StampId)
510
- )
511
- ) with context (OwnerKey text, Signature text);
512
-
513
- -- A peer (node) that is part of the cadre, carrying a self-published,
514
- -- freshness-stamped, self-signed address record (see PeerAddressRecord in cadre-core).
515
- -- The row IS the peer-address record: a resolver reads it, re-verifies Sig against
516
- -- PublicKey, checks freshness (UpdatedAt) and trust, then dials the addrs.
517
- table CadrePeer (
518
- PeerId text primary key,
519
- PublicKey text null, -- ed25519 (base64url) whose libp2p identity == PeerId (null if non-Ed25519)
520
- Multiaddr text, -- comma-joined current addrs (signaling / p2p-circuit first)
521
- UpdatedAt int null, -- epoch ms; strictly increases per self-update (replay/rollback guard)
522
- Sig text null, -- ed25519 self-signature over the signed payload (base64url); null until self-published
523
- StampId text not null unique, -- per-row authorization nonce, bound into the voucher/remove digests; rotates on
524
- -- (re)insert. A removed row's stamp is retired permanently into Revocation
525
- -- (NotRevoked below), so a captured admission approval cannot re-seat a removed peer.
526
- VouchOwner text null, -- ed25519 (base64url) owner key that vouched this membership (== insert context.OwnerKey)
527
- VouchSig text null, -- that owner's signature over the 'vouch'-tagged digest below (== insert context.Signature). A reader checks VouchOwner against its NODE-LOCAL trusted-owner anchor (not this replicated table, which a self-owner can pollute) to decide authorized membership. The domain/action tags keep this STORED, REPLICATED signature useless against every other rule (pre-tag it satisfied OwnerKey/ValidationKey inserts for Key = PeerId).
528
- -- A removed row's StampId is retired into Revocation, and this refuses any insert
529
- -- naming a retired stamp: the approval that seated this row can never re-seat it after
530
- -- removal. \`unique\` alone did not do this — it only holds over LIVE rows, so a delete
531
- -- freed the stamp and the original never-expiring signature verified again. A
532
- -- legitimate re-add mints a FRESH StampId and a fresh signature, so it is unaffected.
533
- constraint NotRevoked check on insert (
534
- not exists (select 1 from Revocation R where R.TableName = 'CadrePeer' and R.StampId = new.StampId)
535
- ),
536
- -- Retirement is MANDATORY: a delete must carry the matching Revocation row in the same
537
- -- transaction, or the stamp would free up again. Deferred (subquery), so the sibling
538
- -- insert is visible at commit regardless of statement order. The tombstone must also
539
- -- name the removed ROW, not just its stamp — see Revocation.RowKey.
540
- constraint RevocationRecorded check on delete (
541
- exists (select 1 from Revocation R where R.TableName = 'CadrePeer' and R.StampId = old.StampId and R.RowKey = old.PeerId)
542
- ),
543
- constraint AuthorizedInsert check on insert (
544
- -- Authorized by an owner key, which vouches both membership and the
545
- -- PublicKey<->PeerId binding (cadre-core derives PublicKey from PeerId before insert).
546
- -- The digest binds PeerId + the single-use StampId nonce (two TEXT fields),
547
- -- matching cadre-core peer-authorization.ts:cadrePeerVoucherDigest. The same
548
- -- 'vouch'-tagged digest is DELIBERATELY shared with the owner branch of
549
- -- AuthorizedUpdate below: both mean "this owner vouches this membership row".
550
- exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and verify(digest('CadreControl.CadrePeer', 'vouch', new.PeerId, new.StampId), context.Signature, A.Key, 'ed25519'))
551
- -- Persist the vouching (owner, signature) onto the row so a reader can later
552
- -- re-check VouchOwner against its node-local anchor. The stored pair MUST equal
553
- -- the pair the verify above validated, so a writer cannot store a voucher it did
554
- -- not actually receive a signature for.
555
- and new.VouchOwner = context.OwnerKey
556
- and new.VouchSig = context.Signature
557
- ),
558
- constraint AuthorizedDelete check on delete (
559
- -- Delete is authorized by a signature over the DISTINCT 'remove'-tagged digest bound
560
- -- to the row's StampId (cadre-core peer-authorization.ts:cadrePeerRemoveDigest), so
561
- -- the stored voucher (a signature over the 'vouch'-tagged digest) can NEVER be
562
- -- replayed to authorize a delete. The remove signature rides in context and is never stored.
563
- exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and verify(digest('CadreControl.CadrePeer', 'remove', old.PeerId, old.StampId), context.Signature, A.Key, 'ed25519'))
564
-
565
- -- or REAP: this node already holds a COMMITTED tombstone retiring THIS EXACT row
566
- -- incarnation, so the party owner has already authorized this row's removal and this
567
- -- node is merely catching up. That is what lets a node which was offline at
568
- -- revocation time delete the stale row locally, with no owner private key
569
- -- (control-database.ts:reapRevokedRow). Same shape as the consent branch of
570
- -- Strand.AuthorizedInsert: authorization by the EXISTENCE of a row rather than by a
571
- -- signature — and that row is itself owner-signed (Revocation.Authorized), so this
572
- -- widens WHO may execute a removal, never WHO may decide one. RevocationRecorded is
573
- -- satisfied by the same committed tombstone, so a reap files no second one.
574
- --
575
- -- committed.Revocation, NOT Revocation — and the stamp clause — are both load-bearing:
576
- -- * committed.* : this CHECK defers to commit (it has a subquery), by which point a
577
- -- tombstone written in the SAME transaction is live. Reading plain Revocation
578
- -- would therefore let deleteGuardedRow's own sibling tombstone satisfy this
579
- -- branch, making the 'remove'-tagged delete signature above dead weight and
580
- -- collapsing two domain-separated approvals into one. committed.* states the rule
581
- -- exactly: the tombstone must have existed BEFORE this transaction.
582
- -- * R.StampId = old.StampId : binds the ROW INCARNATION, not the name. One name may
583
- -- legitimately carry several tombstones over its life (seat -> delete -> owner
584
- -- re-seat -> delete). Without this clause a tombstone from a PREVIOUS incarnation
585
- -- would authorize deleting the CURRENT row, which the owner never removed.
586
- or exists (select 1 from committed.Revocation R where R.TableName = 'CadrePeer' and R.RowKey = old.PeerId and R.StampId = old.StampId)
587
- ),
588
- constraint AuthorizedUpdate check on update (
589
- -- Peer self-updates its own addrs + freshness, signing with its OWN ed25519 key
590
- -- (the key behind PeerId). PeerId/PublicKey/StampId/voucher are immutable on
591
- -- self-update, UpdatedAt must strictly increase (replay guard), and Sig is verified
592
- -- against the stored PublicKey over the same payload the publish path signs
593
- -- (cadre-core peer-record.ts:peerRecordSignedPayload).
594
- (
595
- new.PeerId = old.PeerId
596
- and new.PublicKey = old.PublicKey
597
- and new.StampId = old.StampId
598
- and new.VouchOwner = old.VouchOwner
599
- and new.VouchSig = old.VouchSig
600
- and new.UpdatedAt > coalesce(old.UpdatedAt, 0)
601
- and verify(
602
- digest('CadreControl.CadrePeer', 'publish', new.PeerId, new.Multiaddr, cast(new.UpdatedAt as text)),
603
- new.Sig, new.PublicKey, 'ed25519')
604
- )
605
- -- or an owner re-authorizes (rotation / correction): re-vouches the row over
606
- -- its current StampId, so the stored voucher is re-bound to the re-authorizing owner.
607
- -- StampId is immutable here too: an update that rotated it would leave the OLD
608
- -- stamp live-free and untombstoned (RevocationRecorded only retires the stamp a
609
- -- delete carries), so the original admission approval — which is stored on the
610
- -- replicated row as VouchSig — would resurrect the peer after a later removal.
611
- -- Retiring a stamp is delete + Revocation row, never an update.
612
- or (
613
- new.StampId = old.StampId
614
- and exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and verify(digest('CadreControl.CadrePeer', 'vouch', new.PeerId, new.StampId), context.Signature, A.Key, 'ed25519'))
615
- and new.VouchOwner = context.OwnerKey
616
- and new.VouchSig = context.Signature
617
- )
618
- )
619
- ) with context (OwnerKey text null, Signature text);
620
-
621
- -- A mobile cadre peer's platform push token (FCM/APNs), self-published so a
622
- -- server peer can deliver a push-wake to a suspended app over the platform push
623
- -- channel (a control-network dial cannot reach an OS-suspended process). The row
624
- -- mirrors CadrePeer: a resolver re-verifies Sig against the CadrePeer.PublicKey for
625
- -- this PeerId (see device-token.ts:deviceTokenSignedPayload), checks freshness, then
626
- -- hands the token to the push sender.
627
- table DeviceToken (
628
- PeerId text primary key, -- the CadrePeer this token belongs to
629
- Platform text not null, -- 'fcm' | 'apns'
630
- Token text not null, -- opaque platform device/registration token
631
- UpdatedAt int null, -- epoch ms; strictly increases per self-update (replay guard)
632
- Sig text null, -- ed25519 self-sig over (PeerId|Platform|Token|UpdatedAt)
633
- StampId text not null unique, -- single-use authorization nonce, bound into the add/remove digests;
634
- -- rotates on (re)insert. A cleared token's stamp is retired permanently
635
- -- into Revocation (NotRevoked below), so the approval that published a
636
- -- token cannot re-seat it after the owner cleared it (logout).
637
- -- A removed row's StampId is retired into Revocation, and this refuses any insert
638
- -- naming a retired stamp: the approval that seated this row can never re-seat it after
639
- -- the clear. \`unique\` alone did not do this — it only holds over LIVE rows, so a delete
640
- -- freed the stamp and the original never-expiring signature verified again. A legitimate
641
- -- re-register mints a FRESH StampId and a fresh signature, so it is unaffected. This
642
- -- matters more here than the stored-approval case (CadrePeer.VouchSig): a resurrected
643
- -- push token has NO freshness ceiling to retire it — cadre-node.ts:resolveDeviceToken
644
- -- defaults maxAgeMs to infinity by design — so retirement is the only thing that sticks.
645
- constraint NotRevoked check on insert (
646
- not exists (select 1 from Revocation R where R.TableName = 'DeviceToken' and R.StampId = new.StampId)
647
- ),
648
- -- Retirement is MANDATORY: a delete must carry the matching Revocation row in the same
649
- -- transaction, or the stamp would free up again. Deferred (subquery), so the sibling
650
- -- insert is visible at commit regardless of statement order. The tombstone must also
651
- -- name the removed ROW, not just its stamp — see Revocation.RowKey.
652
- constraint RevocationRecorded check on delete (
653
- exists (select 1 from Revocation R where R.TableName = 'DeviceToken' and R.StampId = old.StampId and R.RowKey = old.PeerId)
654
- ),
655
- constraint AuthorizedInsert check on insert (
656
- -- Authorized by an owner key (membership vouch), exactly like CadrePeer.
657
- -- The digest binds the WHOLE row behind the domain/action tags — every column,
658
- -- with the nullable UpdatedAt/Sig signing as '' when absent (the shape
659
- -- FormationInvite and Strand already use) — so a replayed approval can only ever
660
- -- reproduce the row it approved, never one carrying attacker-chosen
661
- -- Platform/Token/UpdatedAt; NotRevoked above then refuses even that exact row
662
- -- once it has been cleared. The DISTINCT 'remove'-tagged digest below keeps a
663
- -- captured insert approval from doubling as a delete (and vice versa).
664
- -- Mirrors cadre-core peer-authorization.ts:deviceTokenAddDigest.
665
- exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and verify(
666
- digest(
667
- 'CadreControl.DeviceToken', 'add',
668
- new.PeerId,
669
- new.Platform,
670
- new.Token,
671
- coalesce(cast(new.UpdatedAt as text), ''),
672
- coalesce(new.Sig, ''),
673
- new.StampId
674
- ),
675
- context.Signature, A.Key, 'ed25519'))
676
- ),
677
- constraint AuthorizedDelete check on delete (
678
- -- Bound to the STORED row's (PeerId, StampId) — cadre-core
679
- -- peer-authorization.ts:deviceTokenRemoveDigest — so the never-expiring insert
680
- -- approval can never be replayed as a clear, and a captured clear approval is
681
- -- dead once the stamp it names is retired.
682
- exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and verify(digest('CadreControl.DeviceToken', 'remove', old.PeerId, old.StampId), context.Signature, A.Key, 'ed25519'))
683
-
684
- -- or REAP: a COMMITTED tombstone already retires this exact row incarnation, so a node
685
- -- that was offline at removal time may delete the stale row locally with no owner key.
686
- -- Why committed.* and why the stamp must be bound: stated in full on
687
- -- CadrePeer.AuthorizedDelete.
688
- or exists (select 1 from committed.Revocation R where R.TableName = 'DeviceToken' and R.RowKey = old.PeerId and R.StampId = old.StampId)
689
- ),
690
- constraint AuthorizedUpdate check on update (
691
- -- Peer self-updates its own token, signing with its OWN ed25519 key (the key
692
- -- behind PeerId). PeerId and StampId are immutable, UpdatedAt must strictly
693
- -- increase (replay guard), and Sig is verified against the stored
694
- -- CadrePeer.PublicKey over the signed payload (cadre-core
695
- -- device-token.ts:deviceTokenSignedPayload). Platform and Token may change on
696
- -- self-update (platform switch / reinstall / rotation).
697
- -- StampId is immutable for the reason CadrePeer spells out: an update that
698
- -- rotated it would leave the OLD stamp live-free and untombstoned, so retiring a
699
- -- stamp stays delete + Revocation row, never an update.
700
- -- This is the ONLY branch: there is deliberately NO owner re-touch. The branch
701
- -- that used to sit beside it verified an owner signature over the peer id alone
702
- -- and sat OUTSIDE the monotonicity requirement, so one captured owner approval
703
- -- rewrote Platform/Token and rolled UpdatedAt BACKWARDS — and seating a
704
- -- far-future UpdatedAt wedged the peer's own self-updates permanently. No writer
705
- -- used it. An owner that must correct a row deletes it (retiring the stamp) and
706
- -- inserts a fresh one, the same path it already takes for CadrePeer.
707
- new.PeerId = old.PeerId
708
- and new.StampId = old.StampId
709
- and new.UpdatedAt > coalesce(old.UpdatedAt, 0)
710
- and exists (select 1 from CadrePeer P where P.PeerId = new.PeerId and verify(
711
- digest('CadreControl.DeviceToken', 'publish', new.PeerId, new.Platform, new.Token, cast(new.UpdatedAt as text)),
712
- new.Sig, P.PublicKey, 'ed25519'))
713
- )
714
- ) with context (OwnerKey text null, Signature text);
715
-
716
- -- An open invitation to form a strand with this party
717
- table FormationInvite (
718
- Token text primary key, -- Just a random string
719
- sAppId text, -- The app for the strand that will be formed
720
- ExpiresAt datetime null,
721
- TotalUses int null check (TotalUses >= 0),
722
- ValidationUrl text null, -- Web hook - send disclosure, IP address...
723
- StrandId text null, -- Host strand this invite binds to (provision-then-record). Non-null => the responder records consent against this pre-existing strand; null => unbound invite: the responder provisions a fresh open strand and atomically records its one consent row.
724
- StampId text not null unique, -- single-use authorization nonce (anti-replay)
725
- constraint AuthorizedInsert check on insert (
726
- -- Authorized by an owner signing over THIS row
727
- -- (Token, sAppId, ExpiresAt, TotalUses, ValidationUrl, StrandId, StampId); single-use via unique StampId.
728
- -- Nullable bound fields (ExpiresAt/TotalUses/ValidationUrl/StrandId) sign as '' when absent.
729
- -- The 'remove'-tagged AuthorizedDelete digest below is DISTINCT, so a captured
730
- -- insert approval can never be replayed to revoke-then-reissue an invite (or vice versa).
731
- exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and verify(
732
- digest(
733
- 'CadreControl.FormationInvite', 'add',
734
- new.Token,
735
- new.sAppId,
736
- coalesce(cast(new.ExpiresAt as text), ''),
737
- coalesce(cast(new.TotalUses as text), ''),
738
- coalesce(new.ValidationUrl, ''),
739
- coalesce(new.StrandId, ''),
740
- new.StampId
741
- ),
742
- context.Signature, A.Key, 'ed25519'))
743
- ),
744
- constraint AuthorizedDelete check on delete (
745
- exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and verify(
746
- digest(
747
- 'CadreControl.FormationInvite', 'remove',
748
- old.Token,
749
- old.sAppId,
750
- coalesce(cast(old.ExpiresAt as text), ''),
751
- coalesce(cast(old.TotalUses as text), ''),
752
- coalesce(old.ValidationUrl, ''),
753
- coalesce(old.StrandId, ''),
754
- old.StampId
755
- ),
756
- context.Signature, A.Key, 'ed25519'))
757
- ),
758
- -- Invites are insert/delete only: forbid in-place mutation so the row-bound
759
- -- AuthorizedInsert signature cannot be sidestepped by updating consent
760
- -- parameters (TotalUses/ExpiresAt/...) after a legitimate insert. A bare check
761
- -- defaults to insert+update; the checks on insert / on delete above exclude update,
762
- -- so this guard is required. Mirrors FormationUsage.InsertOnly.
763
- constraint Immutable check on update (false)
764
- ) with context (OwnerKey text, Signature text);
765
-
766
- table FormationUsage (
767
- Token text not null, -- the FormationInvite this acceptance redeems. Stated explicitly because
768
- -- Token is no longer a primary-key prefix; Authorized below would refuse a
769
- -- null anyway (null = null is never true, so its FormationInvite
770
- -- exists-clause cannot match), but the column carries the invariant rather
771
- -- than leaving it to a CHECK.
772
- UsageStampId text not null, -- single-use nonce for THIS redemption, and this table's PRIMARY KEY.
773
- -- The JOINING peer mints it and sends it in its contact message, having
774
- -- signed it into its own 'consent' digest (PeerConsented below); the
775
- -- responder inserts that same value and asks the approver about it, so it
776
- -- is bound into the 'vouch' digest below too and one sign-off is spendable
777
- -- exactly once. Neither side can swap it: a substituted nonce fails the
778
- -- joiner's consent signature.
779
- -- As the row key, a verbatim replay of an approval collides on the
780
- -- primary key locally instead of on a column \`unique\`. Unlike every other
781
- -- StampId in this schema, this table is append-only (InsertOnly below),
782
- -- so the nonce is never freed by a delete: the key alone makes it
783
- -- permanent and there is deliberately NO NotRevoked / RevocationRecorded
784
- -- pair here — do not go looking for the missing one.
785
- -- NOTE: the key is evaluated against LOCALLY VISIBLE rows, so two nodes
786
- -- that have not yet converged could each admit the same nonce and both
787
- -- rows survive the merge. Same class as the NotRevoked convergence notes
788
- -- on OwnerKey / Strand / ValidationKey; the digest binding still holds on
789
- -- both nodes, so the outcome is a duplicated audit row, never a join
790
- -- nobody approved.
791
- PeerKey text not null, -- the joining peer's own ed25519 public key (base64url, 32 bytes), bound
792
- -- into BOTH the approver's 'vouch' digest and the joiner's 'consent'
793
- -- digest below, so an approval cannot be re-filed under a different
794
- -- joiner's name and the named joiner provably agreed (PeerConsented).
795
- -- The key IS the joiner's identity — its libp2p peer id is derivable
796
- -- (identity multihash of these bytes), never stored here.
797
- PeerSig text not null, -- the joining peer's signature over the 'consent' digest — see
798
- -- PeerConsented below for what it proves and why it is stored on the row.
799
- Disclosure text,
800
- StrandId text,
801
- StrandStampId text not null, -- the Strand.StampId this consent record authorizes. Consent is
802
- -- bound to ONE strand ROW, not to a strand id forever — see
803
- -- Strand.AuthorizedInsert's consent branch for why. NOT unique:
804
- -- in the bound (provision-then-record) path several invitees
805
- -- each write a usage row against the SAME host strand, so many
806
- -- usage rows legitimately share one stamp.
807
- primary key (UsageStampId),
808
- constraint InsertOnly check on update, delete (false),
809
- constraint Authorized check on insert (
810
- -- Satisfies an invitation
811
- exists (
812
- select 1 from FormationInvite FI
813
- where FI.Token = new.Token
814
- -- The use cap is COUNTED against the pre-transaction snapshot, never
815
- -- sequenced. This CHECK auto-defers to commit (it has a subquery), by
816
- -- which point the in-flight row is live; committed.* is what excludes
817
- -- it, so a sequentially exhausted invite — every seat already committed
818
- -- on this node — is still refused here. What is deliberately NOT
819
- -- refused: concurrent redeemers on separate nodes (or nodes that have
820
- -- not yet converged) each count the same committed snapshot and both
821
- -- land, over-admitting by at most the number of concurrent redeemers.
822
- -- Both rows survive in this append-only record, so the overage is
823
- -- auditable, and removal is owner-gated (member removal + Revocation).
824
- -- Same class as the convergence note on UsageStampId above.
825
- -- One rule for FUTURE writers: two FormationUsage rows for one token
826
- -- inserted in ONE transaction each read the same committed count, so
827
- -- they can jointly exceed the cap. No writer in this repo does that; it
828
- -- is a constraint on new writers, not a case to engineer for.
829
- -- Do not "fix" this into a per-token sequence number to restore a
830
- -- strict cap — that trade was weighed and rejected (2026-08-04): a
831
- -- fought-over sequence key turns a concurrent redemption into a silent,
832
- -- unrecoverable loss of a consented join, which is strictly worse than
833
- -- a visible, reversible over-admission.
834
- and (FI.TotalUses is null or FI.TotalUses > (select count(1) from committed.FormationUsage U where U.Token = new.Token))
835
- and (FI.ExpiresAt is null or FI.ExpiresAt > context.Now)
836
- -- An invite carrying a ValidationUrl demands a sign-off from a key the party
837
- -- ENROLLED in ValidationKey. The signature is verified against the STORED VK.Key,
838
- -- NOT against context.ValidationKey — that distinction is the whole point:
839
- -- context.ValidationKey is a plain insert parameter, so verifying against it asked
840
- -- only "did the redeemer sign its own disclosure with SOME key it holds?", which any
841
- -- redeemer answers by minting a throwaway keypair. context.ValidationKey survives
842
- -- purely to SELECT which enrolled row is being claimed; naming an enrolled key while
843
- -- signing with a different one fails the verify against VK.Key.
844
- --
845
- -- Deliberate: CHECKs run on write only, so removing a validation key later does NOT
846
- -- re-examine the FormationUsage rows it already approved. The sign-off was valid when
847
- -- given and the strand it authorized stays formed; removal narrows who may approve
848
- -- FUTURE redemptions, it is not a retroactive revocation of past ones.
849
- --
850
- -- What ONE sign-off buys: exactly ONE row. The digest covers
851
- -- (Token, UsageStampId, StrandId, PeerKey, Disclosure), so an approval is NOT
852
- -- transferable across invitations, redemptions, networks, joiners, or disclosure
853
- -- text. Replay is closed twice over, by two independent mechanisms: presenting an
854
- -- approval VERBATIM repeats its UsageStampId and is refused by this table's
855
- -- primary key; presenting it under any OTHER redemption changes a signed field
856
- -- and fails this verify.
857
- -- Every signed field is supplied by the redeeming side — nothing derived from
858
- -- the table's current state is bound — so no concurrent write can invalidate an
859
- -- approval between the sign-off and the row landing.
860
- -- Why StrandStampId is deliberately NOT bound: it names the strand ROW
861
- -- incarnation, which the unbound path (redeemInvitation) mints inside its own
862
- -- transaction, and it adds nothing on top of a strictly single-use approval —
863
- -- StrandId already pins WHICH network the approver approved joining.
864
- and (FI.ValidationUrl is null or exists (
865
- select 1 from ValidationKey VK
866
- where VK.Key = context.ValidationKey
867
- and verify(digest('CadreControl.FormationUsage', 'vouch', new.Token, new.UsageStampId, new.StrandId, new.PeerKey, new.Disclosure), context.ValidationSignature, VK.Key, 'ed25519')))
868
- -- A bound invite (non-null StrandId) may only ever name its own host strand:
869
- -- without this, any holder of a bound token could record consent against an
870
- -- arbitrary strand id — annotating an unrelated strand, or (before
871
- -- Strand.AuthorizedInsert's consent branch was narrowed to unbound invites)
872
- -- consent-seating that id outright.
873
- -- NOTE: an UNBOUND invite is deliberately left free to name any strand, so a
874
- -- holder can spend a use appending a consent row to an unrelated existing
875
- -- strand. Harmless today (the row grants nothing, it only burns a use and
876
- -- forecloses that id's consent-seating, which is the safe direction); if
877
- -- unlimited-TotalUses invites ever become common, this is an append-only
878
- -- growth surface worth bounding.
879
- and (FI.StrandId is null or FI.StrandId = new.StrandId)
880
- )
881
- ),
882
- -- Matched on the (id, stamp) PAIR, the same key Strand.AuthorizedInsert's consent branch
883
- -- reads back: a usage row may only ever name a strand ROW that exists, so a consent record
884
- -- can never be filed against a stamp of the writer's choosing and held in reserve to
885
- -- re-seat the id after a removal.
886
- constraint StrandExists check (exists (select 1 from Strand S where S.Id = new.StrandId and S.StampId = new.StrandStampId)),
887
- -- The JOINING peer proves it consented to this redemption: PeerKey is its own ed25519
888
- -- public key and PeerSig is its signature over the 'consent'-tagged digest below.
889
- -- Unlike the approver's sign-off (context.ValidationSignature, checked against a STORED
890
- -- ValidationKey row), the identity here IS the key, so there is no enrolled row to look
891
- -- up and nothing for a writer to substitute: a forged joiner would need that joiner's
892
- -- private key. Stored rather than passed in context so any later reader can re-check it
893
- -- (verifyFormationConsent); the 'consent' action tag keeps this replicated signature
894
- -- useless against every other rule in this schema (notably the approver's 'vouch'
895
- -- digest over the same table).
896
- -- NOTE: StrandId is deliberately NOT in this digest — the joiner cannot know it when it
897
- -- signs (a bound invite's host strand arrives only in the result frame; an unbound
898
- -- strand is responder-minted). The responder cannot substitute a strand anyway: bound
899
- -- invites are pinned by Authorized's FI.StrandId check, unbound invites mint fresh.
900
- constraint PeerConsented check on insert (
901
- verify(digest('CadreControl.FormationUsage', 'consent',
902
- new.Token, new.UsageStampId, new.PeerKey, new.Disclosure),
903
- new.PeerSig, new.PeerKey, 'ed25519')
904
- ),
905
- -- Context here is only what the approver-side rules read: Now for expiry, and the
906
- -- ValidationKey/ValidationSignature pair for the 'vouch' check in Authorized. The joiner's
907
- -- signature is NOT context — it lives on the row (PeerSig, verified by PeerConsented above).
908
- ) with context (Now datetime, ValidationKey text null, ValidationSignature text null);
909
-
910
- -- Token is no longer a primary-key prefix (the key is the redemption nonce), so the
911
- -- per-token filters — the cap count in Authorized above, and cadre-core's
912
- -- countFormationUsage / isTokenUsed / hasOutstandingFormationInvite — are served by this
913
- -- index rather than by a full scan of a table that is append-only and grows for the life
914
- -- of the party.
915
- --
916
- -- The cap count is only ever as correct as this index's convergence across machines. That
917
- -- convergence failed once: from 2026-08-04 to 2026-08-25 a descent on a second machine
918
- -- returned only the rows THAT machine wrote, so the index was removed and the reads went
919
- -- back to the scan. The engine defect was fixed upstream and re-measured here on
920
- -- 2026-09-17, so the index is declared again. Its guard is the integration-tests scenario
921
- -- \`strand-formation-concurrent-redemption\`, which asserts BOTH machines' views of a raced
922
- -- redemption and therefore goes red if that convergence regresses.
923
- --
924
- -- NOTE: the write side has its own exposure. Every FormationUsage insert now maintains this
925
- -- sub-collection as well as the table, and an intermittent engine failure once hit exactly that
926
- -- step on other tables (a table's rows committed while its index did not; closed 2026-09-17 with
927
- -- the upstream sync fixes, \`tickets/complete/strand-unique-index-sync-stale-revision\`). Nothing
928
- -- has been observed here, and redemptions are rare writes; but if joins start failing
929
- -- intermittently with an index-sync error naming FormationUsage, this declaration is the first
930
- -- thing to suspect.
931
- index FormationUsageByToken on FormationUsage (Token);
932
-
933
- -- Append-only retirement record for the one-off StampId nonces of removed rows of every
934
- -- guarded table (listed on TableName below). Without it a removal was undoable: the add
935
- -- approval is a signature over
936
- -- (row key, stamp) that never expires, and deleting the row freed the stamp, so anyone who
937
- -- kept a copy of the approval could resurrect the row.
938
- -- Each tombstone also records WHICH row was retired (RowKey below): the consent branch of
939
- -- Strand.AuthorizedInsert reads it to refuse consent-seating any strand id that has ever
940
- -- been removed, whichever way it was seated.
941
- -- NOTE: the guarded tables' NotRevoked CHECK runs against locally visible rows, so a node
942
- -- that has not yet converged on a Revocation row can still accept the replayed add, and the
943
- -- resurrected row coexists with the tombstone after merge — same class as the MinOneOwner
944
- -- note on OwnerKey. The read-side mitigation is ControlDatabase.queryCadrePeers, which
945
- -- drops any CadrePeer row whose StampId appears here; every membership reader inherits it.
946
- -- NOTE: append-only, so this table only ever grows. Every append is owner-signed
947
- -- (Authorized below), so the growth surface is the owner keys, not every writer that can
948
- -- reach the control database. Cadres are a handful of peers and owner rotation is rare, so
949
- -- unbounded growth is fine today; revisit if either changes.
950
- -- A tombstone committed while the node was alone is local-only (never broadcast); the
951
- -- ReissuedAt counter below exists so an owner can re-write — and therefore re-broadcast —
952
- -- such a tombstone once peers are reachable, without changing what it means.
953
- -- One row retires nothing: the singleton ledger marker ('Revocation', 'ledger', 'opened'),
954
- -- filed once by an owner's connected reconcile pass (ControlDatabase.openRevocationLedger).
955
- -- It exists only so this table is never a never-written block. The storage layer consults
956
- -- a block's cohort on EVERY read of a block it does not hold, and every membership lookup
957
- -- and every guarded insert (NotRevoked) reads this table; once any row exists the block is
958
- -- held and re-checked on the storage layer's normal read-repair schedule, like every other
959
- -- populated table. It cannot read as a retirement: every reader of this table filters on
960
- -- its own guarded TableName, 'Revocation' is not one, and ControlDatabase.queryRevocations
961
- -- skips the marker, so nothing reaps or re-issues it.
962
- table Revocation (
963
- TableName text, -- 'OwnerKey' | 'CadrePeer' | 'ValidationKey' | 'Strand' | 'StrandPartyKey' |
964
- -- 'JoinedStrand' | 'PendingJoin' | 'DeviceToken', or 'Revocation' for the
965
- -- ledger marker only (all confined by RowIsGone below)
966
- RowKey text not null, -- primary key of the removed row: OwnerKey.Key / ValidationKey.Key /
967
- -- CadrePeer.PeerId / DeviceToken.PeerId / Strand.Id / StrandPartyKey.Id /
968
- -- JoinedStrand.Id / PendingJoin.Id
969
- -- Every guarded table's RevocationRecorded CHECK requires the
970
- -- accompanying tombstone to carry the removed row's key here, so
971
- -- a REMOVAL cannot file a misnamed tombstone. A tombstone with no
972
- -- accompanying delete is not covered by that (see Authorized).
973
- StampId text, -- the retired nonce
974
- -- Bumped by an owner-signed re-issue so a tombstone written while the node was alone
975
- -- (committed local-only, never broadcast) can be re-written and therefore re-broadcast
976
- -- on cohort growth. Carries NO semantics: nothing reads it, and retirement is decided
977
- -- by the row's existence, not by this value.
978
- ReissuedAt integer not null default 0,
979
- -- Keyed on the stamp, not the RowKey: stamps are unique per row incarnation, and one
980
- -- name may legitimately carry several tombstones over its life (seat -> delete ->
981
- -- owner re-seat -> delete).
982
- primary key (TableName, StampId),
983
- -- Retirement is permanent: a tombstone may never be withdrawn.
984
- constraint NoDelete check on delete (false),
985
- -- Pinned at 0 on insert so an owner cannot seat a tombstone at a saturated counter and
986
- -- thereby freeze its own later re-issues (ReissueOnly below). Deliberately NOT folded
987
- -- into the Authorized digest: the value is fixed by this rule, so signing over it would
988
- -- only widen the signed surface for no gain, and would invalidate every existing
989
- -- insert-side digest.
990
- constraint FreshTombstone check on insert (new.ReissuedAt = 0),
991
- -- A re-issue may move NOTHING but the counter, and only upward. Without the identity
992
- -- clause an "update" would be a way to re-point a tombstone at a different row,
993
- -- restoring exactly the replay this table exists to stop.
994
- constraint ReissueOnly check on update (
995
- new.TableName = old.TableName and new.RowKey = old.RowKey
996
- and new.StampId = old.StampId and new.ReissuedAt > old.ReissuedAt
997
- ),
998
- -- A stamp may only be retired once its row is actually gone. Deferred (subquery), so a
999
- -- delete in the same transaction has already landed. Retiring a stamp that never
1000
- -- existed is permitted and harmless: a stamp carries 128 bits of CSPRNG output
1001
- -- (control-database.ts:generateStampId — 16 bytes of sha256(PeerId) followed by 16
1002
- -- random bytes), so a future legitimate one cannot be guessed and pre-planted.
1003
- -- Deliberately keyed on the STAMP only, not RowKey: checking "no live row named
1004
- -- new.RowKey" breaks under out-of-order convergence — a peer removed (tombstone names
1005
- -- it, stamp1) then re-admitted (stamp2) would leave a node that converges on the
1006
- -- re-add FIRST rejecting the stamp1 tombstone forever, never learning that stamp is
1007
- -- retired. The stamp-only form passes there (stamp1 is not live).
1008
- -- The last branch admits the ledger marker (see the table comment) and nothing else under
1009
- -- TableName 'Revocation': the whole triple is pinned, so with the primary key
1010
- -- (TableName, StampId) it is a singleton and the append-only growth surface is unchanged.
1011
- -- 'opened' cannot collide with a real stamp (43 base64url characters, generateStampId),
1012
- -- and the primary key includes TableName anyway.
1013
- constraint RowIsGone check on insert (
1014
- (new.TableName = 'OwnerKey' and not exists (select 1 from OwnerKey K where K.StampId = new.StampId))
1015
- or (new.TableName = 'CadrePeer' and not exists (select 1 from CadrePeer P where P.StampId = new.StampId))
1016
- or (new.TableName = 'ValidationKey' and not exists (select 1 from ValidationKey V where V.StampId = new.StampId))
1017
- or (new.TableName = 'Strand' and not exists (select 1 from Strand S where S.StampId = new.StampId))
1018
- or (new.TableName = 'StrandPartyKey' and not exists (select 1 from StrandPartyKey K where K.StampId = new.StampId))
1019
- or (new.TableName = 'JoinedStrand' and not exists (select 1 from JoinedStrand J where J.StampId = new.StampId))
1020
- or (new.TableName = 'PendingJoin' and not exists (select 1 from PendingJoin P where P.StampId = new.StampId))
1021
- or (new.TableName = 'DeviceToken' and not exists (select 1 from DeviceToken D where D.StampId = new.StampId))
1022
- or (new.TableName = 'Revocation' and new.RowKey = 'ledger' and new.StampId = 'opened')
1023
- ),
1024
- -- Appending a tombstone is an OWNER action, like every other write in this schema.
1025
- -- Ungated, any writer that could reach the control database could retire a stamp:
1026
- -- filing one against a CadrePeer row not visible locally (RowIsGone reads LOCALLY
1027
- -- VISIBLE rows — see the convergence note above) evicted that peer party-wide AND
1028
- -- permanently blocked the owner's own later re-admission of it (NotRevoked), and
1029
- -- filing arbitrary stamps flooded a table whose rows never go away.
1030
- -- The digest binds the WHOLE row (TableName, RowKey, StampId); the 'CadreControl.Revocation'
1031
- -- domain tag keeps it disjoint from every other rule — including the
1032
- -- 'CadreControl.CadrePeer' 'remove' digest the same owner signs in the SAME
1033
- -- transaction for the delete this tombstone accompanies.
1034
- -- RowKey is not cross-checked HERE against the retired row: positively binding it (e.g.
1035
- -- requiring a committed.Strand row carrying the pair) would reject a tombstone re-issued
1036
- -- in a LATER transaction — which the delete-while-alone durability path
1037
- -- (docs/architecture.md) plans to do — and reject it outright on a node that never
1038
- -- converged on the removed row. The binding lives on the other side instead: each
1039
- -- guarded table's RevocationRecorded requires the tombstone accompanying a DELETE to
1040
- -- name that row's key, which the re-issue path never trips (it files no delete).
1041
- -- Residual: a STANDALONE tombstone — owner-signed, naming a stamp no live row holds —
1042
- -- is still accepted, so an owner can name an id that never existed and thereby
1043
- -- permanently foreclose CONSENT-seating of that id (Strand.AuthorizedInsert).
1044
- -- Owner-only, the owner already controls invite issuance, and unbound ids are 128
1045
- -- random bytes minted at redemption — no escalation (same reasoning as the pre-plant
1046
- -- note on RowIsGone).
1047
- -- Reads LIVE OwnerKey (like ValidationKey / Strand / CadrePeer / DeviceToken), not
1048
- -- committed.OwnerKey. The one difference that makes: a founding transaction on an
1049
- -- owner-less database could seat an owner key and file a tombstone in one go. An
1050
- -- owner-less control database is already a total loss, and a second transaction
1051
- -- achieves the same thing under committed.*, so consistency with the siblings is
1052
- -- worth more than the distinction — do not "tighten" this without a reason other
1053
- -- than that one.
1054
- -- RowIsGone and NoDelete are unchanged: they guard an owner retiring a stamp early
1055
- -- or withdrawing one later, which authorization does not cover.
1056
- -- The ledger marker is signed under this same rule, over
1057
- -- digest('CadreControl.Revocation', 'remove', 'Revocation', 'ledger', 'opened'). The
1058
- -- 'remove' tag on a row that retires nothing is deliberate: reusing the one append rule
1059
- -- keeps every append owner-signed without adding an unsigned or differently tagged
1060
- -- branch, and a captured marker signature can only re-file the identical row, whose
1061
- -- primary key is already taken.
1062
- constraint Authorized check on insert (
1063
- exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and verify(digest('CadreControl.Revocation', 'remove', new.TableName, new.RowKey, new.StampId), context.Signature, A.Key, 'ed25519'))
1064
- ),
1065
- -- A re-issue is an OWNER action, like the original append. Distinct 'reissue' action
1066
- -- tag, so an append approval can never be replayed as a re-issue and vice versa. The
1067
- -- digest adds ReissuedAt so a captured re-issue signature cannot bump the counter to
1068
- -- any other value. Reads LIVE OwnerKey, matching Authorized above.
1069
- constraint AuthorizedReissue check on update (
1070
- exists (select 1 from OwnerKey A where A.Key = context.OwnerKey
1071
- and verify(digest('CadreControl.Revocation', 'reissue',
1072
- new.TableName, new.RowKey, new.StampId, cast(new.ReissuedAt as text)),
1073
- context.Signature, A.Key, 'ed25519'))
1074
- )
1075
- ) with context (OwnerKey text, Signature text);
1076
- }
1077
-
1078
- apply schema CadreControl;`;
12
+ export const CONTROL_SCHEMA = `-- A Sereus party's cadre (its nodes) and their participation in strands (networks).
13
+ --
14
+ -- Every write is authorized by a check: an owner signs an action-tagged digest of the row
15
+ -- (cadre-core control-authorization.ts), or a peer signs its own record. Each digest leads
16
+ -- with a 'CadreControl.<Table>' domain tag, an action tag and party_id(), the party this
17
+ -- machine is configured for (a zero-argument function ControlDatabase registers on its own
18
+ -- engine, never read from a context value or a replicated table), so an approval verifies
19
+ -- only against the one rule, in the one party, it was minted for: two parties that share
20
+ -- an owner key never accept each other's approvals. FormationUsage is the exception, its
21
+ -- two digests scoped by the invitation token instead (FormationUsage.Authorized).
22
+ --
23
+ -- 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.
31
+ --
32
+ -- A check whose expression evaluates to null passes (SQL three-valued logic), so every
33
+ -- comparison over a nullable column that is meant to pin a value is written null-safe
34
+ -- (\`coalesce(col, '')\` or an explicit \`is null\`); a bare \`new.Col = context.X\` over a null
35
+ -- column would pass with nothing pinned.
36
+ --
37
+ -- A check with a subquery defers to commit, so a sibling row written in the same transaction
38
+ -- is visible to it; \`committed.<Table>\` reads the pre-transaction snapshot instead. Every
39
+ -- check reads the rows this node holds: a node that has not converged on a Revocation row
40
+ -- still accepts the replayed add, and the resurrected row then coexists with its tombstone.
41
+ -- ControlDatabase drops retired stamps on the reads that matter (queryCadrePeers,
42
+ -- queryPendingJoins, getOwnerKeys), and every owner lookup in the checks below drops them
43
+ -- too (the \`not exists ... Revocation\` conjunct beside each OwnerKey read, reading live
44
+ -- Revocation so a tombstone filed in the same transaction counts), so a removal is
45
+ -- effective at the writer as soon as its tombstone arrives, whether or not the row's delete
46
+ -- has. Only the founding count in OwnerKey.Authorized stays raw.
47
+ declare schema CadreControl {
48
+ -- A key that can authorize control changes. A row is seated one of three ways, told apart
49
+ -- 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.
54
+ table OwnerKey (
55
+ Key text primary key,
56
+ StampId text unique,
57
+ VouchOwner text null,
58
+ VouchSig text null,
59
+ VouchUsage text null,
60
+ constraint NotRevoked check on insert (
61
+ not exists (select 1 from Revocation R where R.TableName = 'OwnerKey' and R.StampId = new.StampId)
62
+ ),
63
+ -- The tombstone must name the removed row, not just its stamp (Revocation.RowKey).
64
+ constraint RevocationRecorded check on delete (
65
+ exists (select 1 from Revocation R where R.TableName = 'OwnerKey' and R.StampId = old.StampId and R.RowKey = old.Key)
66
+ ),
67
+ -- Every other table's checks require an owner, so an empty table is a permanent denial of
68
+ -- the party's control plane. Counted post-delete, on this node's rows only: two partitioned
69
+ -- nodes removing different owners can still converge to zero.
70
+ constraint MinOneOwner check on delete (
71
+ (select count(1) from OwnerKey) >= 1
72
+ ),
73
+ -- Rotation is add-then-remove. An update branch would let the sole owner be re-pointed at
74
+ -- another key under a post-image count of one.
75
+ constraint NoUpdate check on update (false),
76
+ -- The authorizer is read from the pre-transaction snapshot: the check defers to commit, by
77
+ -- which point the inserted row (and any sibling) is live, so a plain OwnerKey read would let
78
+ -- a row authorize itself or two strangers seat each other. The founding transaction, whose
79
+ -- pre-transaction owner set is empty, needs no authorization; gating on the pre-transaction
80
+ -- count keeps a same-transaction swap of the sole owner off that branch. That count is
81
+ -- deliberately raw, the one owner read here that ignores Revocation: a machine holding only
82
+ -- a retired owner row must still be refused a second genesis, or a removed device could
83
+ -- 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.
89
+ constraint Authorized check on insert, delete (
90
+ (old.Key is null and (select count(1) from committed.OwnerKey) = 0
91
+ 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'))
93
+ and coalesce(new.VouchOwner, '') = context.OwnerKey
94
+ and coalesce(new.VouchSig, '') = context.Signature
95
+ and new.VouchUsage is null)
96
+ or (old.Key is null and new.VouchSig is null and new.VouchUsage is not null
97
+ and exists (
98
+ select 1 from CadreInviteUsage U
99
+ 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)
103
+ ))
104
+ 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
+ )
106
+ ) with context (OwnerKey text null, Signature text null);
107
+
108
+ -- A key that can validate a strand formation disclosure (FormationUsage.Authorized).
109
+ table ValidationKey (
110
+ Key text primary key,
111
+ StampId text unique,
112
+ constraint NotRevoked check on insert (
113
+ not exists (select 1 from Revocation R where R.TableName = 'ValidationKey' and R.StampId = new.StampId)
114
+ ),
115
+ constraint RevocationRecorded check on delete (
116
+ exists (select 1 from Revocation R where R.TableName = 'ValidationKey' and R.StampId = old.StampId and R.RowKey = old.Key)
117
+ ),
118
+ constraint NoUpdate check on update (false),
119
+ constraint AuthorizedInsert check on insert (
120
+ 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.ValidationKey', 'add', party_id(), new.Key, new.StampId), context.Signature, A.Key, 'ed25519'))
121
+ ),
122
+ -- Owner-signed over the stored row, or a reap: a committed tombstone naming this exact row
123
+ -- incarnation lets a node that was offline at removal time delete the stale row unsigned
124
+ -- (ControlDatabase.reapRevokedRow). committed.*, so a tombstone filed in this transaction
125
+ -- cannot stand in for the signature; the stamp clause, so a tombstone of an earlier
126
+ -- incarnation cannot remove the current one.
127
+ constraint AuthorizedDelete check on delete (
128
+ 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.ValidationKey', 'remove', party_id(), old.Key, old.StampId), context.Signature, A.Key, 'ed25519'))
129
+ or exists (select 1 from committed.Revocation R where R.TableName = 'ValidationKey' and R.RowKey = old.Key and R.StampId = old.StampId)
130
+ )
131
+ ) with context (OwnerKey text, Signature text);
132
+
133
+ -- A network of members sharing an sApp database, each contributing its cadre's peers. This
134
+ -- party's own strands; a strand joined from another party is a JoinedStrand row.
135
+ table Strand (
136
+ Id text primary key, -- UUID
137
+ MemberPrivateKey text null unique, -- the strand's shared read secret; closed strands only
138
+ 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).
142
+ StampId text unique,
143
+ constraint NotRevoked check on insert (
144
+ not exists (select 1 from Revocation R where R.TableName = 'Strand' and R.StampId = new.StampId)
145
+ ),
146
+ constraint RevocationRecorded check on delete (
147
+ exists (select 1 from Revocation R where R.TableName = 'Strand' and R.StampId = old.StampId and R.RowKey = old.Id)
148
+ ),
149
+ -- The consent branch below carries no signature, so an update rule would let anyone rewrite
150
+ -- a consent-seated strand.
151
+ 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:
155
+ -- the usage row outlives the strand, and keyed on the id alone it would re-authorize an
156
+ -- unsigned re-seat under a fresh stamp. The consent branch is narrow, each clause closing
157
+ -- one door:
158
+ -- * open and keyless: a closed strand with a caller-chosen read secret has no legitimate
159
+ -- producer;
160
+ -- * through an unbound invite only: a bound invite's host strand is owner-provisioned,
161
+ -- and consent-seating its id would downgrade the real row on a node it has not reached;
162
+ -- * no usage row under another stamp, and no tombstone for the id: an id is consent-seated
163
+ -- once, ever, and never after any removal. Re-joining a removed id is owner-signed.
164
+ -- A standalone tombstone (no accompanying delete, Revocation.Authorized) lets an owner
165
+ -- foreclose consent-seating of an id that never existed: owner-only, and ids are random.
166
+ 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)
169
+ or (
170
+ new.Type = 'o'
171
+ and new.MemberPrivateKey is null
172
+ and new.FounderOwnerKey is null
173
+ and exists (
174
+ select 1 from FormationUsage FU
175
+ where FU.StrandId = new.Id
176
+ and FU.StrandStampId = new.StampId
177
+ and exists (select 1 from FormationInvite FI where FI.Token = FU.Token and FI.StrandId is null)
178
+ )
179
+ and not exists (
180
+ select 1 from FormationUsage FU2
181
+ where FU2.StrandId = new.Id and FU2.StrandStampId <> new.StampId
182
+ )
183
+ and not exists (
184
+ select 1 from Revocation R
185
+ where R.TableName = 'Strand' and R.RowKey = new.Id
186
+ )
187
+ )
188
+ ),
189
+ -- No consent branch (an invitation forms a strand, never destroys one) and no reap branch:
190
+ -- MemberPrivateKey is stored nowhere else, so this delete is unrecoverable
191
+ -- (tickets/backlog/debt-strand-tombstone-reap.md owns any change).
192
+ constraint AuthorizedDelete check on delete (
193
+ 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', 'remove', party_id(), old.Id, old.StampId), context.Signature, A.Key, 'ed25519'))
194
+ ),
195
+ -- An open strand has no membership gate, so no member key.
196
+ constraint MemberKeyClosedOnly check (
197
+ new.MemberPrivateKey is null or new.Type = 'c'
198
+ )
199
+ ) with context (OwnerKey text, Signature text);
200
+
201
+ -- This party's own membership identity for one strand: the private key whose public half
202
+ -- seats its Strand.Member / Strand.Manager rows. Separate from Strand.MemberPrivateKey, which
203
+ -- every joining party receives and so could forge (gotchoices/sereus#4). A side table because
204
+ -- a joiner holds no Strand row. Replicated in plaintext to every machine the party owns
205
+ -- (docs/strands.md → "Closed-Strand Member Key Handling") but never put on the formation wire.
206
+ table StrandPartyKey (
207
+ Id text primary key, -- the strand id
208
+ PrivateKey text, -- ed25519, base64 protobuf, as Strand.MemberPrivateKey (cadre-core strandMemberKeyPair)
209
+ StampId text unique,
210
+ constraint NotRevoked check on insert (
211
+ not exists (select 1 from Revocation R where R.TableName = 'StrandPartyKey' and R.StampId = new.StampId)
212
+ ),
213
+ constraint RevocationRecorded check on delete (
214
+ exists (select 1 from Revocation R where R.TableName = 'StrandPartyKey' and R.StampId = old.StampId and R.RowKey = old.Id)
215
+ ),
216
+ constraint NoUpdate check on update (false),
217
+ -- Binding PrivateKey means a captured approval can only reproduce the key it approved.
218
+ constraint AuthorizedInsert check on insert (
219
+ 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.StrandPartyKey', 'add', party_id(), new.Id, new.PrivateKey, new.StampId), context.Signature, A.Key, 'ed25519'))
220
+ ),
221
+ -- No reap branch: the key is stored nowhere else (as Strand.AuthorizedDelete).
222
+ constraint AuthorizedDelete check on delete (
223
+ 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.StrandPartyKey', 'remove', party_id(), old.Id, old.StampId), context.Signature, A.Key, 'ed25519'))
224
+ )
225
+ ) with context (OwnerKey text, Signature text);
226
+
227
+ -- A strand this party joined from another party, whose control database holds the Strand
228
+ -- row. Every machine of the party reads it, so a replica host keeps a copy and the party's
229
+ -- other machines can attach. A separate table because every reader of Strand takes it to
230
+ -- mean this party's own strand. MemberPrivateKey is held by the founding party and every
231
+ -- member, so a removed row is recoverable by re-forming, which is why this table has a reap
232
+ -- branch (unlike Strand and StrandPartyKey).
233
+ table JoinedStrand (
234
+ Id text primary key, -- the founder's Strand.Id
235
+ Type text, -- 'o' | 'c', as Strand.Type
236
+ MemberPrivateKey text null, -- the closed strand's shared read secret
237
+ StampId text unique,
238
+ constraint NotRevoked check on insert (
239
+ not exists (select 1 from Revocation R where R.TableName = 'JoinedStrand' and R.StampId = new.StampId)
240
+ ),
241
+ constraint RevocationRecorded check on delete (
242
+ exists (select 1 from Revocation R where R.TableName = 'JoinedStrand' and R.StampId = old.StampId and R.RowKey = old.Id)
243
+ ),
244
+ constraint NoUpdate check on update (false),
245
+ -- Readers cast Type (ControlDatabase.queryJoinedStrands).
246
+ constraint KnownType check (new.Type = 'o' or new.Type = 'c'),
247
+ constraint MemberKeyClosedOnly check (new.MemberPrivateKey is null or new.Type = 'c'),
248
+ -- Owner-signed only: every always-on machine of the party downloads the strands named
249
+ -- here, so a machine that is not an owner keeps its joins machine-local.
250
+ constraint AuthorizedInsert check on insert (
251
+ 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.JoinedStrand', 'add', party_id(), new.Id, new.Type, coalesce(new.MemberPrivateKey, ''), new.StampId), context.Signature, A.Key, 'ed25519'))
252
+ ),
253
+ constraint AuthorizedDelete check on delete (
254
+ 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.JoinedStrand', 'remove', party_id(), old.Id, old.StampId), context.Signature, A.Key, 'ed25519'))
255
+ or exists (select 1 from committed.Revocation R where R.TableName = 'JoinedStrand' and R.RowKey = old.Id and R.StampId = old.StampId)
256
+ )
257
+ ) with context (OwnerKey text, Signature text);
258
+
259
+ -- A join this party asked for by redeeming another party's invitation, kept party-wide so
260
+ -- the request outlives the device that asked and any owner machine can carry it on
261
+ -- (docs/strands.md → "Joining while the inviter is offline"). The row is the request alone
262
+ -- and never changes; its outcome is a JoinSuccess or JoinFailure row keyed by this row's
263
+ -- stamp, so asking again (a fresh stamp) starts clean and a dismissal's tombstone retires
264
+ -- the outcome with the request. Invitation is a bearer credential, replicated in plaintext
265
+ -- to every machine the party owns, under the same accepted risk as JoinedStrand.
266
+ -- Owner-signed only; whether a machine that is not an owner may write here is
267
+ -- tickets/blocked/decide-non-owner-machine-completes-a-pending-join.md.
268
+ -- The three tables reference only tables declared before them, so an apply that fails
269
+ -- midway can drop them in reverse order. The two rules that would close a cycle are the
270
+ -- writer's (ControlDatabase): a request's delete takes its outcome row with it, and a join
271
+ -- recorded over a failure deletes the failure. An outcome row left behind is unreadable,
272
+ -- since readers start from the request.
273
+ table JoinRequest (
274
+ Id text primary key, -- base64url sha256 of the invitation token (cadre-core pendingJoinId),
275
+ -- so the token never reaches Revocation
276
+ Invitation text, -- CadreNode.encodeInvitation
277
+ Disclosure text, -- canonicalJson of the disclosure the requester gave
278
+ RequestedAt integer, -- epoch ms
279
+ ExpiresAt integer, -- epoch ms; no attempt starts at or after it
280
+ StampId text unique,
281
+ constraint NotRevoked check on insert (
282
+ not exists (select 1 from Revocation R where R.TableName = 'JoinRequest' and R.StampId = new.StampId)
283
+ ),
284
+ constraint RevocationRecorded check on delete (
285
+ exists (select 1 from Revocation R where R.TableName = 'JoinRequest' and R.StampId = old.StampId and R.RowKey = old.Id)
286
+ ),
287
+ constraint NoUpdate check on update (false),
288
+ constraint AuthorizedInsert check on insert (
289
+ 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(
290
+ digest('CadreControl.JoinRequest', 'add', party_id(), new.Id, new.Invitation, new.Disclosure, cast(new.RequestedAt as text), cast(new.ExpiresAt as text), new.StampId),
291
+ context.Signature, A.Key, 'ed25519'))
292
+ ),
293
+ -- Reapable: the inviting party and the user hold the invitation too.
294
+ constraint AuthorizedDelete check on delete (
295
+ 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.JoinRequest', 'remove', party_id(), old.Id, old.StampId), context.Signature, A.Key, 'ed25519'))
296
+ or exists (select 1 from committed.Revocation R where R.TableName = 'JoinRequest' and R.RowKey = old.Id and R.StampId = old.StampId)
297
+ )
298
+ ) with context (OwnerKey text null, Signature text null);
299
+
300
+ -- The outcome of a JoinRequest that joined: the strand the formation returned and, for a
301
+ -- closed strand, the single-use membership invitation it delivered. Keyed by the request's
302
+ -- stamp, so it names one request incarnation and a replayed approval can only re-seat the
303
+ -- row it approved, against the request it approved. A join is final: it may be recorded
304
+ -- over a failure (whose row the writer deletes with it), and nothing is recorded over it.
305
+ table JoinSuccess (
306
+ RequestStampId text primary key,
307
+ RecordedAt integer, -- epoch ms
308
+ StrandId text,
309
+ MembershipInvite text null, -- JSON {inviteKey, invitePrivateKey}, the StrandMembershipInvite; closed strands only
310
+ constraint RequestExists check on insert (
311
+ exists (select 1 from JoinRequest R where R.StampId = new.RequestStampId)
312
+ ),
313
+ constraint NoUpdate check on update (false),
314
+ constraint AuthorizedInsert check on insert (
315
+ 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(
316
+ digest('CadreControl.JoinSuccess', 'add', party_id(), new.RequestStampId, cast(new.RecordedAt as text), new.StrandId, coalesce(new.MembershipInvite, '')),
317
+ context.Signature, A.Key, 'ed25519'))
318
+ ),
319
+ -- Removed only with its request: the tombstone retiring the request's stamp (owner-signed,
320
+ -- or already committed for a reap) authorizes this delete.
321
+ constraint AuthorizedDelete check on delete (
322
+ exists (select 1 from Revocation R where R.TableName = 'JoinRequest' and R.StampId = old.RequestStampId)
323
+ )
324
+ ) with context (OwnerKey text null, Signature text null);
325
+
326
+ -- The outcome of a JoinRequest that failed for good. Keyed as JoinSuccess is.
327
+ table JoinFailure (
328
+ RequestStampId text primary key,
329
+ RecordedAt integer, -- epoch ms
330
+ Code text, -- a FormationRejectionCode, 'expired' or 'local'
331
+ Reason text, -- human-readable
332
+ constraint RequestExists check on insert (
333
+ exists (select 1 from JoinRequest R where R.StampId = new.RequestStampId)
334
+ ),
335
+ -- A join is final: nothing records a failure over it.
336
+ constraint NotJoined check on insert (
337
+ not exists (select 1 from JoinSuccess S where S.RequestStampId = new.RequestStampId)
338
+ ),
339
+ constraint NoUpdate check on update (false),
340
+ constraint AuthorizedInsert check on insert (
341
+ 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(
342
+ digest('CadreControl.JoinFailure', 'add', party_id(), new.RequestStampId, cast(new.RecordedAt as text), new.Code, new.Reason),
343
+ context.Signature, A.Key, 'ed25519'))
344
+ ),
345
+ -- Removed with its request (JoinSuccess.AuthorizedDelete), or superseded by a join
346
+ -- recorded in the same transaction.
347
+ constraint AuthorizedDelete check on delete (
348
+ exists (select 1 from Revocation R where R.TableName = 'JoinRequest' and R.StampId = old.RequestStampId)
349
+ or exists (select 1 from JoinSuccess S where S.RequestStampId = old.RequestStampId)
350
+ )
351
+ ) with context (OwnerKey text null, Signature text null);
352
+
353
+ -- A node of the cadre, carrying its self-published, freshness-stamped, self-signed address
354
+ -- record (cadre-core PeerAddressRecord): a resolver re-verifies Sig against PublicKey,
355
+ -- checks UpdatedAt and trust, then dials Multiaddr.
356
+ table CadrePeer (
357
+ PeerId text primary key,
358
+ PublicKey text null, -- ed25519 (base64url) behind PeerId; null if not ed25519
359
+ Multiaddr text, -- comma-joined current addrs (signaling / p2p-circuit first)
360
+ UpdatedAt int null, -- epoch ms; strictly increases per self-update
361
+ Sig text null, -- self-signature over the 'publish' digest; null until self-published
362
+ StampId text unique,
363
+ VouchOwner text null, -- the owner key that vouched this membership (== insert context.OwnerKey), or
364
+ -- the issuer of the invitation that admitted it (CadreInvite.IssuerKey)
365
+ VouchSig text null, -- that owner's 'vouch' signature (== insert context.Signature); null on an
366
+ -- invitation-admitted row. A reader checks VouchOwner against its node-local
367
+ -- trusted-owner anchor, not against this replicated table, which a self-owner
368
+ -- can pollute.
369
+ VouchUsage text null, -- the CadreInviteUsage.UsageStampId that admitted this row; null on an
370
+ -- owner-vouched row. A row carries exactly one proof: a signature or a usage.
371
+ Profile text null, -- the machine's own node profile (cadre-core NodeProfile): 'transaction' is an
372
+ -- app (phone, browser), 'storage' an always-on node; null until it self-publishes.
373
+ -- Self-declared and covered by Sig. No insert rule constrains it: an owner could
374
+ -- only mislabel a machine it already has full authority over, and that machine's
375
+ -- next self-publish corrects it.
376
+ constraint NotRevoked check on insert (
377
+ not exists (select 1 from Revocation R where R.TableName = 'CadrePeer' and R.StampId = new.StampId)
378
+ ),
379
+ constraint KnownProfile check (new.Profile is null or new.Profile in ('transaction', 'storage')),
380
+ constraint RevocationRecorded check on delete (
381
+ exists (select 1 from Revocation R where R.TableName = 'CadrePeer' and R.StampId = old.StampId and R.RowKey = old.PeerId)
382
+ ),
383
+ -- The owner vouches membership and the PublicKey<->PeerId binding (cadre-core derives
384
+ -- PublicKey from PeerId) over (PeerId, StampId); the stored voucher must be the pair
385
+ -- verified here. The same 'vouch' digest serves the owner branch of AuthorizedUpdate:
386
+ -- both mean "this owner vouches this membership row".
387
+ -- Or admitted by consent: a CadreInviteUsage row written in the same transaction names
388
+ -- this exact row incarnation (PeerId, StampId and PublicKey), and the invitation it
389
+ -- redeemed, read from committed.CadreInvite, was issued by the stored VouchOwner. The
390
+ -- usage row carries the liveness conditions (unexpired, not withdrawn, uses left,
391
+ -- issuer still an owner); this branch only requires that it exists. A reader verifies
392
+ -- the chain row -> usage -> invitation against its anchor (cadre-core
393
+ -- verifyInvitationAdmission), so the invitation row must outlive the rows it admitted.
394
+ -- A removed device may redeem a still-live invitation again and come back: keeping a
395
+ -- device out is removing its row AND withdrawing the invitation.
396
+ constraint AuthorizedInsert check on insert (
397
+ (
398
+ 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.CadrePeer', 'vouch', party_id(), new.PeerId, new.StampId), context.Signature, A.Key, 'ed25519'))
399
+ and coalesce(new.VouchOwner, '') = context.OwnerKey
400
+ and coalesce(new.VouchSig, '') = context.Signature
401
+ and new.VouchUsage is null
402
+ )
403
+ or (
404
+ new.VouchSig is null
405
+ and new.VouchOwner is not null
406
+ and new.VouchUsage is not null
407
+ and exists (
408
+ select 1 from CadreInviteUsage U
409
+ where U.UsageStampId = new.VouchUsage
410
+ and U.PeerId = new.PeerId
411
+ and U.PeerStampId = new.StampId
412
+ and U.PeerKey = new.PublicKey
413
+ and exists (select 1 from committed.CadreInvite I where I.Key = U.InviteKey and I.IssuerKey = new.VouchOwner)
414
+ )
415
+ )
416
+ ),
417
+ -- The stored 'vouch' voucher never authorizes a delete; the 'remove' signature rides in
418
+ -- context and is never stored. Or a reap (ValidationKey.AuthorizedDelete).
419
+ constraint AuthorizedDelete check on delete (
420
+ 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.CadrePeer', 'remove', party_id(), old.PeerId, old.StampId), context.Signature, A.Key, 'ed25519'))
421
+ or exists (select 1 from committed.Revocation R where R.TableName = 'CadrePeer' and R.RowKey = old.PeerId and R.StampId = old.StampId)
422
+ ),
423
+ -- A peer re-publishes its own addrs, profile and freshness under its own key (cadre-core
424
+ -- peer-record.ts); everything else is immutable and UpdatedAt must increase. Or an owner
425
+ -- re-vouches the row over its current stamp, which also clears VouchUsage: the row then
426
+ -- stands on the signature and its invitation is no longer consulted for it. The owner
427
+ -- branch keeps what the machine itself declares (PublicKey, Multiaddr, Sig, Profile): the
428
+ -- row's stored VouchSig is a valid re-vouch signature, so any machine holding the row can
429
+ -- take this branch. NOTE: it may still bump UpdatedAt (reauthorizeCadrePeer's re-broadcast
430
+ -- needs it), which on a self-published row leaves Sig unverifiable until the machine's
431
+ -- next heartbeat re-signs past it; if that window ever matters, pin UpdatedAt here when
432
+ -- old.Sig is not null. StampId never
433
+ -- changes on update: an update that rotated it would leave the old stamp free and
434
+ -- untombstoned, so retiring a stamp is delete plus tombstone. The proof columns are
435
+ -- compared null-safe: VouchSig and VouchUsage are null on one of the two row kinds, and a
436
+ -- bare \`new.X = old.X\` over a null would let a self-publish fill the column in.
437
+ constraint AuthorizedUpdate check on update (
438
+ (
439
+ new.PeerId = old.PeerId
440
+ and new.PublicKey = old.PublicKey
441
+ and new.StampId = old.StampId
442
+ and new.VouchOwner = old.VouchOwner
443
+ and coalesce(new.VouchSig, '') = coalesce(old.VouchSig, '')
444
+ and coalesce(new.VouchUsage, '') = coalesce(old.VouchUsage, '')
445
+ and new.UpdatedAt > coalesce(old.UpdatedAt, 0)
446
+ and verify(
447
+ digest('CadreControl.CadrePeer', 'publish', party_id(), new.PeerId, new.Multiaddr, cast(new.UpdatedAt as text), coalesce(new.Profile, '')),
448
+ new.Sig, new.PublicKey, 'ed25519')
449
+ )
450
+ or (
451
+ new.StampId = old.StampId
452
+ and 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.CadrePeer', 'vouch', party_id(), new.PeerId, new.StampId), context.Signature, A.Key, 'ed25519'))
453
+ and coalesce(new.VouchOwner, '') = context.OwnerKey
454
+ and coalesce(new.VouchSig, '') = context.Signature
455
+ and new.VouchUsage is null
456
+ and coalesce(new.PublicKey, '') = coalesce(old.PublicKey, '')
457
+ and coalesce(new.Multiaddr, '') = coalesce(old.Multiaddr, '')
458
+ and coalesce(new.Sig, '') = coalesce(old.Sig, '')
459
+ and coalesce(new.Profile, '') = coalesce(old.Profile, '')
460
+ )
461
+ )
462
+ ) with context (OwnerKey text null, Signature text);
463
+
464
+ -- A mobile peer's platform push token, self-published so a server peer can push-wake a
465
+ -- suspended app. A resolver re-verifies Sig against the CadrePeer.PublicKey for PeerId
466
+ -- (cadre-core device-token.ts), checks freshness, then hands the token to the push sender.
467
+ table DeviceToken (
468
+ PeerId text primary key,
469
+ Platform text, -- 'fcm' | 'apns'
470
+ Token text, -- opaque platform device/registration token
471
+ UpdatedAt int null, -- epoch ms; strictly increases per self-update
472
+ Sig text null, -- self-signature over the 'publish' digest
473
+ StampId text unique,
474
+ -- A resurrected token has no freshness ceiling to retire it (resolveDeviceToken defaults
475
+ -- maxAgeMs to infinity), so stamp retirement is what makes a clear stick.
476
+ constraint NotRevoked check on insert (
477
+ not exists (select 1 from Revocation R where R.TableName = 'DeviceToken' and R.StampId = new.StampId)
478
+ ),
479
+ constraint RevocationRecorded check on delete (
480
+ exists (select 1 from Revocation R where R.TableName = 'DeviceToken' and R.StampId = old.StampId and R.RowKey = old.PeerId)
481
+ ),
482
+ -- The owner signs the whole row, nullable columns as '', so a replayed approval can only
483
+ -- reproduce the row it approved (cadre-core peer-authorization.ts deviceTokenAddDigest).
484
+ constraint AuthorizedInsert check on insert (
485
+ 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(
486
+ digest(
487
+ 'CadreControl.DeviceToken', 'add', party_id(),
488
+ new.PeerId,
489
+ new.Platform,
490
+ new.Token,
491
+ coalesce(cast(new.UpdatedAt as text), ''),
492
+ coalesce(new.Sig, ''),
493
+ new.StampId
494
+ ),
495
+ context.Signature, A.Key, 'ed25519'))
496
+ ),
497
+ constraint AuthorizedDelete check on delete (
498
+ 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.DeviceToken', 'remove', party_id(), old.PeerId, old.StampId), context.Signature, A.Key, 'ed25519'))
499
+ or exists (select 1 from committed.Revocation R where R.TableName = 'DeviceToken' and R.RowKey = old.PeerId and R.StampId = old.StampId)
500
+ ),
501
+ -- Self-update only: Platform and Token may change, PeerId and StampId may not, UpdatedAt
502
+ -- must increase. No owner re-touch branch: one outside the monotonicity rule would let a
503
+ -- captured approval roll UpdatedAt back; an owner correcting a row deletes and re-inserts.
504
+ constraint AuthorizedUpdate check on update (
505
+ new.PeerId = old.PeerId
506
+ and new.StampId = old.StampId
507
+ and new.UpdatedAt > coalesce(old.UpdatedAt, 0)
508
+ and exists (select 1 from CadrePeer P where P.PeerId = new.PeerId and verify(
509
+ digest('CadreControl.DeviceToken', 'publish', party_id(), new.PeerId, new.Platform, new.Token, cast(new.UpdatedAt as text)),
510
+ new.Sig, P.PublicKey, 'ed25519'))
511
+ )
512
+ ) with context (OwnerKey text null, Signature text);
513
+
514
+ -- An open invitation to form a strand with this party.
515
+ table FormationInvite (
516
+ Token text primary key, -- a random string
517
+ sAppId text, -- the app of the strand to be formed
518
+ ExpiresAt datetime null,
519
+ TotalUses int null check (TotalUses >= 0),
520
+ ValidationUrl text null, -- web hook sent the disclosure; a ValidationKey then signs off each redemption
521
+ StrandId text null, -- bound invite: the host strand consent is recorded against;
522
+ -- null: the responder provisions a fresh open strand per redemption
523
+ StampId text unique,
524
+ -- The owner signs the whole row, nullable columns as ''.
525
+ constraint AuthorizedInsert check on insert (
526
+ 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(
527
+ digest(
528
+ 'CadreControl.FormationInvite', 'add', party_id(),
529
+ new.Token,
530
+ new.sAppId,
531
+ coalesce(cast(new.ExpiresAt as text), ''),
532
+ coalesce(cast(new.TotalUses as text), ''),
533
+ coalesce(new.ValidationUrl, ''),
534
+ coalesce(new.StrandId, ''),
535
+ new.StampId
536
+ ),
537
+ context.Signature, A.Key, 'ed25519'))
538
+ ),
539
+ constraint AuthorizedDelete check on delete (
540
+ 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(
541
+ digest(
542
+ 'CadreControl.FormationInvite', 'remove', party_id(),
543
+ old.Token,
544
+ old.sAppId,
545
+ coalesce(cast(old.ExpiresAt as text), ''),
546
+ coalesce(cast(old.TotalUses as text), ''),
547
+ coalesce(old.ValidationUrl, ''),
548
+ coalesce(old.StrandId, ''),
549
+ old.StampId
550
+ ),
551
+ context.Signature, A.Key, 'ed25519'))
552
+ ),
553
+ -- Insert and delete only, so the consent parameters cannot change under a signature.
554
+ constraint Immutable check on update (false)
555
+ ) with context (OwnerKey text, Signature text);
556
+
557
+ -- Append-only record of formation invite redemptions, one row per redemption.
558
+ table FormationUsage (
559
+ Token text, -- the FormationInvite redeemed
560
+ UsageStampId text, -- single-use nonce minted by the joining peer, signed into its 'consent' digest
561
+ -- and the approver's 'vouch' digest. The primary key: this table never
562
+ -- deletes, so the key alone makes a nonce permanent and there is no
563
+ -- NotRevoked pair here. Two nodes that have not converged can each admit
564
+ -- the same nonce: a duplicated audit row, never a join nobody approved.
565
+ PeerKey text, -- the joining peer's ed25519 public key (base64url); its peer id derives from it
566
+ PeerSig text, -- the joiner's signature over the 'consent' digest (PeerConsented)
567
+ Disclosure text,
568
+ StrandId text,
569
+ StrandStampId text, -- the Strand.StampId this consent names: one strand row, not an id forever
570
+ -- (Strand.AuthorizedInsert). Not unique: a bound invite's redeemers share
571
+ -- a host strand.
572
+ primary key (UsageStampId),
573
+ constraint InsertOnly check on update, delete (false),
574
+ -- The use cap counts the pre-transaction snapshot, so concurrent redeemers on separate
575
+ -- nodes can over-admit by their number; both rows survive and removal is owner-gated. A
576
+ -- strict per-token sequence was rejected: a lost race would silently drop a consented
577
+ -- join. Two usage rows for one token in one transaction read the same count; no writer
578
+ -- does that.
579
+ -- A ValidationUrl invite needs a sign-off verified against a STORED ValidationKey row;
580
+ -- context.ValidationKey only selects which row, so a redeemer cannot approve itself with
581
+ -- a throwaway key. The sign-off covers (Token, UsageStampId, StrandId, PeerKey,
582
+ -- Disclosure), so it buys exactly one row; StrandStampId is not bound because the
583
+ -- unbound path mints it inside the redeeming transaction. Removing the key later does
584
+ -- not revisit rows it approved.
585
+ -- A bound invite may only name its own host strand. An unbound invite may name any
586
+ -- existing strand, which burns a use and forecloses that id's consent-seating: harmless.
587
+ -- Neither digest of this table binds party_id(), alone in this schema: the joiner signs
588
+ -- its consent before the responder's party id reaches it (it arrives in the result
589
+ -- frame) and with a key made fresh for this one formation, and the outside approver
590
+ -- behind ValidationUrl is sent only the five fields below. Both digests bind the
591
+ -- invitation Token, a random secret seated in one party's FormationInvite table, so
592
+ -- replaying either in another party needs that party's owner to seat the same token in
593
+ -- its own database, and the only database harmed would be its own.
594
+ constraint Authorized check on insert (
595
+ exists (
596
+ select 1 from FormationInvite FI
597
+ where FI.Token = new.Token
598
+ and (FI.TotalUses is null or FI.TotalUses > (select count(1) from committed.FormationUsage U where U.Token = new.Token))
599
+ and (FI.ExpiresAt is null or FI.ExpiresAt > context.Now)
600
+ and (FI.ValidationUrl is null or exists (
601
+ select 1 from ValidationKey VK
602
+ where VK.Key = context.ValidationKey
603
+ and verify(digest('CadreControl.FormationUsage', 'vouch', new.Token, new.UsageStampId, new.StrandId, new.PeerKey, new.Disclosure), context.ValidationSignature, VK.Key, 'ed25519')))
604
+ and (FI.StrandId is null or FI.StrandId = new.StrandId)
605
+ )
606
+ ),
607
+ -- A usage row may only name a strand row that exists, so a consent record cannot be held
608
+ -- in reserve to re-seat an id after its removal.
609
+ constraint StrandExists check (exists (select 1 from Strand S where S.Id = new.StrandId and S.StampId = new.StrandStampId)),
610
+ -- The joiner proves it agreed. PeerKey is its own key, so there is nothing for a writer to
611
+ -- substitute; stored on the row so any reader can re-check it (verifyFormationConsent).
612
+ -- StrandId and the party id are not signed: the joiner cannot know them when it signs
613
+ -- (Authorized, above).
614
+ constraint PeerConsented check on insert (
615
+ verify(digest('CadreControl.FormationUsage', 'consent',
616
+ new.Token, new.UsageStampId, new.PeerKey, new.Disclosure),
617
+ new.PeerSig, new.PeerKey, 'ed25519')
618
+ )
619
+ ) with context (Now datetime, ValidationKey text null, ValidationSignature text null);
620
+
621
+ -- 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
623
+ -- convergence is guarded by the integration scenario strand-formation-concurrent-redemption.
624
+ index FormationUsageByToken on FormationUsage (Token);
625
+
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
628
+ -- 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
631
+ -- through one carries no owner signature of its own, and its membership is the chain
632
+ -- invitation -> usage -> peer row, verified at read time, so the row must outlive every
633
+ -- device it admitted. Withdrawing an invitation is an owner-signed Revocation tombstone
634
+ -- over the row's stamp with the row kept (Revocation.RowIsGone); liveness is read as no
635
+ -- tombstone, unexpired, uses left, issuer still an owner (CadreInviteUsage.Authorized).
636
+ -- NOTE: expired and withdrawn rows accumulate for the life of the party; the same tripwire
637
+ -- as hasOutstandingFormationInvite applies (ControlDatabase.hasLiveCadreInvite). An owner
638
+ -- retires a device's dependency on its invitation by re-vouching the row (the owner branch
639
+ -- of CadrePeer.AuthorizedUpdate).
640
+ table CadreInvite (
641
+ Key text primary key, -- the invitation's ed25519 public key (base64url)
642
+ 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
+ ExpiresAt datetime null,
645
+ TotalUses int null check (TotalUses >= 0),
646
+ IssuerKey text, -- the owner that signed the row (== insert context.OwnerKey). A reader checks it
647
+ -- against its node-local anchor, never against the replicated OwnerKey table.
648
+ IssuerSig text, -- that owner's 'add' signature (== insert context.Signature), stored so any reader
649
+ -- can re-verify the row from the holder's copy or the replicated one
650
+ StampId text unique,
651
+ -- A withdrawn invitation cannot be re-seated from the signed copy its holder keeps, on
652
+ -- any node that holds the tombstone (schema header for the node that does not).
653
+ constraint NotRevoked check on insert (
654
+ not exists (select 1 from Revocation R where R.TableName = 'CadreInvite' and R.StampId = new.StampId)
655
+ ),
656
+ -- The owner signs the whole row, nullable columns as '' (cadre-core cadreInviteRowFields),
657
+ -- and the stored issuer pair must be the pair verified here, as CadrePeer pins its voucher.
658
+ constraint AuthorizedInsert check on insert (
659
+ 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(
660
+ digest(
661
+ 'CadreControl.CadreInvite', 'add', party_id(),
662
+ new.Key,
663
+ coalesce(new.PeerId, ''),
664
+ cast(new.GrantsOwner as text),
665
+ coalesce(cast(new.ExpiresAt as text), ''),
666
+ coalesce(cast(new.TotalUses as text), ''),
667
+ new.StampId
668
+ ),
669
+ context.Signature, A.Key, 'ed25519'))
670
+ and coalesce(new.IssuerKey, '') = context.OwnerKey
671
+ and coalesce(new.IssuerSig, '') = context.Signature
672
+ ),
673
+ constraint NoUpdate check on update (false),
674
+ -- Never deleted (table comment). Withdrawal is the tombstone alone.
675
+ constraint NoDelete check on delete (false)
676
+ ) with context (OwnerKey text, Signature text);
677
+
678
+ -- 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.
681
+ table CadreInviteUsage (
682
+ UsageStampId text primary key, -- nonce minted by the redeeming device and signed into both signatures
683
+ -- below. The primary key: this table never deletes, so the key alone
684
+ -- makes a nonce permanent and there is no NotRevoked pair here. Two nodes
685
+ -- that have not converged can each admit the same nonce (FormationUsage).
686
+ InviteKey text, -- the CadreInvite redeemed
687
+ PeerId text, -- the redeeming device: the identity multihash of PeerKey. The writer
688
+ -- asserts the pair and a reader re-derives it (verifyInvitationAdmission);
689
+ -- the schema cannot unwrap a multihash.
690
+ PeerKey text, -- the redeeming device's ed25519 public key (base64url)
691
+ PeerStampId text, -- the CadrePeer.StampId this usage seats: one row incarnation, not a peer id
692
+ -- forever. Minted by the writer inside the redeeming transaction and not
693
+ -- signed (FormationUsage.StrandStampId).
694
+ OwnerStampId text null, -- the OwnerKey.StampId this usage seats when the invitation grants ownership
695
+ InviteSig text, -- by the invitation's private key over the 'redeem' digest: proof of possession
696
+ PeerSig text, -- by PeerKey over the 'consent' digest: the device agreed to join
697
+ constraint InsertOnly check on update, delete (false),
698
+ -- 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).
700
+ -- 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.
710
+ constraint Authorized check on insert (
711
+ exists (
712
+ select 1 from committed.CadreInvite I
713
+ where I.Key = new.InviteKey
714
+ and not exists (select 1 from Revocation R where R.TableName = 'CadreInvite' and R.StampId = I.StampId)
715
+ and (I.ExpiresAt is null or I.ExpiresAt > context.Now)
716
+ and (I.TotalUses is null or I.TotalUses > (select count(1) from committed.CadreInviteUsage U where U.InviteKey = new.InviteKey))
717
+ and (I.PeerId is null or I.PeerId = new.PeerId)
718
+ and exists (select 1 from committed.OwnerKey K where K.Key = I.IssuerKey and not exists (select 1 from Revocation R where R.TableName = 'OwnerKey' and R.StampId = K.StampId))
719
+ )
720
+ ),
721
+ -- Verified against the invitation key itself: the member writing the row can check this
722
+ -- signature but, holding only the public half, cannot produce it.
723
+ constraint InvitePossessed check on insert (
724
+ verify(digest('CadreControl.CadreInviteUsage', 'redeem', party_id(), new.InviteKey, new.UsageStampId, new.PeerKey),
725
+ new.InviteSig, new.InviteKey, 'ed25519')
726
+ ),
727
+ -- The device proves it agreed (FormationUsage.PeerConsented).
728
+ constraint PeerConsented check on insert (
729
+ verify(digest('CadreControl.CadreInviteUsage', 'consent', party_id(), new.InviteKey, new.UsageStampId, new.PeerKey),
730
+ new.PeerSig, new.PeerKey, 'ed25519')
731
+ ),
732
+ -- Deferred, so the same-transaction peer insert counts: a usage row can never be held in
733
+ -- 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.
736
+ constraint PeerExists check (exists (
737
+ 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
+ )
743
+ ) with context (Now datetime);
744
+
745
+ -- Serves the cap count above and the liveness reads (cadre-core countCadreInviteUsage,
746
+ -- hasLiveCadreInvite) on an append-only table, as FormationUsageByToken does.
747
+ index CadreInviteUsageByInvite on CadreInviteUsage (InviteKey);
748
+
749
+ -- Append-only retirement record for the stamps of removed rows of every guarded table, with
750
+ -- 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).
757
+ table Revocation (
758
+ TableName text, -- 'OwnerKey' | 'ValidationKey' | 'Strand' | 'StrandPartyKey' | 'JoinedStrand' |
759
+ -- 'JoinRequest' | 'CadrePeer' | 'DeviceToken' | 'CadreInvite', or 'Revocation'
760
+ -- for the marker
761
+ RowKey text, -- primary key of the removed row
762
+ StampId text, -- the retired stamp
763
+ ReissuedAt integer default 0, -- bumped by an owner-signed re-issue so a tombstone committed while the
764
+ -- node was alone can be re-written and so re-broadcast; nothing reads it
765
+ SignerKey text null, -- the owner that filed the tombstone (== insert context.OwnerKey)
766
+ SignerSig text null, -- that owner's 'remove' signature (== insert context.Signature). A node that
767
+ -- received the row by replication re-verifies the pair and judges SignerKey
768
+ -- against its node-local trusted-owner anchor (cadre-core
769
+ -- verifyRevocationSigner), not against this replicated table.
770
+ -- Keyed on the stamp: one row key may carry several tombstones over its life.
771
+ primary key (TableName, StampId),
772
+ -- Retirement is permanent.
773
+ constraint NoDelete check on delete (false),
774
+ -- Pinned at 0 so an owner cannot seat a tombstone at a counter its own re-issues cannot pass.
775
+ constraint FreshTombstone check on insert (new.ReissuedAt = 0),
776
+ -- A re-issue moves nothing but the counter, and only upward. The signer pair stays the
777
+ -- filing owner's; the re-issuing owner's signature rides in context only.
778
+ constraint ReissueOnly check on update (
779
+ new.TableName = old.TableName and new.RowKey = old.RowKey
780
+ and new.StampId = old.StampId and new.ReissuedAt > old.ReissuedAt
781
+ and coalesce(new.SignerKey, '') = coalesce(old.SignerKey, '')
782
+ and coalesce(new.SignerSig, '') = coalesce(old.SignerSig, '')
783
+ ),
784
+ -- A stamp is retired only once its row is gone (deferred, so the same-transaction delete
785
+ -- counts). Keyed on the stamp, not RowKey: a node that converges on a re-admission before
786
+ -- the earlier removal's tombstone must still accept that tombstone. Retiring a stamp that
787
+ -- 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'.
791
+ constraint RowIsGone check on insert (
792
+ (new.TableName = 'OwnerKey' and not exists (select 1 from OwnerKey K where K.StampId = new.StampId))
793
+ or (new.TableName = 'CadrePeer' and not exists (select 1 from CadrePeer P where P.StampId = new.StampId))
794
+ or (new.TableName = 'ValidationKey' and not exists (select 1 from ValidationKey V where V.StampId = new.StampId))
795
+ or (new.TableName = 'Strand' and not exists (select 1 from Strand S where S.StampId = new.StampId))
796
+ or (new.TableName = 'StrandPartyKey' and not exists (select 1 from StrandPartyKey K where K.StampId = new.StampId))
797
+ or (new.TableName = 'JoinedStrand' and not exists (select 1 from JoinedStrand J where J.StampId = new.StampId))
798
+ or (new.TableName = 'JoinRequest' and not exists (select 1 from JoinRequest J where J.StampId = new.StampId))
799
+ or (new.TableName = 'DeviceToken' and not exists (select 1 from DeviceToken D where D.StampId = new.StampId))
800
+ or (new.TableName = 'CadreInvite')
801
+ or (new.TableName = 'Revocation' and new.RowKey = 'ledger' and new.StampId = 'opened')
802
+ ),
803
+ -- Owner-signed over the whole row under its own domain tag, so the delete's 'remove'
804
+ -- signature filed in the same transaction never doubles as the tombstone's. Ungated, any
805
+ -- writer could evict a peer party-wide and block its re-admission. RowKey is not checked
806
+ -- against the retired row here (that would reject a tombstone re-issued later, or filed on
807
+ -- 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.
811
+ constraint Authorized check on insert (
812
+ 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
+ and coalesce(new.SignerKey, '') = context.OwnerKey
814
+ and coalesce(new.SignerSig, '') = context.Signature
815
+ ),
816
+ -- Distinct 'reissue' tag, binding ReissuedAt, so neither approval replays as the other.
817
+ constraint AuthorizedReissue check on update (
818
+ exists (select 1 from OwnerKey A where A.Key = context.OwnerKey
819
+ and not exists (select 1 from Revocation R where R.TableName = 'OwnerKey' and R.StampId = A.StampId)
820
+ and verify(digest('CadreControl.Revocation', 'reissue', party_id(),
821
+ new.TableName, new.RowKey, new.StampId, cast(new.ReissuedAt as text)),
822
+ context.Signature, A.Key, 'ed25519'))
823
+ )
824
+ ) with context (OwnerKey text, Signature text);
825
+ }
826
+
827
+ apply schema CadreControl;
828
+ `;
1079
829
  //# sourceMappingURL=control-schema.js.map