@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
package/dist/types.d.ts CHANGED
@@ -8,6 +8,7 @@ import type { StrandRevocationEnforcementConfig } from './strand-revocation-enfo
8
8
  import type { StrandMembershipReconciliationConfig } from './strand-membership-reconciler.js';
9
9
  import type { StrandFirstSyncConfig } from './strand-first-sync-gate.js';
10
10
  import type { StrandDatabase } from './strand-database.js';
11
+ import type { StrandCommitDurability, StrandCommitHeld } from './strand-commit-durability.js';
11
12
  import type { SeedTrustPolicy } from './seed-trust-policy.js';
12
13
  import type { KeyStore, KeyId } from './key-store.js';
13
14
  import type { TrustedOwnerStore, TrustSource } from './trusted-owner-store.js';
@@ -16,9 +17,11 @@ import type { StrandNetworkStateStore } from './strand-network-state.js';
16
17
  import type { EnrolledMachineStore } from './enrolled-machine-store.js';
17
18
  import type { JoinedStrandStore } from './joined-strand-store.js';
18
19
  import type { PushNotifier } from './push-notifier.js';
19
- import type { RevocableTable } from './control-authorization.js';
20
+ import type { CadreInviteSignedFields, RevocableTable } from './control-authorization.js';
21
+ import type { CadreInvitation, CadreInviteAddressOutcome } from './cadre-invite-protocol.js';
20
22
  import type { ControlRetryAbandonment } from './control-retry.js';
21
23
  import type { FormationRejectionCode } from './strand-formation-rejection.js';
24
+ import type { ListenTransportOptions } from './relay-addrs.js';
22
25
  /**
23
26
  * Extended Libp2p node with the coordinatedRepo attached by db-p2p's
24
27
  * createLibp2pNode after node creation (not surfaced on the base Libp2p type).
@@ -368,6 +371,20 @@ export interface NetworkConfig {
368
371
  * ```
369
372
  */
370
373
  transports?: Libp2pTransports;
374
+ /**
375
+ * Builds `@optimystic/db-p2p`'s default transport list, for the one case where cadre has to
376
+ * pass a list itself while {@link transports} is unset: a node with configured relays, which
377
+ * is built with libp2p's relay discovery switched off (`node-transports.ts`). Called with the
378
+ * switches the node's listen entries imply (`wsPort` when one names WebSocket). Ignored when
379
+ * {@link transports} is set.
380
+ *
381
+ * A Node host passes `nodeDefaultTransports` from the Node-only
382
+ * `@serfab/cadre-core/node-default-transports` subpath, as `cadre-cli` does. It is injected
383
+ * rather than built in, because that list names `@libp2p/tcp`, which must stay out of the
384
+ * graph React Native and browser bundles reach. Left unset, such a node is built from
385
+ * db-p2p's own list with relay discovery on.
386
+ */
387
+ defaultTransports?: (listen: ListenTransportOptions) => Libp2pTransports;
371
388
  /**
372
389
  * Crypto primitives for the Noise handshake and the encrypted connection that
373
390
  * follows, threaded to both the control node and every strand's cohort node — every
@@ -534,7 +551,7 @@ export interface NetworkConfig {
534
551
  /**
535
552
  * Limit on dialing ONE peer — all of its candidate addresses together — in
536
553
  * ms. Bounds a reconcile pass at (dialed siblings) × this. The same limit
537
- * applies to the owner dials of `applySeed` and to `dialInvite`. Defaults to
554
+ * applies to the owner dials of `applySeed`. Defaults to
538
555
  * {@link DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS}.
539
556
  */
540
557
  dialTimeoutMs?: number;
@@ -591,6 +608,16 @@ export interface NetworkConfig {
591
608
  * kept it (the same pair aborted at a 200ms stall only when the deadline was 300ms). An
592
609
  * interval strictly above the deadline is what makes the 30 seconds real.
593
610
  *
611
+ * WHY THE OTHER END'S PING HANDLER CAPS IT TOO. The node being pinged echoes through
612
+ * `@libp2p/ping`'s inbound handler, which aborts the ping stream when its own timer fires; the
613
+ * pinging node's monitor sees the reset and aborts the connection exactly as it would on its
614
+ * own timeout. So this deadline is only real up to the handler timeout of the node being
615
+ * pinged, which has to stay above it. The deployed relay sets its handler to 60 s for that
616
+ * reason (`ops/docker/libp2p-infra/src/main.ts`). Every cadre node's handler is Optimystic's
617
+ * bare `ping()`, `@libp2p/ping`'s 10 s, and nothing here can move it
618
+ * (`tickets/blocked/optimystic-ping-handler-timeout-below-the-monitor-deadline.md`): between
619
+ * two cadre nodes, a ping whose echo takes longer than 10 s still drops the connection.
620
+ *
594
621
  * WHAT THE LINK NEEDS OF IT. One ping opens a fresh stream and echoes over it: a protocol
595
622
  * negotiation plus the echo, two link round trips by `link-budget.ts`'s counts (a `newStream`
596
623
  * over a relayed circuit measured 3016 ms at 1500 ms one-way). At the slowest link sereus
@@ -777,7 +804,7 @@ export interface CadreNodeConfig {
777
804
  * should enable it, because a node without it neither announces nor serves as part of the
778
805
  * group a notification is rooted at. Enabling registers Optimystic's change-notification
779
806
  * protocol handlers on the strand node, which the closed-strand per-stream gate does not
780
- * cover, and each watched table pays a cold-start proof of work on every node build. Both
807
+ * cover, and each watched table registers again on every node build. Both
781
808
  * are spelled out in `docs/strands.md` → "Change notifications (reactivity)". The control
782
809
  * node never gets it.
783
810
  */
@@ -860,9 +887,43 @@ export interface CadreNodeConfig {
860
887
  * inside SeedBootstrapService when unset — a node whose node-local trusted-owner
861
888
  * anchor was never seeded then rejects every seed. A per-call
862
889
  * `applySeed(seed, { trustPolicy })` override still wins over this default for
863
- * callers that hold out-of-band material (e.g. a pinned key from a CadreInvite).
890
+ * callers that hold out-of-band material (e.g. an operator-pinned owner key).
864
891
  */
865
892
  seedTrustPolicy?: SeedTrustPolicy;
893
+ /**
894
+ * Start this node waiting to be CLAIMED: it belongs to nobody until someone delivers
895
+ * a seed carrying a proof of this one-time secret (`claim-proof.ts`,
896
+ * `claimSecretTrustPolicy`), and that signer becomes its anchored owner. The node
897
+ * turns its seed listener on itself and, while unclaimed, lets a stranger do nothing
898
+ * but deliver a seed: connections are admitted, control-database streams and relay
899
+ * reservations are refused (`CadreNode.isAwaitingClaim`).
900
+ *
901
+ * A node has exactly one cold-start trust source, so `claim` beside a non-empty
902
+ * `trustedOwners.pinnedKeys` or a `seedTrustPolicy` fails `start()` closed, and
903
+ * `initializeSeedBootstrap` (an owner key) refuses on such a node: a hosted node never
904
+ * holds an owner key. A claimed node restarted with this still set ignores it (its
905
+ * anchor is non-empty), so the operator need not remove it.
906
+ *
907
+ * The claim also decides which party the node serves: `controlNetwork.partyId` on an
908
+ * unclaimed node is a placeholder, and the real party is the claim seed's. The node
909
+ * itself keeps serving the placeholder for the rest of the process — everything it
910
+ * holds is keyed on the party it started with — so `record` is where the embedder
911
+ * learns the claimed party, and it is the embedder's job to restart into it.
912
+ */
913
+ claim?: {
914
+ /** The one-time claim secret, base64url of 32 bytes. Never written to replicated state or logs. */
915
+ secret: string;
916
+ /**
917
+ * Record the claim durably: called with the seed's party and the claimant's key after
918
+ * the proof verifies and BEFORE the claimant is anchored, and awaited. A rejection
919
+ * refuses the claim `claim-not-persisted` with nothing anchored, so the claimant
920
+ * retries. Record first, anchor second: a crash between the two leaves a record and
921
+ * an empty anchor, and an embedder that starts from its record (cadre-cli does)
922
+ * then comes up claimed by that owner, who is accepted idempotently when it retries.
923
+ * The other order would leave an anchored node with no record of its party.
924
+ */
925
+ record: (claim: NodeClaimRecord) => Promise<void>;
926
+ };
866
927
  /**
867
928
  * Node-local trusted-owner anchor (see `trusted-owner-store.ts`): the
868
929
  * NON-replicated, per-party record of owner keys established out of band —
@@ -883,16 +944,18 @@ export interface CadreNodeConfig {
883
944
  /**
884
945
  * Owner keys (base64url ed25519) established out of band and seeded into
885
946
  * the store during start(): operator pins (e.g. cadre-cli
886
- * `--pin-owner-key` / `CADRE_OWNER_KEYS`) or a `CadreInvite.ownerKeys`
887
- * already known at config time. Seeding is idempotent across restarts.
947
+ * `--pin-owner-key` / `CADRE_OWNER_KEYS`). A cadre invitation's `ownerKeys`
948
+ * are pinned at redemption instead (`CadreNode.redeemCadreInvitation`).
949
+ * Seeding is idempotent across restarts.
888
950
  */
889
951
  pinnedKeys?: string[];
890
952
  /**
891
953
  * Provenance recorded for {@link pinnedKeys}. Default: 'operator'.
892
954
  * ('genesis' is reserved for the node's own founding key, seeded
893
- * internally by `initializeSeedBootstrap`.)
955
+ * internally by `initializeSeedBootstrap`; 'chain' is written only by the
956
+ * anchor sync, `CadreNode.syncOwnerAnchor`.)
894
957
  */
895
- pinnedSource?: Exclude<TrustSource, 'genesis'>;
958
+ pinnedSource?: Exclude<TrustSource, 'genesis' | 'chain'>;
896
959
  };
897
960
  /**
898
961
  * Node-local cold-start bootstrap-peer store (see `bootstrap-peer-store.ts`):
@@ -1124,13 +1187,11 @@ export interface StrandRow {
1124
1187
  FounderOwnerKey: string | null;
1125
1188
  }
1126
1189
  /**
1127
- * A `CadreControl.PendingJoin` row: a join this party asked for through another party's
1128
- * invitation, readable by every machine of the party. Each outcome carries only its own
1129
- * columns (the schema's `OutcomeShape` check): `'joined'` sets `StrandId` (and, for a
1130
- * closed strand, `MembershipInvite`), `'failed'` sets `FailureCode`, and a pending row
1131
- * (`Outcome: null`) sets neither.
1190
+ * A `CadreControl.JoinRequest` row: a join this party asked for through another party's
1191
+ * invitation, readable by every machine of the party. The row never changes; its outcome is
1192
+ * a `JoinSuccess` or `JoinFailure` row keyed by `StampId` (see {@link PendingJoin}).
1132
1193
  */
1133
- export interface PendingJoinRow {
1194
+ export interface JoinRequestRow {
1134
1195
  /** base64url sha256 of the invitation token (`pendingJoinId`). */
1135
1196
  Id: string;
1136
1197
  /** `CadreNode.encodeInvitation` of the invitation. A bearer credential. */
@@ -1141,30 +1202,45 @@ export interface PendingJoinRow {
1141
1202
  RequestedAt: number;
1142
1203
  /** Epoch ms; no attempt starts at or after it. */
1143
1204
  ExpiresAt: number;
1144
- Outcome: null | 'joined' | 'failed';
1145
- /** Epoch ms the outcome was recorded; null while pending. */
1146
- OutcomeAt: number | null;
1147
- /** `'joined'`: the strand the formation returned. */
1148
- StrandId: string | null;
1149
- /** `'joined'`, closed strand: JSON of the {@link StrandMembershipInvite} the formation delivered. */
1150
- MembershipInvite: string | null;
1151
- /** `'failed'`: a `FormationRejectionCode`, `'expired'` or `'local'`. Not constrained by the schema. */
1152
- FailureCode: string | null;
1153
- /** `'failed'`: human-readable text. */
1154
- FailureReason: string | null;
1155
- /** Single-use nonce of this row incarnation; `ControlDatabase.replacePendingJoin` names it as the row it replaces. */
1205
+ /** Single-use nonce of this request incarnation; its outcome rows name it. */
1156
1206
  StampId: string;
1157
1207
  }
1208
+ /**
1209
+ * The outcome of a {@link JoinRequestRow}: a `CadreControl.JoinSuccess` row (`'joined'`) or
1210
+ * a `CadreControl.JoinFailure` row (`'failed'`), without the request stamp that keys it.
1211
+ */
1212
+ export type JoinOutcome = {
1213
+ kind: 'joined';
1214
+ /** Epoch ms the outcome was recorded. */
1215
+ RecordedAt: number;
1216
+ /** The strand the formation returned. */
1217
+ StrandId: string;
1218
+ /** Closed strand: JSON of the {@link StrandMembershipInvite} the formation delivered. */
1219
+ MembershipInvite: string | null;
1220
+ } | {
1221
+ kind: 'failed';
1222
+ /** Epoch ms the outcome was recorded. */
1223
+ RecordedAt: number;
1224
+ /** A `FormationRejectionCode`, `'expired'` or `'local'`. Not constrained by the schema. */
1225
+ Code: string;
1226
+ /** Human-readable text. */
1227
+ Reason: string;
1228
+ };
1229
+ /** A join request with its outcome, as `ControlDatabase.queryPendingJoins` reads the three tables. */
1230
+ export interface PendingJoin extends JoinRequestRow {
1231
+ /** Null while pending. */
1232
+ outcome: JoinOutcome | null;
1233
+ }
1158
1234
  /**
1159
1235
  * A join this party asked for through `CadreNode.requestJoin`, as one machine sees it.
1160
1236
  *
1161
- * `pending`, `joined` and `failed` come from the party-wide `PendingJoin` row and read the same
1237
+ * `pending`, `joined` and `failed` come from the party-wide join request and its outcome row and read the same
1162
1238
  * on every machine. `trying` (an attempt is running on this machine) and `waiting` (this
1163
1239
  * machine's last attempt failed in a way worth retrying) are this machine's own view of a
1164
1240
  * pending row, so two machines can report different states for it.
1165
1241
  */
1166
1242
  export interface PendingJoinStatus {
1167
- /** `PendingJoin.Id`: the sha256 of the invitation token. */
1243
+ /** `JoinRequest.Id`: the sha256 of the invitation token. */
1168
1244
  id: string;
1169
1245
  sAppId: string;
1170
1246
  /** Epoch ms. */
@@ -1377,7 +1453,7 @@ export interface StrandMembershipInvite {
1377
1453
  * whoever holds it can `consumeInvite` exactly once. Same sensitivity class and
1378
1454
  * handling as `memberPrivateKey` — delivered only inside the validated,
1379
1455
  * post-approval formation result. The inviting side never writes it to its control
1380
- * DB. The joining side may copy it into its own `PendingJoin` row
1456
+ * DB. The joining side may copy it into its own `JoinSuccess` row
1381
1457
  * (`MembershipInvite`), owner-signed and party-private like
1382
1458
  * `JoinedStrand.MemberPrivateKey`, so a join finished on one machine of the party
1383
1459
  * can be seated by whichever machine launches the strand first.
@@ -1489,6 +1565,24 @@ export interface CadreNodeEvents {
1489
1565
  'strand:writable': {
1490
1566
  strandId: string;
1491
1567
  };
1568
+ /**
1569
+ * Emitted after every commit on a strand's database that wrote something, with whether
1570
+ * another member already holds it (`heldElsewhere`) and whether `strand:commit-held` will
1571
+ * say when it does (`followed`). Synchronous, inside the commit, before the app's statement
1572
+ * returns: keep handlers short. Covers every commit on the strand database, cadre-core's own
1573
+ * membership writes included, so filter by the ids you hold — the `StampId()` a
1574
+ * `returning StampId() as Tx` statement gave you. The same record is readable afterwards with
1575
+ * `StrandDatabase.commitDurability(tx)`. See docs/strands.md, "A commit's durability".
1576
+ */
1577
+ 'strand:commit-durability': StrandCommitDurability;
1578
+ /**
1579
+ * Emitted once when a commit reported `followed` below full replication becomes held by every
1580
+ * member of its cohort — the moment a write made while the others were away reaches them.
1581
+ * Synchronous, like `strand:commit-durability`, and can come right after it inside the same
1582
+ * commit. Never emitted for a commit made before your listener attached, or one forgotten
1583
+ * because its database closed, hibernated or the node restarted.
1584
+ */
1585
+ 'strand:commit-held': StrandCommitHeld;
1492
1586
  /**
1493
1587
  * Emitted when this node discovers it is no longer a member of a CLOSED
1494
1588
  * strand — its party was removed by a manager, or it left. Detected by the
@@ -1627,6 +1721,30 @@ export interface CadreNodeEvents {
1627
1721
  partyId: string;
1628
1722
  error: string;
1629
1723
  };
1724
+ /**
1725
+ * Emitted once on a node started with `CadreNodeConfig.claim`, when a seed carrying a
1726
+ * valid claim proof has been recorded (`claim.record`) and has durably anchored its
1727
+ * signer as this node's owner. Carries the claim's party beside the owner key: the
1728
+ * party this node should serve from now on, which its own config did not know. Fires
1729
+ * BEFORE the seed is acknowledged to the claimant, its peers merged and its owners
1730
+ * dialed, so it precedes that seed's `seed:applied`; an embedder that restarts into
1731
+ * the claimed party on this event should wait for that `seed:applied` (or `seed:error`)
1732
+ * so the acknowledgement is not lost with the node. `CadreNode.isAwaitingClaim` is the
1733
+ * matching predicate for a reader that subscribed too late to see the event.
1734
+ */
1735
+ 'claim:accepted': NodeClaimRecord;
1736
+ /**
1737
+ * Emitted once per node object when this node learns that an owner removed it from the
1738
+ * cadre: a `CadrePeer` tombstone names its peer id, it has no live `CadrePeer` row, and a
1739
+ * membership refresh of this process had seen its live row before. `CadreNode.removedFromCadre`
1740
+ * is the matching query for a reader that subscribed too late.
1741
+ *
1742
+ * BEST-EFFORT: it fires only if the tombstone reached this node before the other machines
1743
+ * cut it off, so a machine that was offline at removal time usually never sees it. Nothing
1744
+ * is stopped or deleted for you; the node only stops its owner re-issue drains, which can no
1745
+ * longer succeed. Stopping the node and deleting the cadre's data are the embedder's.
1746
+ */
1747
+ 'cadre:removed': CadreRemoval;
1630
1748
  }
1631
1749
  /**
1632
1750
  * A peer's self-published, freshness-stamped, self-signed address record.
@@ -1637,8 +1755,8 @@ export interface CadreNodeEvents {
1637
1755
  * PeerId by reading the record, re-verifying the self-signature against
1638
1756
  * `publicKey`, checking freshness, and applying a trust gate.
1639
1757
  *
1640
- * The signature covers `peerId`, the comma-joined `addrs`, and `updatedAt` — see
1641
- * `peerRecordSignedPayload`. `publicKey` is NOT inside the signed payload: it is
1758
+ * The signature covers `peerId`, the comma-joined `addrs`, `updatedAt` and `profile` —
1759
+ * see `peerRecordSignedPayload`. `publicKey` is NOT inside the signed payload: it is
1642
1760
  * the key the signature is verified *with*, and its libp2p-identity binding to
1643
1761
  * `peerId` is checked separately at resolve time, so re-signing it would be
1644
1762
  * redundant.
@@ -1652,34 +1770,244 @@ export interface PeerAddressRecord {
1652
1770
  addrs: string[];
1653
1771
  /** epoch ms — freshness; strictly increasing per peer. */
1654
1772
  updatedAt: number;
1773
+ /**
1774
+ * The publisher's own {@link CadreNodeConfig.profile}: `'transaction'` is an app (phone,
1775
+ * browser), `'storage'` an always-on node. Null on a row the machine has not yet
1776
+ * self-published (an owner or an invitation seated it).
1777
+ */
1778
+ profile: NodeProfile | null;
1655
1779
  /** ed25519 self-signature over the signed payload, base64url. */
1656
1780
  sig: string;
1657
1781
  }
1658
1782
  /**
1659
1783
  * One `CadreControl.CadrePeer` row as read by `ControlDatabase.queryCadrePeers`:
1660
- * the addressing columns plus the persisted membership voucher.
1784
+ * the addressing columns plus the persisted membership proof.
1661
1785
  *
1662
- * The voucher triple (`stampId`, `vouchOwner`, `vouchSig`) is what
1786
+ * A row carries exactly one proof. An owner-vouched row has `vouchSig`, the owner's
1787
+ * signature over the row's (`peerId`, `stampId`), which
1663
1788
  * {@link CadreNode.listAuthorizedMembers} re-checks against the node-local
1664
- * trusted-owner anchor — it is null on a row written before the voucher columns
1665
- * existed, and all three must be present for the row to be authorizable. Named
1666
- * (rather than restated inline at each read site) so a future column addition
1667
- * cannot reach the query without the predicate seeing it.
1789
+ * trusted-owner anchor. An invitation-admitted row has `vouchSig` null and
1790
+ * `vouchUsage` naming the `CadreInviteUsage` row that admitted it; the reader
1791
+ * verifies the chain row → usage → invitation instead (`verifyInvitationAdmission`),
1792
+ * with `vouchOwner` the invitation's issuer. All columns are null on a row written
1793
+ * before they existed, and such a row is never authorizable. Named (rather than
1794
+ * restated inline at each read site) so a future column addition cannot reach the
1795
+ * query without the predicate seeing it.
1668
1796
  */
1669
1797
  export interface CadrePeerRow {
1670
1798
  /** libp2p peer ID (base58btc) — the row key. */
1671
1799
  peerId: string;
1800
+ /** ed25519 public key (base64url) stored for the peer, or null (not ed25519, or a row written before the column). */
1801
+ publicKey: string | null;
1672
1802
  /** Comma-joined dialable multiaddrs as stored, or null when unpublished. */
1673
1803
  multiaddr: string | null;
1674
- /** Single-use anti-replay nonce the voucher signature is bound to. */
1804
+ /** The machine's freshness stamp (epoch ms); 0 when it has never self-published. */
1805
+ updatedAt: number;
1806
+ /** The machine's self-declared node profile; null until it self-publishes. */
1807
+ profile: NodeProfile | null;
1808
+ /** The machine's self-signature over its address record (`peerRecordSignedPayload`); null until it self-publishes. */
1809
+ sig: string | null;
1810
+ /** Single-use anti-replay nonce the voucher signature, or the usage row, is bound to. */
1675
1811
  stampId: string | null;
1676
- /** ed25519 public key (base64url) of the owner that vouched this row. */
1812
+ /** ed25519 public key (base64url) of the owner that vouched this row, or that issued the invitation that admitted it. */
1677
1813
  vouchOwner: string | null;
1678
- /** That owner's signature over `digest('CadreControl.CadrePeer', 'vouch', peerId, stampId)`, base64url. */
1814
+ /** That owner's signature over `digest('CadreControl.CadrePeer', 'vouch', partyId, peerId, stampId)`, base64url; null on an invitation-admitted row. */
1679
1815
  vouchSig: string | null;
1816
+ /** The `CadreInviteUsage.UsageStampId` that admitted this row; null on an owner-vouched row. */
1817
+ vouchUsage: string | null;
1818
+ }
1819
+ /** The proof-bearing subset of a {@link CadrePeerRow} — what the authorized-membership predicate reads. */
1820
+ export type CadrePeerVoucherFields = Pick<CadrePeerRow, 'peerId' | 'stampId' | 'vouchOwner' | 'vouchSig' | 'vouchUsage'>;
1821
+ /**
1822
+ * One live `CadreControl.OwnerKey` row as `ControlDatabase.queryOwnerKeyRows` reads it: the
1823
+ * key and the persisted proof of how it was seated. The three proof shapes are the schema's
1824
+ * (`OwnerKey.Authorized`): the founding row (all three null), an owner-signed add (`vouchOwner`
1825
+ * and `vouchSig`, the signer's signature over `ownerKeyAddDigest(key, stampId)`), or an
1826
+ * owner-granting invitation redeemed by consent (`vouchOwner` the issuer, `vouchSig` null,
1827
+ * `vouchUsage` the `CadreInviteUsage` row). Rows whose stamp is retired in `Revocation`, and
1828
+ * the stampless row the schema permits but no writer produces, never reach a reader, so
1829
+ * `stampId` is non-null here. The anchor sync (`deriveOwnerAnchor`) judges
1830
+ * the proof against the keys it already trusts, never against the table itself.
1831
+ */
1832
+ export interface OwnerKeyRow {
1833
+ /** ed25519 public key (base64url) — the row key. */
1834
+ key: string;
1835
+ /** Single-use anti-replay nonce the voucher signature, or the usage row, is bound to. */
1836
+ stampId: string;
1837
+ /** The owner that signed the add, or that issued the invitation that admitted it; null on the founding row. */
1838
+ vouchOwner: string | null;
1839
+ /** That owner's signature over the row's `'add'` digest, base64url; null on the founding row and on an invitation-admitted row. */
1840
+ vouchSig: string | null;
1841
+ /** The `CadreInviteUsage.UsageStampId` that admitted this row; null otherwise. */
1842
+ vouchUsage: string | null;
1843
+ }
1844
+ /**
1845
+ * One `CadreControl.CadreInvite` row: an owner-signed invitation to join the cadre,
1846
+ * as `ControlDatabase.insertCadreInvite` returns it and `queryCadreInvite` reads it.
1847
+ * Every column is here because the row travels: the holder carries it inside the
1848
+ * invitation bundle and a member re-seats it verbatim (`seatCadreInvite`), so
1849
+ * `expiresAt` is the engine-canonical `datetime` string the row stores, not epoch
1850
+ * ms — the `'add'` signature covers that exact text. The signed subset is
1851
+ * `CadreInviteSignedFields` (`control-authorization.ts`).
1852
+ */
1853
+ export interface CadreInviteRow extends CadreInviteSignedFields {
1854
+ /** The owner that signed the row (`IssuerKey`, base64url ed25519); a reader checks it against its anchor. */
1855
+ issuerKey: string;
1856
+ /** That owner's `'add'` signature over the row (`IssuerSig`, base64url). */
1857
+ issuerSig: string;
1858
+ }
1859
+ /**
1860
+ * One `CadreControl.CadreInviteUsage` row: a redemption of a {@link CadreInviteRow},
1861
+ * as `ControlDatabase.queryCadreInviteUsages` reads it. The two signatures are
1862
+ * stored so any reader can re-verify the admission chain (`verifyInvitationAdmission`).
1863
+ */
1864
+ export interface CadreInviteUsageRow {
1865
+ /** The redemption's single-use nonce (the row key), signed into both signatures. */
1866
+ usageStampId: string;
1867
+ /** The `CadreInvite.Key` redeemed. */
1868
+ inviteKey: string;
1869
+ /** The admitted device's libp2p peer id — the identity multihash of `peerKey`. */
1870
+ peerId: string;
1871
+ /** The admitted device's ed25519 public key (base64url). */
1872
+ peerKey: string;
1873
+ /** The `CadrePeer.StampId` this redemption seated (or found seated). */
1874
+ peerStampId: string;
1875
+ /** The `OwnerKey.StampId` this redemption seated, when the invitation grants ownership; else null. */
1876
+ ownerStampId: string | null;
1877
+ /** The invitation private key's signature over the `'redeem'` digest, base64url. */
1878
+ inviteSig: string;
1879
+ /** The device's signature over the `'consent'` digest, base64url. */
1880
+ peerSig: string;
1881
+ }
1882
+ /**
1883
+ * The usage and invitation rows an invitation-admitted row is verified through, keyed by
1884
+ * `UsageStampId` and `CadreInvite.Key`: a `CadrePeer` row through `verifyInvitationAdmission`
1885
+ * (`CadreNode.listAuthorizedMembers`) and an `OwnerKey` row through
1886
+ * `verifyInvitationOwnerAdmission` (`deriveOwnerAnchor`). Loaded once per membership refresh
1887
+ * (`CadreNode.loadInvitationChain`) and shared by both readers.
1888
+ */
1889
+ export interface InvitationChain {
1890
+ usages: Map<string, CadreInviteUsageRow>;
1891
+ invites: Map<string, CadreInviteRow>;
1892
+ }
1893
+ /**
1894
+ * One machine of the cadre as `CadreNode.listMachines` reports it: a live `CadrePeer` row,
1895
+ * or a live `OwnerKey` whose key derives a peer id with no `CadrePeer` row (an owner key
1896
+ * seated for a machine not yet admitted, or a founder that never self-published).
1897
+ *
1898
+ * `profile` and `lastPublishedAt` are the machine's own claim only when the row's
1899
+ * self-signature verifies; both are null otherwise, so an owner re-vouch that bumped
1900
+ * `UpdatedAt` without re-signing, or a row the machine never published, reports nothing
1901
+ * rather than someone else's stamp.
1902
+ */
1903
+ export interface CadreMachine {
1904
+ peerId: string;
1905
+ /** ed25519 public key (base64url) behind `peerId`; null when the id is not ed25519. */
1906
+ publicKey: string | null;
1907
+ /** This node itself. */
1908
+ isSelf: boolean;
1909
+ /** `publicKey` is a live `OwnerKey` here. */
1910
+ isOwner: boolean;
1911
+ /** Has a live `CadrePeer` row; false for an owner key with no machine row. */
1912
+ isMember: boolean;
1913
+ /** Passes this node's authorized-membership predicate now (`CadreNode.listAuthorizedMembers`; always false for self, which that set excludes). */
1914
+ trusted: boolean;
1915
+ /** The machine's self-published node profile; null until it publishes, or when its signature does not verify. */
1916
+ profile: NodeProfile | null;
1917
+ /** `UpdatedAt` of its self-signed record, re-stamped while it runs; null on the same terms as `profile`. */
1918
+ lastPublishedAt: number | null;
1919
+ /** `CadrePeer.VouchOwner` (or `OwnerKey.VouchOwner` for an owner-only entry): the owner that vouched it, or the issuer of the invitation that admitted it. */
1920
+ vouchedBy: string | null;
1921
+ }
1922
+ /** Why `CadreNode.removeMachine` refuses by name (`MachineRemovalRefusedError.code`). */
1923
+ export type MachineRemovalRefusal =
1924
+ /** This node holds no enrolled owner key, so it cannot sign the removal. */
1925
+ 'not-an-owner'
1926
+ /** The machine is this node: an owner cannot sign its own `OwnerKey` removal, so ask another owner. */
1927
+ | 'self'
1928
+ /** The machine holds the only live owner key here (`MinOneOwner`). */
1929
+ | 'last-owner';
1930
+ /**
1931
+ * What `CadreNode.removeMachine` would do for one machine, read without writing
1932
+ * (`CadreNode.planMachineRemoval`), for an app's confirmation step.
1933
+ */
1934
+ export interface MachineRemovalPlan {
1935
+ /** The machine as listed, or null when no live row names it here (already removed). */
1936
+ machine: CadreMachine | null;
1937
+ refusal: MachineRemovalRefusal | null;
1938
+ /** The machine's key is a live owner key, so the removal also retires it. */
1939
+ removesOwnerKey: boolean;
1940
+ /** Rows the machine's owner key vouched, which stop passing the membership predicate everywhere once it is removed unless re-vouched first. Empty unless `removesOwnerKey`. */
1941
+ dependents: {
1942
+ /** Live `OwnerKey` rows whose `VouchOwner` is the machine's key, this node's own excluded. */
1943
+ owners: string[];
1944
+ /** Live `CadrePeer` rows whose `VouchOwner` is the machine's key, the machine itself and this node excluded. */
1945
+ peers: string[];
1946
+ };
1947
+ /** Live `CadreInvite` keys the machine could redeem to come back: targeted at it, or already redeemed by it with uses left. */
1948
+ invitations: string[];
1949
+ /** This node's own `OwnerKey` or `CadrePeer` row was vouched by the machine. It cannot re-vouch its own rows; another owner has to re-add it afterwards. */
1950
+ ownDependency: boolean;
1951
+ }
1952
+ /** Options for `CadreNode.removeMachine`. */
1953
+ export interface RemoveMachineOptions {
1954
+ /** Dependents to re-vouch under this node's key before the removal; each must appear in the plan's `dependents`. */
1955
+ revouch?: {
1956
+ owners?: readonly string[];
1957
+ peers?: readonly string[];
1958
+ };
1959
+ }
1960
+ /** What `CadreNode.removeMachine` wrote. All false and empty when no live row named the machine here. */
1961
+ export interface MachineRemovalResult {
1962
+ removedPeer: boolean;
1963
+ removedOwnerKey: boolean;
1964
+ removedDeviceToken: boolean;
1965
+ /** `CadreInvite` keys whose withdrawal tombstone this call filed. */
1966
+ withdrawnInvitations: string[];
1967
+ /** Dependents re-vouched under this node's key before the removal. */
1968
+ revouched: {
1969
+ owners: string[];
1970
+ peers: string[];
1971
+ };
1972
+ }
1973
+ /**
1974
+ * This node's own removal from its cadre, as it learned it: the `CadrePeer` tombstone naming
1975
+ * its peer id, read after it had seen its own live row (`CadreNode.removedFromCadre`, the
1976
+ * `cadre:removed` event; "Removing a machine" in docs/architecture.md).
1977
+ */
1978
+ export interface CadreRemoval {
1979
+ /** The cadre this node was removed from (`CadreNode.partyId`). */
1980
+ partyId: string;
1981
+ /** The `StampId` the tombstone retired. */
1982
+ stampId: string;
1983
+ /** The owner key that filed the tombstone, when its signature verifies and the key is in this node's trusted-owner anchor; null otherwise. */
1984
+ signerKey: string | null;
1985
+ /** When this node learned of the removal (epoch ms). */
1986
+ at: number;
1987
+ }
1988
+ /**
1989
+ * One {@link CadreInviteRow} with its standing, as `ControlDatabase.listCadreInviteStatuses`
1990
+ * (and `CadreNode.listCadreInvitations`) reports it. `live` is the redemption's own test:
1991
+ * not withdrawn, unexpired, uses left, issuer still an owner here.
1992
+ */
1993
+ export interface CadreInviteStatus {
1994
+ invite: CadreInviteRow;
1995
+ live: boolean;
1996
+ withdrawn: boolean;
1997
+ /** Redemptions this node holds for it (`CadreInviteUsage` rows). */
1998
+ usesRecorded: number;
1999
+ }
2000
+ /**
2001
+ * What `ControlDatabase.redeemCadreInvite` did. `alreadyMember` is the idempotent
2002
+ * retry after a dropped reply: the device's row (and, for an owner-granting
2003
+ * invitation, its `OwnerKey` row) was already present, so nothing was written and
2004
+ * no seat was spent. The stamps are the live rows' either way.
2005
+ */
2006
+ export interface CadreInviteRedemptionResult {
2007
+ alreadyMember: boolean;
2008
+ peerStampId: string;
2009
+ ownerStampId: string | null;
1680
2010
  }
1681
- /** The voucher-bearing subset of a {@link CadrePeerRow} — what the authorized-membership predicate reads. */
1682
- export type CadrePeerVoucherFields = Pick<CadrePeerRow, 'peerId' | 'stampId' | 'vouchOwner' | 'vouchSig'>;
1683
2011
  /**
1684
2012
  * One `CadreControl.Revocation` tombstone as read by
1685
2013
  * `ControlDatabase.queryRevocations`: the identity triple plus the `ReissuedAt`
@@ -1697,6 +2025,10 @@ export interface RevocationRow {
1697
2025
  stampId: string;
1698
2026
  /** Monotonic re-issue counter; carries no semantics (see the schema comment). */
1699
2027
  reissuedAt: number;
2028
+ /** The owner key that filed the tombstone (`Revocation.SignerKey`). */
2029
+ signerKey: string;
2030
+ /** That owner's signature over the tombstone's `'remove'` digest (`Revocation.SignerSig`). */
2031
+ signerSig: string;
1700
2032
  }
1701
2033
  /**
1702
2034
  * Outcome of `ControlDatabase.openRevocationLedger`: `'opened'` when this call filed the
@@ -1907,6 +2239,47 @@ export interface SeedMessage {
1907
2239
  signature: string;
1908
2240
  /** The owner key that signed this message */
1909
2241
  signerKey: string;
2242
+ /**
2243
+ * Proof that the sender holds the receiving node's one-time claim secret
2244
+ * (`claim-proof.ts`), sent only when claiming a brand-new node. Rides beside
2245
+ * the seed rather than inside `ControlNetworkSeed`: it is not part of what the
2246
+ * owner signs, and a claimed node never needs it again.
2247
+ *
2248
+ * The protocol id stays `/sereus/seed/1.0.0` and no version field is added:
2249
+ * there is no backwards compatibility yet, a receiver without this feature
2250
+ * ignores the field and refuses by its anchored policy, and a sender without it
2251
+ * gets a definite refusal (`claim-proof-invalid`) from an unclaimed node.
2252
+ */
2253
+ claimProof?: string;
2254
+ }
2255
+ /**
2256
+ * Why a seed was refused, for callers that must branch on the cause without
2257
+ * parsing the human-readable `reason`. The phone claiming a node needs to tell
2258
+ * "someone else already claimed it" from "wrong secret".
2259
+ *
2260
+ * - `already-claimed`: the node's anchor already holds an owner key other than
2261
+ * the signer's; the claim secret is no longer consulted.
2262
+ * - `claim-proof-invalid`: the node is unclaimed and the seed carried no claim
2263
+ * proof, or a proof that does not verify.
2264
+ * - `claim-rate-limited`: too many failed proofs recently; the proof was not
2265
+ * verified. Retry after the window.
2266
+ * - `claim-not-persisted`: the proof verified but the node could not durably
2267
+ * record the claim; nothing was anchored, so the same claim may be retried.
2268
+ */
2269
+ export type SeedRefusalCode = 'already-claimed' | 'claim-proof-invalid' | 'claim-rate-limited' | 'claim-not-persisted';
2270
+ /**
2271
+ * What a claim settles for the node that accepted it: the party it serves from now on
2272
+ * and the owner it belongs to. Both come from the claim seed — the party is the seed's
2273
+ * `partyId`, the owner the key that signed it — because an unclaimed node's own
2274
+ * configured party is a placeholder; nobody could know the real one before the claim.
2275
+ * Handed to `CadreNodeConfig.claim.record` before the owner is anchored, and carried by
2276
+ * `claim:accepted`.
2277
+ */
2278
+ export interface NodeClaimRecord {
2279
+ /** The party the claim seed named: the cadre this node now serves. */
2280
+ partyId: string;
2281
+ /** The claimant's ed25519 owner key (base64url): the key the claim anchors. */
2282
+ ownerKey: string;
1910
2283
  }
1911
2284
  /**
1912
2285
  * Acknowledgment message from new node to instigator.
@@ -1916,6 +2289,8 @@ export interface SeedAckMessage {
1916
2289
  accepted: boolean;
1917
2290
  /** Reason for rejection (if not accepted) */
1918
2291
  reason?: string;
2292
+ /** Machine-readable refusal cause, when the refusing policy supplies one. */
2293
+ code?: SeedRefusalCode;
1919
2294
  }
1920
2295
  /**
1921
2296
  * Request sent over the control network's `WAKE_PROTOCOL` from a same-cadre peer
@@ -1968,12 +2343,14 @@ export interface StrandAddrRequest {
1968
2343
  *
1969
2344
  * - `ok` — looked up; `multiaddrs` is the truth, possibly empty (strand not running here).
1970
2345
  * - `unavailable` — the responder could not answer: over its concurrency cap, the
1971
- * request was unreadable, or its own lookup threw (e.g. a control-database read failed).
1972
- * - `refused` — the requester is not an authorized member in the responder's
1973
- * current view (possibly because its `CadrePeer` row has not replicated there yet);
1974
- * any `delegatePeerId` it carried was NOT recorded.
2346
+ * request was unreadable, or its own lookup threw.
2347
+ *
2348
+ * A requester the responder does not count as an authorized member (possibly because
2349
+ * its `CadrePeer` row has not replicated there yet) gets no response at all: the
2350
+ * responder's protocol guard resets the stream, and any `delegatePeerId` it carried
2351
+ * is not recorded.
1975
2352
  */
1976
- export type StrandAddrStatus = 'ok' | 'unavailable' | 'refused';
2353
+ export type StrandAddrStatus = 'ok' | 'unavailable';
1977
2354
  /**
1978
2355
  * Response to a {@link StrandAddrRequest}, carrying the responder's strand-node
1979
2356
  * multiaddrs, returned on the same stream.
@@ -1998,6 +2375,17 @@ export interface AuthorizePeerOptions {
1998
2375
  /** Optional multiaddrs for the peer */
1999
2376
  multiaddrs?: string[];
2000
2377
  }
2378
+ /**
2379
+ * Where `deliverSeed` sends a seed: one multiaddr string, dialed as given, or a
2380
+ * peer id with its known addresses. The object form merges the addresses into
2381
+ * the peer store and dials by peer id, so an existing connection is reused and
2382
+ * libp2p tries every address under the one delivery deadline. It is what a phone
2383
+ * uses to claim a node from the addresses it scanned.
2384
+ */
2385
+ export type SeedDeliveryTarget = string | {
2386
+ peerId: string;
2387
+ multiaddrs: string[];
2388
+ };
2001
2389
  /**
2002
2390
  * Result of applying a seed to a node.
2003
2391
  */
@@ -2008,6 +2396,8 @@ export interface ApplySeedResult {
2008
2396
  peersAdded: number;
2009
2397
  /** Error message if not successful */
2010
2398
  error?: string;
2399
+ /** Machine-readable refusal cause, when the refusing trust policy supplied one. */
2400
+ code?: SeedRefusalCode;
2011
2401
  /**
2012
2402
  * Owner-flagged seed peers this apply attempted to dial. Zero when the seed
2013
2403
  * carried no owner peer with an address, or when the seed was rejected before
@@ -2023,7 +2413,8 @@ export interface ApplySeedResult {
2023
2413
  * `CadreNode.dialColdStartBootstrap`), so this is a signal, not a fatal error.
2024
2414
  *
2025
2415
  * Zero failures is NOT proof of a live connection: the receiving node's
2026
- * membership gate denies AFTER the dialer's upgrade completes (see
2416
+ * connection gate decides AFTER the dialer's upgrade completes, and closes a
2417
+ * provisionally admitted connection at its deadline (see
2027
2418
  * `createMembershipConnectionGater`), so a dial can resolve and then be torn
2028
2419
  * down. Treat a non-zero value as certain failure and zero as "no failure
2029
2420
  * observed".
@@ -2035,30 +2426,80 @@ export interface ApplySeedResult {
2035
2426
  */
2036
2427
  export type NodeTopology = 'public' | 'nat';
2037
2428
  /**
2038
- * Invite code for phone-to-server enrollment.
2039
- * Used when a server invites a phone to join the cadre.
2429
+ * What an owner asks `CadreNode.createCadreInvitation` for. The invitation is a keypair the
2430
+ * node mints; whoever holds the returned bundle can redeem it at any member machine.
2040
2431
  */
2041
- export interface CadreInvite {
2042
- /** Party ID of the cadre */
2043
- partyId: string;
2044
- /** Multiaddrs to dial the owner */
2045
- ownerAddrs: string[];
2046
- /**
2047
- * Owner ed25519 public keys (base64url) of the cadre, carried out-of-band
2048
- * so a cold-start invitee can pin the trusted owner set before applying
2049
- * any seed (the seed itself cannot vouch for its own signer). Populated by
2050
- * `createInvite` from the issuer's OWN node-local trusted-owner anchor —
2051
- * not its replicated `OwnerKey` table, because the invitee anchors whatever
2052
- * arrives here and a stranger's genesis-inserted key must not ride an
2053
- * otherwise-legitimate invite into a fresh node's anchor.
2054
- */
2055
- ownerKeys?: string[];
2056
- /** Optional invite token for validation */
2057
- token?: string;
2058
- /** Timestamp when invite was created */
2059
- createdAt: number;
2060
- /** Optional expiration timestamp */
2061
- expiresAt?: number;
2432
+ export interface CreateCadreInvitationOptions {
2433
+ /** The one device that may redeem it; omit for whoever redeems first. */
2434
+ peerId?: string;
2435
+ /** Whether redemption also makes the device an owner (seats an `OwnerKey` row). */
2436
+ grantsOwner: boolean;
2437
+ /**
2438
+ * Lifetime from now, in ms. Defaults to 15 minutes for an untargeted owner-granting
2439
+ * invitation — a bearer credential for admin rights — and 24 hours otherwise. A negative
2440
+ * value mints an already-expired invitation (tests).
2441
+ */
2442
+ expiresInMs?: number;
2443
+ /** How many redemptions it allows; default 1. */
2444
+ uses?: number;
2445
+ }
2446
+ /**
2447
+ * Where a phone could redeem a cadre invitation from, judged on the addresses of machines
2448
+ * other than the one that minted it — the answer to "can the new app join once this one is
2449
+ * closed?". A minting machine that is itself always on can therefore read `issuer-only`.
2450
+ */
2451
+ export type CadreInvitationReach =
2452
+ /** Another machine has a relayed (`/p2p-circuit`), public-name or public-IP WebSocket address. */
2453
+ 'anywhere'
2454
+ /** Other machines are named, but only at private or loopback addresses: works on their network only. */
2455
+ | 'local-network'
2456
+ /** The bundle names no other machine at an address a phone can dial. */
2457
+ | 'issuer-only';
2458
+ /** What `CadreNode.createCadreInvitation` returns: the bundle, and the same encoded for sharing. */
2459
+ export interface CreateCadreInvitationResult {
2460
+ invitation: CadreInvitation;
2461
+ /** `encodeCadreInvitation(invitation)`: `sereus-invite:1.` and base64url JSON, for a QR code, a paste or a link. */
2462
+ encoded: string;
2463
+ /** Whether a device could redeem it once the minting machine is gone; see {@link CadreInvitationReach}. */
2464
+ reach: CadreInvitationReach;
2465
+ }
2466
+ /** How `CadreNode.redeemCadreInvitation` runs. */
2467
+ export interface RedeemCadreInvitationOptions {
2468
+ /**
2469
+ * Keep making passes over the bundle's addresses while every answer is retryable, until a
2470
+ * member accepts, one refuses finally, the invitation expires, or `signal` aborts.
2471
+ * Default false: one pass.
2472
+ */
2473
+ waitForMember?: boolean;
2474
+ /** Ends the pause and the pass in flight; the call then throws the signal's reason. */
2475
+ signal?: AbortSignal;
2476
+ /** Called at each pass and each pause; an exception it throws is logged and ignored. */
2477
+ onProgress?: (progress: CadreJoinProgress) => void;
2478
+ }
2479
+ /** A redemption's progress, as `RedeemCadreInvitationOptions.onProgress` reports it. */
2480
+ export type CadreJoinProgress = {
2481
+ state: 'redeeming';
2482
+ pass: number;
2483
+ } | {
2484
+ state: 'waiting';
2485
+ pass: number;
2486
+ /** Epoch ms of the next pass. */
2487
+ retryAtMs: number;
2488
+ /**
2489
+ * `'no-member-answered'`: every address failed to dial or answer. `'member-not-ready'`: at
2490
+ * least one member answered with a retryable refusal (issuer-unknown, busy, conflict, internal).
2491
+ */
2492
+ reason: 'no-member-answered' | 'member-not-ready';
2493
+ outcomes: readonly CadreInviteAddressOutcome[];
2494
+ };
2495
+ /** What `CadreNode.redeemCadreInvitation` returns once a member accepted. */
2496
+ export interface RedeemCadreInvitationResult {
2497
+ /** The member machine that admitted this device, when its address named one. */
2498
+ peerId: string | null;
2499
+ /** Whether the invitation also seated this device's key as an owner. */
2500
+ grantsOwner: boolean;
2501
+ /** When the member accepted, ISO 8601. */
2502
+ redeemedAt: string;
2062
2503
  }
2063
2504
  /**
2064
2505
  * Options for adding a drone via provider API.
@@ -2069,15 +2510,6 @@ export interface AddDroneOptions {
2069
2510
  /** Multiaddrs of the drone (returned from provider) */
2070
2511
  droneMultiaddrs: string[];
2071
2512
  }
2072
- /**
2073
- * Options for adding a phone via invite flow.
2074
- */
2075
- export interface AddPhoneOptions {
2076
- /** Peer ID of the phone (sent by phone when it connects) */
2077
- phonePeerId: string;
2078
- /** Invite token for validation (must match issued invite) */
2079
- token?: string;
2080
- }
2081
2513
  /**
2082
2514
  * Result of preparing a seed for drone initialization.
2083
2515
  */
@@ -2087,12 +2519,3 @@ export interface DroneInitResult {
2087
2519
  /** Base64url encoded seed for transport */
2088
2520
  encodedSeed: string;
2089
2521
  }
2090
- /**
2091
- * Result of creating an invite for a phone.
2092
- */
2093
- export interface InviteResult {
2094
- /** The invite to share out-of-band */
2095
- invite: CadreInvite;
2096
- /** Base64url encoded invite for QR/link */
2097
- encodedInvite: string;
2098
- }