@serfab/cadre-core 1.15.0 → 1.17.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 (199) hide show
  1. package/README.md +1 -1
  2. package/dist/bootstrap-peer-store-file.js +1 -1
  3. package/dist/bootstrap-peer-store.d.ts +1 -1
  4. package/dist/bootstrap-peer-store.js +4 -4
  5. package/dist/cadre-invite-protocol.d.ts +55 -138
  6. package/dist/cadre-invite-protocol.js +94 -449
  7. package/dist/cadre-invite-protocol.js.map +1 -1
  8. package/dist/cadre-join-request.d.ts +15 -2
  9. package/dist/cadre-join-request.js +22 -3
  10. package/dist/cadre-join-request.js.map +1 -1
  11. package/dist/cadre-node.d.ts +530 -247
  12. package/dist/cadre-node.js +1046 -525
  13. package/dist/cadre-node.js.map +1 -1
  14. package/dist/control-authorization.d.ts +36 -14
  15. package/dist/control-authorization.js +20 -3
  16. package/dist/control-authorization.js.map +1 -1
  17. package/dist/control-cohort.d.ts +22 -23
  18. package/dist/control-cohort.js +24 -24
  19. package/dist/control-cohort.js.map +1 -1
  20. package/dist/control-database.d.ts +416 -136
  21. package/dist/control-database.js +757 -378
  22. package/dist/control-database.js.map +1 -1
  23. package/dist/control-protocol-guard.d.ts +5 -4
  24. package/dist/control-protocol-guard.js +6 -3
  25. package/dist/control-protocol-guard.js.map +1 -1
  26. package/dist/control-schema-version.d.ts +80 -0
  27. package/dist/control-schema-version.js +203 -0
  28. package/dist/control-schema-version.js.map +1 -0
  29. package/dist/control-schema.d.ts +45 -5
  30. package/dist/control-schema.js +254 -70
  31. package/dist/control-schema.js.map +1 -1
  32. package/dist/control-write-retry.d.ts +5 -5
  33. package/dist/control-write-retry.js +10 -8
  34. package/dist/control-write-retry.js.map +1 -1
  35. package/dist/delegate-admission.d.ts +13 -0
  36. package/dist/delegate-admission.js +27 -0
  37. package/dist/delegate-admission.js.map +1 -1
  38. package/dist/ed25519-key.d.ts +10 -7
  39. package/dist/ed25519-key.js +23 -20
  40. package/dist/ed25519-key.js.map +1 -1
  41. package/dist/edge-node/config.d.ts +122 -0
  42. package/dist/edge-node/config.js +199 -0
  43. package/dist/edge-node/config.js.map +1 -0
  44. package/dist/edge-node/dissolve.d.ts +95 -0
  45. package/dist/edge-node/dissolve.js +184 -0
  46. package/dist/edge-node/dissolve.js.map +1 -0
  47. package/dist/edge-node/edge-join.d.ts +44 -0
  48. package/dist/edge-node/edge-join.js +227 -0
  49. package/dist/edge-node/edge-join.js.map +1 -0
  50. package/dist/edge-node/edge-node.d.ts +295 -0
  51. package/dist/edge-node/edge-node.js +704 -0
  52. package/dist/edge-node/edge-node.js.map +1 -0
  53. package/dist/edge-node/index.d.ts +18 -0
  54. package/dist/edge-node/index.js +18 -0
  55. package/dist/edge-node/index.js.map +1 -0
  56. package/dist/edge-node/join-credential.d.ts +11 -0
  57. package/dist/edge-node/join-credential.js +11 -0
  58. package/dist/edge-node/join-credential.js.map +1 -0
  59. package/dist/edge-node/lazy-raw-storage.d.ts +40 -0
  60. package/dist/edge-node/lazy-raw-storage.js +134 -0
  61. package/dist/edge-node/lazy-raw-storage.js.map +1 -0
  62. package/dist/edge-node/node-local.d.ts +60 -0
  63. package/dist/edge-node/node-local.js +58 -0
  64. package/dist/edge-node/node-local.js.map +1 -0
  65. package/dist/edge-node/options.d.ts +83 -0
  66. package/dist/edge-node/options.js +110 -0
  67. package/dist/edge-node/options.js.map +1 -0
  68. package/dist/edge-node/owner-grant.d.ts +41 -0
  69. package/dist/edge-node/owner-grant.js +38 -0
  70. package/dist/edge-node/owner-grant.js.map +1 -0
  71. package/dist/edge-node/platform.d.ts +140 -0
  72. package/dist/edge-node/platform.js +12 -0
  73. package/dist/edge-node/platform.js.map +1 -0
  74. package/dist/edge-node/strand-move.d.ts +115 -0
  75. package/dist/edge-node/strand-move.js +223 -0
  76. package/dist/edge-node/strand-move.js.map +1 -0
  77. package/dist/edge-node/strands.d.ts +28 -0
  78. package/dist/edge-node/strands.js +60 -0
  79. package/dist/edge-node/strands.js.map +1 -0
  80. package/dist/index.d.ts +16 -7
  81. package/dist/index.js +26 -7
  82. package/dist/index.js.map +1 -1
  83. package/dist/joined-strand-store.d.ts +3 -3
  84. package/dist/joined-strand-store.js +3 -3
  85. package/dist/joined-strand-store.js.map +1 -1
  86. package/dist/key-store.d.ts +9 -4
  87. package/dist/key-store.js +9 -4
  88. package/dist/key-store.js.map +1 -1
  89. package/dist/machine-removal.d.ts +19 -39
  90. package/dist/machine-removal.js +24 -98
  91. package/dist/machine-removal.js.map +1 -1
  92. package/dist/member-redemption.d.ts +250 -0
  93. package/dist/member-redemption.js +485 -0
  94. package/dist/member-redemption.js.map +1 -0
  95. package/dist/owner-anchor-sync.d.ts +4 -4
  96. package/dist/owner-anchor-sync.js +16 -15
  97. package/dist/owner-anchor-sync.js.map +1 -1
  98. package/dist/owner-grant-protocol.d.ts +247 -0
  99. package/dist/owner-grant-protocol.js +330 -0
  100. package/dist/owner-grant-protocol.js.map +1 -0
  101. package/dist/owner-key-export.d.ts +70 -0
  102. package/dist/owner-key-export.js +279 -0
  103. package/dist/owner-key-export.js.map +1 -0
  104. package/dist/owner-key-flows.d.ts +55 -0
  105. package/dist/owner-key-flows.js +60 -0
  106. package/dist/owner-key-flows.js.map +1 -0
  107. package/dist/owner-key-store.d.ts +31 -0
  108. package/dist/owner-key-store.js +81 -0
  109. package/dist/owner-key-store.js.map +1 -0
  110. package/dist/owner-keys.d.ts +78 -0
  111. package/dist/owner-keys.js +133 -0
  112. package/dist/owner-keys.js.map +1 -0
  113. package/dist/owner-signer.d.ts +54 -0
  114. package/dist/owner-signer.js +51 -0
  115. package/dist/owner-signer.js.map +1 -0
  116. package/dist/peer-authorization.d.ts +83 -18
  117. package/dist/peer-authorization.js +123 -53
  118. package/dist/peer-authorization.js.map +1 -1
  119. package/dist/provider/rent-node.js +1 -1
  120. package/dist/provider/rent-node.js.map +1 -1
  121. package/dist/seed-bootstrap.d.ts +131 -109
  122. package/dist/seed-bootstrap.js +260 -248
  123. package/dist/seed-bootstrap.js.map +1 -1
  124. package/dist/seed-trust-policy.js +4 -4
  125. package/dist/strand-addr-protocol.d.ts +4 -1
  126. package/dist/strand-addr-protocol.js +8 -2
  127. package/dist/strand-addr-protocol.js.map +1 -1
  128. package/dist/strand-cohort.d.ts +3 -0
  129. package/dist/strand-cohort.js.map +1 -1
  130. package/dist/strand-database.d.ts +1 -0
  131. package/dist/strand-database.js +3 -0
  132. package/dist/strand-database.js.map +1 -1
  133. package/dist/strand-first-sync-gate.d.ts +4 -3
  134. package/dist/strand-first-sync-gate.js +4 -3
  135. package/dist/strand-first-sync-gate.js.map +1 -1
  136. package/dist/strand-instance-manager.d.ts +1 -1
  137. package/dist/strand-membership-writer.d.ts +13 -2
  138. package/dist/strand-membership-writer.js +52 -16
  139. package/dist/strand-membership-writer.js.map +1 -1
  140. package/dist/strand-move.d.ts +6 -5
  141. package/dist/strand-move.js +31 -30
  142. package/dist/strand-move.js.map +1 -1
  143. package/dist/strand-peer-redial.js +2 -3
  144. package/dist/strand-peer-redial.js.map +1 -1
  145. package/dist/types.d.ts +286 -103
  146. package/dist/types.js.map +1 -1
  147. package/package.json +16 -7
  148. package/src/bootstrap-peer-store-file.ts +44 -44
  149. package/src/bootstrap-peer-store.ts +214 -214
  150. package/src/cadre-invite-protocol.ts +131 -531
  151. package/src/cadre-join-request.ts +188 -167
  152. package/src/cadre-node.ts +1166 -541
  153. package/src/control-authorization.ts +47 -14
  154. package/src/control-cohort.ts +127 -128
  155. package/src/control-database.ts +940 -456
  156. package/src/control-protocol-guard.ts +7 -4
  157. package/src/control-schema-version.ts +251 -0
  158. package/src/control-schema.ts +265 -70
  159. package/src/control-write-retry.ts +569 -567
  160. package/src/delegate-admission.ts +37 -0
  161. package/src/ed25519-key.ts +174 -171
  162. package/src/edge-node/config.ts +277 -0
  163. package/src/edge-node/dissolve.ts +251 -0
  164. package/src/edge-node/edge-join.ts +319 -0
  165. package/src/edge-node/edge-node.ts +1045 -0
  166. package/src/edge-node/index.ts +59 -0
  167. package/src/edge-node/join-credential.ts +16 -0
  168. package/src/edge-node/lazy-raw-storage.ts +178 -0
  169. package/src/edge-node/node-local.ts +84 -0
  170. package/src/edge-node/options.ts +179 -0
  171. package/src/edge-node/owner-grant.ts +63 -0
  172. package/src/edge-node/platform.ts +151 -0
  173. package/src/edge-node/strand-move.ts +270 -0
  174. package/src/edge-node/strands.ts +72 -0
  175. package/src/index.ts +102 -5
  176. package/src/joined-strand-store.ts +4 -4
  177. package/src/key-store.ts +10 -4
  178. package/src/machine-removal.ts +26 -111
  179. package/src/member-redemption.ts +642 -0
  180. package/src/owner-anchor-sync.ts +19 -18
  181. package/src/owner-grant-protocol.ts +532 -0
  182. package/src/owner-key-export.ts +348 -0
  183. package/src/owner-key-flows.ts +109 -0
  184. package/src/owner-key-store.ts +85 -0
  185. package/src/owner-keys.ts +178 -0
  186. package/src/owner-signer.ts +94 -0
  187. package/src/peer-authorization.ts +558 -488
  188. package/src/provider/rent-node.ts +1 -1
  189. package/src/seed-bootstrap.ts +301 -312
  190. package/src/seed-trust-policy.ts +4 -4
  191. package/src/strand-addr-protocol.ts +547 -538
  192. package/src/strand-cohort.ts +4 -0
  193. package/src/strand-database.ts +3 -0
  194. package/src/strand-first-sync-gate.ts +4 -3
  195. package/src/strand-instance-manager.ts +1 -1
  196. package/src/strand-membership-writer.ts +61 -16
  197. package/src/strand-move.ts +45 -36
  198. package/src/strand-peer-redial.ts +2 -3
  199. package/src/types.ts +304 -103
@@ -1,15 +1,43 @@
1
+ import { Parser } from '@quereus/quereus';
2
+
1
3
  /**
2
- * Embedded control schema for cross-platform compatibility.
4
+ * The version of the declared control schema, carried in the schema text as the
5
+ * {@link CONTROL_SCHEMA_VERSION_TAG} tag on the `OwnerKey` table. A warm start compares
6
+ * it with the version the store records before applying anything
7
+ * (`control-schema-version.ts`, which also says why that is the version the store was
8
+ * created under).
3
9
  *
4
- * This is the authoritative runtime copy of the CadreControl authorization
5
- * schema. It is duplicated from `schemas/control.qsql` so that React Native and
6
- * other filesystem-less environments get the schema without a runtime file read.
10
+ * When it changes, what a new version may change, and the fixture each version keeps:
11
+ * docs/architecture.md → "Control schema changes and incompatible stores", enforced by
12
+ * `control-schema-versions.spec.ts`.
13
+ */
14
+ export const CONTROL_SCHEMA_VERSION = 3;
15
+
16
+ /**
17
+ * The first control schema version whose fixture is kept, and the oldest a store can be
18
+ * migrated from. Stores that record no version (written by 1.16.0 and earlier) are not
19
+ * migrated; docs/architecture.md says why.
20
+ */
21
+ export const CONTROL_SCHEMA_BASELINE_VERSION = 3;
22
+
23
+ /** The `OwnerKey` table tag that records {@link CONTROL_SCHEMA_VERSION} in the schema and in every store. */
24
+ export const CONTROL_SCHEMA_VERSION_TAG = 'sereus.controlSchemaVersion';
25
+
26
+ /**
27
+ * The `declare schema CadreControl { … }` statement, with its header comment.
28
+ *
29
+ * Together with {@link APPLY_CONTROL_SCHEMA} this is the authoritative runtime copy of
30
+ * the CadreControl authorization schema ({@link CONTROL_SCHEMA}). It is duplicated from
31
+ * `schemas/control.qsql` so that React Native and other filesystem-less environments
32
+ * get the schema without a runtime file read. Held apart from the apply because
33
+ * `ControlDatabase.loadSchema` declares, checks the pending changes, and only then
34
+ * applies.
7
35
  *
8
36
  * The two copies MUST stay identical — `control-schema-drift.spec.ts` fails the
9
37
  * build if they drift. Any edit here must be mirrored in `schemas/control.qsql`
10
38
  * and vice versa.
11
39
  */
12
- export const CONTROL_SCHEMA = `-- A Sereus party's cadre (its nodes) and their participation in strands (networks).
40
+ export const CONTROL_SCHEMA_DECLARATION = `-- A Sereus party's cadre (its nodes) and their participation in strands (networks).
13
41
  --
14
42
  -- Every write is authorized by a check: an owner signs an action-tagged digest of the row
15
43
  -- (cadre-core control-authorization.ts), or a peer signs its own record. Each digest leads
@@ -21,13 +49,14 @@ export const CONTROL_SCHEMA = `-- A Sereus party's cadre (its nodes) and their p
21
49
  -- two digests scoped by the invitation token instead (FormationUsage.Authorized).
22
50
  --
23
51
  -- 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.
52
+ -- JoinRequest, CadrePeer, DeviceToken, CadreInvite, OwnerGrant) carry a one-off StampId bound
53
+ -- into their add and remove digests. \`unique\` holds over live rows only, so a delete retires
54
+ -- the stamp into Revocation in the same transaction (RevocationRecorded) and an insert refuses
55
+ -- a retired stamp (NotRevoked): the never-expiring add approval cannot re-seat a removed row.
56
+ -- A legitimate re-add mints a fresh stamp and a fresh signature. CadreInvite and OwnerGrant
57
+ -- are the two guarded tables that are never deleted: a tombstone there withdraws the
58
+ -- invitation or grant while the row stays, because the rows it admitted prove themselves
59
+ -- through it.
31
60
  --
32
61
  -- A check whose expression evaluates to null passes (SQL three-valued logic), so every
33
62
  -- comparison over a nullable column that is meant to pin a value is written null-safe
@@ -47,12 +76,16 @@ export const CONTROL_SCHEMA = `-- A Sereus party's cadre (its nodes) and their p
47
76
  declare schema CadreControl {
48
77
  -- A key that can authorize control changes. A row is seated one of three ways, told apart
49
78
  -- 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.
79
+ -- and VouchSig pinned to the insert context, VouchUsage null), or an owner grant redeemed
80
+ -- by consent (VouchOwner = the grant's IssuerKey, VouchSig null, VouchUsage = the
81
+ -- OwnerGrantUsage row that seated it). A machine invitation (CadreInvite) admits a machine
82
+ -- and never seats a row here: a machine's identity key is never an owner key. A reader
83
+ -- judges the stored proof against its node-local anchor, never against this replicated
84
+ -- table.
54
85
  table OwnerKey (
55
86
  Key text primary key,
87
+ Label text null, -- the user's name for the key ("phone", "paper backup"); bound into the
88
+ -- signed add, so a relayed approval cannot seat the key under another name
56
89
  StampId text unique,
57
90
  VouchOwner text null,
58
91
  VouchSig text null,
@@ -60,6 +93,10 @@ declare schema CadreControl {
60
93
  constraint NotRevoked check on insert (
61
94
  not exists (select 1 from Revocation R where R.TableName = 'OwnerKey' and R.StampId = new.StampId)
62
95
  ),
96
+ -- Never empty: the add digest binds coalesce(Label, ''), so an empty label would sign as none.
97
+ constraint LabelLength check (
98
+ Label is null or length(Label) between 1 and 64
99
+ ),
63
100
  -- The tombstone must name the removed row, not just its stamp (Revocation.RowKey).
64
101
  constraint RevocationRecorded check on delete (
65
102
  exists (select 1 from Revocation R where R.TableName = 'OwnerKey' and R.StampId = old.StampId and R.RowKey = old.Key)
@@ -81,29 +118,34 @@ declare schema CadreControl {
81
118
  -- deliberately raw, the one owner read here that ignores Revocation: a machine holding only
82
119
  -- a retired owner row must still be refused a second genesis, or a removed device could
83
120
  -- 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.
121
+ -- The consent branch seats an owner with no signature at all: an OwnerGrantUsage row
122
+ -- written in the same transaction names this exact row (Key and stamp), and the grant
123
+ -- it redeemed was issued by the stored VouchOwner and carries the row's label (the
124
+ -- issuer's choice, bound into the grant's signature). The grant is read from its
125
+ -- committed table, so it cannot be seated and redeemed in one transaction; the usage
126
+ -- row carries the liveness conditions.
89
127
  constraint Authorized check on insert, delete (
90
128
  (old.Key is null and (select count(1) from committed.OwnerKey) = 0
91
129
  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'))
130
+ or (old.Key is null and exists (select 1 from committed.OwnerKey A where A.Key = context.OwnerKey and not exists (select 1 from Revocation R where R.TableName = 'OwnerKey' and R.StampId = A.StampId) and verify(digest('CadreControl.OwnerKey', 'add', party_id(), new.Key, coalesce(new.Label, ''), new.StampId), context.Signature, A.Key, 'ed25519'))
93
131
  and coalesce(new.VouchOwner, '') = context.OwnerKey
94
132
  and coalesce(new.VouchSig, '') = context.Signature
95
133
  and new.VouchUsage is null)
96
134
  or (old.Key is null and new.VouchSig is null and new.VouchUsage is not null
97
135
  and exists (
98
- select 1 from CadreInviteUsage U
136
+ select 1 from OwnerGrantUsage U
99
137
  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)
138
+ and U.SeatedKey = new.Key
139
+ and U.SeatedStampId = new.StampId
140
+ and exists (select 1 from committed.OwnerGrant G
141
+ where G.Key = U.GrantKey and G.IssuerKey = new.VouchOwner
142
+ and coalesce(G.Label, '') = coalesce(new.Label, ''))
103
143
  ))
104
144
  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
145
  )
106
- ) with context (OwnerKey text null, Signature text null);
146
+ -- The tag is the version of this whole schema (cadre-core CONTROL_SCHEMA_VERSION): every edit
147
+ -- to the schema bumps it, and a warm start reads the store's copy before applying anything.
148
+ ) with tags ("${CONTROL_SCHEMA_VERSION_TAG}" = ${CONTROL_SCHEMA_VERSION}) with context (OwnerKey text null, Signature text null);
107
149
 
108
150
  -- A key that can validate a strand formation disclosure (FormationUsage.Authorized).
109
151
  table ValidationKey (
@@ -136,9 +178,12 @@ declare schema CadreControl {
136
178
  Id text primary key, -- UUID
137
179
  MemberPrivateKey text null unique, -- the strand's shared read secret; closed strands only
138
180
  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).
181
+ FounderOwnerKey text null, -- the owner key that signed the insert; null on a consent-seated strand,
182
+ -- whose insert carries no signature. Provenance, not content (strandRowMismatches).
183
+ FounderPeerId text null, -- peer id of the machine that runs the founder bootstrap (cadre-node.ts
184
+ -- launchStrand), bound into the signed add; null on a consent-seated strand.
185
+ -- Not pinned to a CadrePeer row: a solo founder publishes before its own
186
+ -- row exists, and only an owner can name it. Provenance, as FounderOwnerKey.
142
187
  StampId text unique,
143
188
  constraint NotRevoked check on insert (
144
189
  not exists (select 1 from Revocation R where R.TableName = 'Strand' and R.StampId = new.StampId)
@@ -149,9 +194,10 @@ declare schema CadreControl {
149
194
  -- The consent branch below carries no signature, so an update rule would let anyone rewrite
150
195
  -- a consent-seated strand.
151
196
  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:
197
+ -- Owner-signed, with FounderOwnerKey pinned to the verified signer and FounderPeerId bound
198
+ -- into the signature; or consent-seated by a FormationUsage row naming this exact row (id
199
+ -- and stamp), written in the same transaction (ControlDatabase.redeemInvitation). Binding
200
+ -- the stamp is what makes a removal stick:
155
201
  -- the usage row outlives the strand, and keyed on the id alone it would re-authorize an
156
202
  -- unsigned re-seat under a fresh stamp. The consent branch is narrow, each clause closing
157
203
  -- one door:
@@ -164,12 +210,14 @@ declare schema CadreControl {
164
210
  -- A standalone tombstone (no accompanying delete, Revocation.Authorized) lets an owner
165
211
  -- foreclose consent-seating of an id that never existed: owner-only, and ids are random.
166
212
  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)
213
+ (exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and not exists (select 1 from Revocation R where R.TableName = 'OwnerKey' and R.StampId = A.StampId) and verify(digest('CadreControl.Strand', 'add', party_id(), new.Id, new.Type, coalesce(new.MemberPrivateKey, ''), new.FounderPeerId, new.StampId), context.Signature, A.Key, 'ed25519'))
214
+ and new.FounderOwnerKey = context.OwnerKey
215
+ and new.FounderPeerId is not null)
169
216
  or (
170
217
  new.Type = 'o'
171
218
  and new.MemberPrivateKey is null
172
219
  and new.FounderOwnerKey is null
220
+ and new.FounderPeerId is null
173
221
  and exists (
174
222
  select 1 from FormationUsage FU
175
223
  where FU.StrandId = new.Id
@@ -623,11 +671,13 @@ declare schema CadreControl {
623
671
  -- convergence is guarded by the integration scenario strand-formation-concurrent-redemption.
624
672
  index FormationUsageByToken on FormationUsage (Token);
625
673
 
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
674
+ -- An owner-signed invitation to join this cadre as a machine, redeemable on the owner's
675
+ -- behalf by any member machine (ControlDatabase.redeemCadreInvite). It admits a machine
676
+ -- and grants nothing else; ownership is an OwnerGrant. The invitation is a keypair, not a
628
677
  -- 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
678
+ -- with the private half (CadreInviteUsage.InvitePossessed), so a member that only relays
679
+ -- it can neither admit a machine of its own choosing with it nor spend its uses. Rows are
680
+ -- never deleted: a device admitted
631
681
  -- through one carries no owner signature of its own, and its membership is the chain
632
682
  -- invitation -> usage -> peer row, verified at read time, so the row must outlive every
633
683
  -- device it admitted. Withdrawing an invitation is an owner-signed Revocation tombstone
@@ -640,7 +690,6 @@ declare schema CadreControl {
640
690
  table CadreInvite (
641
691
  Key text primary key, -- the invitation's ed25519 public key (base64url)
642
692
  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
693
  ExpiresAt datetime null,
645
694
  TotalUses int null check (TotalUses >= 0),
646
695
  IssuerKey text, -- the owner that signed the row (== insert context.OwnerKey). A reader checks it
@@ -661,7 +710,6 @@ declare schema CadreControl {
661
710
  'CadreControl.CadreInvite', 'add', party_id(),
662
711
  new.Key,
663
712
  coalesce(new.PeerId, ''),
664
- cast(new.GrantsOwner as text),
665
713
  coalesce(cast(new.ExpiresAt as text), ''),
666
714
  coalesce(cast(new.TotalUses as text), ''),
667
715
  new.StampId
@@ -676,8 +724,7 @@ declare schema CadreControl {
676
724
  ) with context (OwnerKey text, Signature text);
677
725
 
678
726
  -- 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.
727
+ -- CadrePeer row was admitted by consent rather than by an owner signature.
681
728
  table CadreInviteUsage (
682
729
  UsageStampId text primary key, -- nonce minted by the redeeming device and signed into both signatures
683
730
  -- below. The primary key: this table never deletes, so the key alone
@@ -691,22 +738,20 @@ declare schema CadreControl {
691
738
  PeerStampId text, -- the CadrePeer.StampId this usage seats: one row incarnation, not a peer id
692
739
  -- forever. Minted by the writer inside the redeeming transaction and not
693
740
  -- signed (FormationUsage.StrandStampId).
694
- OwnerStampId text null, -- the OwnerKey.StampId this usage seats when the invitation grants ownership
695
741
  InviteSig text, -- by the invitation's private key over the 'redeem' digest: proof of possession
696
742
  PeerSig text, -- by PeerKey over the 'consent' digest: the device agreed to join
697
743
  constraint InsertOnly check on update, delete (false),
698
744
  -- 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).
745
+ -- seated and redeemed in one transaction (the CadrePeer consent branch rests on that).
700
746
  -- 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.
747
+ -- for a single-use machine invitation on different machines may both be admitted until
748
+ -- merge, and after merge both rows stand (FormationUsage.Authorized). Serializing would
749
+ -- need cross-machine coordination the control plane does not have; the mitigations are
750
+ -- the single-use default, the targeted form, and the owner seeing the extra machine and
751
+ -- removing it. Revisit if the control plane gains a cross-machine sequencer. The owner
752
+ -- grant's half of this tradeoff is at OwnerGrantUsage.Authorized. An issuer removed
753
+ -- since issuing takes its outstanding invitations with it (the committed.OwnerKey
754
+ -- clause, which a tombstone alone satisfies); devices it already admitted stay members.
710
755
  constraint Authorized check on insert (
711
756
  exists (
712
757
  select 1 from committed.CadreInvite I
@@ -731,33 +776,143 @@ declare schema CadreControl {
731
776
  ),
732
777
  -- Deferred, so the same-transaction peer insert counts: a usage row can never be held in
733
778
  -- 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.
779
+ -- peer row stores, so the usage names the admitted machine's own key and no other.
736
780
  constraint PeerExists check (exists (
737
781
  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
- )
782
+ ))
743
783
  ) with context (Now datetime);
744
784
 
745
785
  -- Serves the cap count above and the liveness reads (cadre-core countCadreInviteUsage,
746
786
  -- hasLiveCadreInvite) on an append-only table, as FormationUsageByToken does.
747
787
  index CadreInviteUsageByInvite on CadreInviteUsage (InviteKey);
748
788
 
789
+ -- An owner-signed grant of ownership to whoever holds the grant's private half: it seats an
790
+ -- OwnerKey row for a key the holder generates or already holds, and admits no machine (a
791
+ -- machine is admitted by a CadreInvite; peer identity and ownership are unrelated, so the
792
+ -- row names no peer id). The grant is a keypair as the invitation is: the row holds the
793
+ -- public half, the holder proves possession by signing with the private half
794
+ -- (OwnerGrantUsage.GrantPossessed), and the member machine that writes the redemption can
795
+ -- verify that signature but never produce it, so it cannot seat an owner for itself. Rows
796
+ -- are never deleted: a key seated through one carries no owner signature of its own, and
797
+ -- its proof is the chain grant -> usage -> OwnerKey row, verified at read time, so the row
798
+ -- must outlive every key it seated. Withdrawal is an owner-signed Revocation tombstone over
799
+ -- the row's stamp with the row kept (Revocation.RowIsGone); liveness is read as no
800
+ -- tombstone, unexpired, uses left, issuer still an owner (OwnerGrantUsage.Authorized).
801
+ -- NOTE: expired and withdrawn rows accumulate for the life of the party, as CadreInvite's.
802
+ -- The label is the issuer's: it is bound into the grant's signature and the seated OwnerKey
803
+ -- row must carry it (the grant branch of OwnerKey.Authorized), so a key seated on a fresh
804
+ -- device during recovery needs no naming prompt, and nothing the holder writes names it.
805
+ table OwnerGrant (
806
+ Key text primary key, -- the grant's ed25519 public key (base64url)
807
+ Label text null, -- the label the seated OwnerKey row carries, chosen by the issuer
808
+ ExpiresAt datetime null, -- null: never. The writer defaults to a short expiry; a lifetime with
809
+ -- no end is the issuer's explicit choice (a backup grant)
810
+ TotalUses int null check (TotalUses >= 0),
811
+ IssuerKey text, -- the owner that signed the row (== insert context.OwnerKey); a reader checks
812
+ -- it against its node-local anchor, never against the replicated OwnerKey table
813
+ IssuerSig text, -- that owner's 'add' signature (== insert context.Signature), stored so any
814
+ -- reader can re-verify the row from the holder's copy or the replicated one
815
+ StampId text unique,
816
+ -- A withdrawn grant cannot be re-seated from the signed copy its holder keeps, on any
817
+ -- node that holds the tombstone (schema header for the node that does not).
818
+ constraint NotRevoked check on insert (
819
+ not exists (select 1 from Revocation R where R.TableName = 'OwnerGrant' and R.StampId = new.StampId)
820
+ ),
821
+ -- Never empty, as OwnerKey.LabelLength: the add digest binds coalesce(Label, '').
822
+ constraint LabelLength check (
823
+ Label is null or length(Label) between 1 and 64
824
+ ),
825
+ -- The owner signs the whole row, nullable columns as '' (cadre-core ownerGrantRowFields),
826
+ -- and the stored issuer pair must be the pair verified here, as CadreInvite pins its issuer.
827
+ constraint AuthorizedInsert check on insert (
828
+ 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(
829
+ digest(
830
+ 'CadreControl.OwnerGrant', 'add', party_id(),
831
+ new.Key,
832
+ coalesce(new.Label, ''),
833
+ coalesce(cast(new.ExpiresAt as text), ''),
834
+ coalesce(cast(new.TotalUses as text), ''),
835
+ new.StampId
836
+ ),
837
+ context.Signature, A.Key, 'ed25519'))
838
+ and coalesce(new.IssuerKey, '') = context.OwnerKey
839
+ and coalesce(new.IssuerSig, '') = context.Signature
840
+ ),
841
+ constraint NoUpdate check on update (false),
842
+ -- Never deleted (table comment). Withdrawal is the tombstone alone.
843
+ constraint NoDelete check on delete (false)
844
+ ) with context (OwnerKey text, Signature text);
845
+
846
+ -- Append-only record of owner-grant redemptions, one row per redemption: the proof an
847
+ -- OwnerKey row was seated by consent rather than by an owner signature.
848
+ table OwnerGrantUsage (
849
+ UsageStampId text primary key, -- nonce minted by the holder and signed into both signatures below. The
850
+ -- primary key: this table never deletes, so the key alone makes a nonce
851
+ -- permanent and there is no NotRevoked pair here (CadreInviteUsage).
852
+ GrantKey text, -- the OwnerGrant redeemed
853
+ SeatedKey text, -- the owner key seated (base64url ed25519): the holder's, fresh or already held
854
+ SeatedStampId text, -- the OwnerKey.StampId this usage seats: one row incarnation, not a key
855
+ -- forever. Minted by the writer inside the redeeming transaction and not
856
+ -- signed (CadreInviteUsage.PeerStampId).
857
+ GrantSig text, -- by the grant's private key over the 'redeem' digest: proof of possession
858
+ OwnerSig text, -- by SeatedKey over the 'consent' digest: the holder holds the key it seats
859
+ constraint InsertOnly check on update, delete (false),
860
+ -- The grant is read from the pre-transaction snapshot, so a grant cannot be seated and
861
+ -- redeemed in one transaction (the grant branch of OwnerKey.Authorized rests on that).
862
+ -- NOTE: accepted tradeoff — the use cap counts committed usage, so two holders of one
863
+ -- single-use grant redeeming on different machines may both be seated until merge, and
864
+ -- after merge both rows stand (CadreInviteUsage.Authorized). Serializing would need
865
+ -- cross-machine coordination the control plane does not have; the mitigations are the
866
+ -- defaults the writer sets (single use and a 15-minute expiry, a longer lifetime only
867
+ -- when the user chooses one) and the owner seeing the extra key in the owner-key list
868
+ -- and removing it. Revisit if the control plane gains a cross-machine sequencer. An
869
+ -- issuer removed since issuing takes its outstanding grants with it (the
870
+ -- committed.OwnerKey clause, which a tombstone alone satisfies); keys it already
871
+ -- seated stay owners.
872
+ constraint Authorized check on insert (
873
+ exists (
874
+ select 1 from committed.OwnerGrant G
875
+ where G.Key = new.GrantKey
876
+ and not exists (select 1 from Revocation R where R.TableName = 'OwnerGrant' and R.StampId = G.StampId)
877
+ and (G.ExpiresAt is null or G.ExpiresAt > context.Now)
878
+ and (G.TotalUses is null or G.TotalUses > (select count(1) from committed.OwnerGrantUsage U where U.GrantKey = new.GrantKey))
879
+ and exists (select 1 from committed.OwnerKey K where K.Key = G.IssuerKey and not exists (select 1 from Revocation R where R.TableName = 'OwnerKey' and R.StampId = K.StampId))
880
+ )
881
+ ),
882
+ -- Verified against the grant key itself: the member writing the row can check this
883
+ -- signature but, holding only the public half, cannot produce it.
884
+ constraint GrantPossessed check on insert (
885
+ verify(digest('CadreControl.OwnerGrantUsage', 'redeem', party_id(), new.GrantKey, new.UsageStampId, new.SeatedKey),
886
+ new.GrantSig, new.GrantKey, 'ed25519')
887
+ ),
888
+ -- The holder proves it holds the key being seated (CadreInviteUsage.PeerConsented).
889
+ constraint OwnerConsented check on insert (
890
+ verify(digest('CadreControl.OwnerGrantUsage', 'consent', party_id(), new.GrantKey, new.UsageStampId, new.SeatedKey),
891
+ new.OwnerSig, new.SeatedKey, 'ed25519')
892
+ ),
893
+ -- Deferred, so the same-transaction OwnerKey insert counts: a usage row can never be
894
+ -- held in reserve to seat a row later (CadreInviteUsage.PeerExists).
895
+ constraint SeatedExists check (
896
+ exists (select 1 from OwnerKey K where K.Key = new.SeatedKey and K.StampId = new.SeatedStampId)
897
+ )
898
+ ) with context (Now datetime);
899
+
900
+ -- Serves the cap count above and the liveness reads (cadre-core countOwnerGrantUsage,
901
+ -- listOwnerGrantStatuses) on an append-only table, as CadreInviteUsageByInvite does.
902
+ index OwnerGrantUsageByGrant on OwnerGrantUsage (GrantKey);
903
+
749
904
  -- Append-only retirement record for the stamps of removed rows of every guarded table, with
750
905
  -- the removed row's key. Every append is owner-signed, so the growth surface is the owner
751
906
  -- keys. One singleton marker row, ('Revocation', 'ledger', 'opened'), retires nothing: an
752
907
  -- owner's connected reconcile pass files it (ControlDatabase.openRevocationLedger) so this
753
908
  -- table is never a never-written block, which the storage layer would consult the cohort
754
909
  -- 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).
910
+ -- TableName, and queryRevocations skips it. A 'CadreInvite' or 'OwnerGrant' row withdraws
911
+ -- an invitation or a grant whose row stays (the table comments).
757
912
  table Revocation (
758
913
  TableName text, -- 'OwnerKey' | 'ValidationKey' | 'Strand' | 'StrandPartyKey' | 'JoinedStrand' |
759
- -- 'JoinRequest' | 'CadrePeer' | 'DeviceToken' | 'CadreInvite', or 'Revocation'
760
- -- for the marker
914
+ -- 'JoinRequest' | 'CadrePeer' | 'DeviceToken' | 'CadreInvite' | 'OwnerGrant', or
915
+ -- 'Revocation' for the marker
761
916
  RowKey text, -- primary key of the removed row
762
917
  StampId text, -- the retired stamp
763
918
  ReissuedAt integer default 0, -- bumped by an owner-signed re-issue so a tombstone committed while the
@@ -785,9 +940,9 @@ declare schema CadreControl {
785
940
  -- counts). Keyed on the stamp, not RowKey: a node that converges on a re-admission before
786
941
  -- the earlier removal's tombstone must still accept that tombstone. Retiring a stamp that
787
942
  -- 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'.
943
+ -- 'CadreInvite' and 'OwnerGrant' there is no row-gone test: a tombstone there is a
944
+ -- withdrawal, and the row stays (the table comments). The last branch admits the marker
945
+ -- row and nothing else under 'Revocation'.
791
946
  constraint RowIsGone check on insert (
792
947
  (new.TableName = 'OwnerKey' and not exists (select 1 from OwnerKey K where K.StampId = new.StampId))
793
948
  or (new.TableName = 'CadrePeer' and not exists (select 1 from CadrePeer P where P.StampId = new.StampId))
@@ -798,6 +953,7 @@ declare schema CadreControl {
798
953
  or (new.TableName = 'JoinRequest' and not exists (select 1 from JoinRequest J where J.StampId = new.StampId))
799
954
  or (new.TableName = 'DeviceToken' and not exists (select 1 from DeviceToken D where D.StampId = new.StampId))
800
955
  or (new.TableName = 'CadreInvite')
956
+ or (new.TableName = 'OwnerGrant')
801
957
  or (new.TableName = 'Revocation' and new.RowKey = 'ledger' and new.StampId = 'opened')
802
958
  ),
803
959
  -- Owner-signed over the whole row under its own domain tag, so the delete's 'remove'
@@ -822,7 +978,46 @@ declare schema CadreControl {
822
978
  context.Signature, A.Key, 'ed25519'))
823
979
  )
824
980
  ) with context (OwnerKey text, Signature text);
825
- }
981
+ }`;
982
+
983
+ /** The name the control schema is declared under; Quereus compares schema names without regard to case. */
984
+ export const CONTROL_SCHEMA_NAME = 'CadreControl';
985
+
986
+ /** The statement that applies {@link CONTROL_SCHEMA_DECLARATION}, run only after the store has been checked against it. */
987
+ export const APPLY_CONTROL_SCHEMA = `apply schema ${CONTROL_SCHEMA_NAME};`;
988
+
989
+ /** The whole control schema as `schemas/control.qsql` holds it: the declaration, then the apply. */
990
+ export const CONTROL_SCHEMA = `${CONTROL_SCHEMA_DECLARATION}
826
991
 
827
- apply schema CadreControl;
992
+ ${APPLY_CONTROL_SCHEMA}
828
993
  `;
994
+
995
+ /**
996
+ * The declaration statement of a control schema file (`ControlDatabaseConfig.schemaPath`),
997
+ * cut from the file's own text.
998
+ *
999
+ * Refuses a file that is anything other than one `declare schema CadreControl { … }`
1000
+ * followed by one plain `apply schema CadreControl` — the shape of {@link CONTROL_SCHEMA}.
1001
+ * `loadSchema` runs its own {@link APPLY_CONTROL_SCHEMA} once the store has been checked,
1002
+ * so any other statement in the file, or an option on its apply, would be dropped or run
1003
+ * unchecked.
1004
+ */
1005
+ export function controlSchemaFileDeclaration(source: string): string {
1006
+ const statements = new Parser().parseAll(source);
1007
+ const [declaration, apply] = statements;
1008
+ if (statements.length !== 2
1009
+ || declaration.type !== 'declareSchema' || !isControlSchemaName(declaration.schemaName)
1010
+ || apply.type !== 'applySchema' || !isControlSchemaName(apply.schemaName)
1011
+ || apply.options !== undefined || apply.toVersion !== undefined || apply.withSeed === true
1012
+ || declaration.loc === undefined) {
1013
+ throw new Error(
1014
+ `A control schema file must hold exactly \`declare schema ${CONTROL_SCHEMA_NAME} { … }\` ` +
1015
+ `followed by \`${APPLY_CONTROL_SCHEMA}\`, the shape of schemas/control.qsql.`
1016
+ );
1017
+ }
1018
+ return source.slice(declaration.loc.start.offset, declaration.loc.end.offset);
1019
+ }
1020
+
1021
+ function isControlSchemaName(name: string | undefined): boolean {
1022
+ return name?.toLowerCase() === CONTROL_SCHEMA_NAME.toLowerCase();
1023
+ }