@serfab/cadre-core 0.13.0 → 1.1.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 (131) hide show
  1. package/README.md +20 -6
  2. package/dist/bootstrap-peer-store-file.js +2 -2
  3. package/dist/bootstrap-peer-store.d.ts +32 -12
  4. package/dist/bootstrap-peer-store.js +41 -15
  5. package/dist/bootstrap-peer-store.js.map +1 -1
  6. package/dist/cadre-node.d.ts +558 -80
  7. package/dist/cadre-node.js +1060 -170
  8. package/dist/cadre-node.js.map +1 -1
  9. package/dist/control-authorization.d.ts +2 -2
  10. package/dist/control-authorization.js +1 -0
  11. package/dist/control-authorization.js.map +1 -1
  12. package/dist/control-cohort.d.ts +13 -26
  13. package/dist/control-cohort.js +0 -26
  14. package/dist/control-cohort.js.map +1 -1
  15. package/dist/control-database.d.ts +299 -44
  16. package/dist/control-database.js +469 -69
  17. package/dist/control-database.js.map +1 -1
  18. package/dist/control-read-retry.d.ts +40 -11
  19. package/dist/control-read-retry.js +48 -15
  20. package/dist/control-read-retry.js.map +1 -1
  21. package/dist/control-retry.d.ts +58 -3
  22. package/dist/control-retry.js +58 -3
  23. package/dist/control-retry.js.map +1 -1
  24. package/dist/control-schema.d.ts +1 -1
  25. package/dist/control-schema.js +103 -19
  26. package/dist/control-schema.js.map +1 -1
  27. package/dist/control-write-retry.d.ts +80 -11
  28. package/dist/control-write-retry.js +176 -29
  29. package/dist/control-write-retry.js.map +1 -1
  30. package/dist/enrolled-machine-store.js +2 -2
  31. package/dist/index.d.ts +14 -7
  32. package/dist/index.js +33 -6
  33. package/dist/index.js.map +1 -1
  34. package/dist/node-local-snapshot.d.ts +15 -7
  35. package/dist/node-local-snapshot.js +23 -8
  36. package/dist/node-local-snapshot.js.map +1 -1
  37. package/dist/peer-dial.d.ts +143 -0
  38. package/dist/peer-dial.js +176 -0
  39. package/dist/peer-dial.js.map +1 -0
  40. package/dist/relay-addrs.d.ts +90 -52
  41. package/dist/relay-addrs.js +91 -59
  42. package/dist/relay-addrs.js.map +1 -1
  43. package/dist/relay-reservation.d.ts +71 -3
  44. package/dist/relay-reservation.js +116 -8
  45. package/dist/relay-reservation.js.map +1 -1
  46. package/dist/seed-bootstrap.d.ts +30 -1
  47. package/dist/seed-bootstrap.js +62 -17
  48. package/dist/seed-bootstrap.js.map +1 -1
  49. package/dist/storage-scope.d.ts +62 -0
  50. package/dist/storage-scope.js +75 -0
  51. package/dist/storage-scope.js.map +1 -0
  52. package/dist/strand-database.d.ts +34 -16
  53. package/dist/strand-database.js +38 -24
  54. package/dist/strand-database.js.map +1 -1
  55. package/dist/strand-first-sync-gate.d.ts +152 -0
  56. package/dist/strand-first-sync-gate.js +208 -0
  57. package/dist/strand-first-sync-gate.js.map +1 -0
  58. package/dist/strand-formation-manager.d.ts +87 -5
  59. package/dist/strand-formation-manager.js +109 -5
  60. package/dist/strand-formation-manager.js.map +1 -1
  61. package/dist/strand-formation-protocol.d.ts +21 -1
  62. package/dist/strand-formation-protocol.js +30 -0
  63. package/dist/strand-formation-protocol.js.map +1 -1
  64. package/dist/strand-instance-manager.d.ts +243 -6
  65. package/dist/strand-instance-manager.js +470 -37
  66. package/dist/strand-instance-manager.js.map +1 -1
  67. package/dist/strand-member-key.d.ts +20 -12
  68. package/dist/strand-member-key.js +20 -12
  69. package/dist/strand-member-key.js.map +1 -1
  70. package/dist/strand-member-registry.js +3 -0
  71. package/dist/strand-member-registry.js.map +1 -1
  72. package/dist/strand-membership-reconciler.d.ts +361 -0
  73. package/dist/strand-membership-reconciler.js +548 -0
  74. package/dist/strand-membership-reconciler.js.map +1 -0
  75. package/dist/strand-membership-writer.d.ts +131 -11
  76. package/dist/strand-membership-writer.js +396 -134
  77. package/dist/strand-membership-writer.js.map +1 -1
  78. package/dist/strand-network-config.d.ts +46 -14
  79. package/dist/strand-network-config.js +79 -23
  80. package/dist/strand-network-config.js.map +1 -1
  81. package/dist/strand-solicitation.d.ts +10 -1
  82. package/dist/strand-solicitation.js +5 -0
  83. package/dist/strand-solicitation.js.map +1 -1
  84. package/dist/strand-wake-protocol.d.ts +11 -7
  85. package/dist/strand-wake-protocol.js +20 -52
  86. package/dist/strand-wake-protocol.js.map +1 -1
  87. package/dist/strand-watcher.js +6 -0
  88. package/dist/strand-watcher.js.map +1 -1
  89. package/dist/timed-step.d.ts +12 -0
  90. package/dist/timed-step.js +28 -0
  91. package/dist/timed-step.js.map +1 -0
  92. package/dist/timeout-scheduler.d.ts +19 -0
  93. package/dist/timeout-scheduler.js +22 -0
  94. package/dist/timeout-scheduler.js.map +1 -0
  95. package/dist/types.d.ts +262 -30
  96. package/dist/types.js.map +1 -1
  97. package/package.json +8 -8
  98. package/src/bootstrap-peer-store-file.ts +2 -2
  99. package/src/bootstrap-peer-store.ts +69 -27
  100. package/src/cadre-node.ts +7301 -6353
  101. package/src/control-authorization.ts +2 -1
  102. package/src/control-cohort.ts +122 -130
  103. package/src/control-database.ts +537 -69
  104. package/src/control-read-retry.ts +202 -167
  105. package/src/control-retry.ts +297 -190
  106. package/src/control-schema.ts +103 -19
  107. package/src/control-write-retry.ts +533 -383
  108. package/src/enrolled-machine-store.ts +2 -2
  109. package/src/index.ts +83 -6
  110. package/src/node-local-snapshot.ts +25 -8
  111. package/src/peer-dial.ts +215 -0
  112. package/src/relay-addrs.ts +458 -436
  113. package/src/relay-reservation.ts +951 -828
  114. package/src/seed-bootstrap.ts +76 -18
  115. package/src/storage-scope.ts +78 -0
  116. package/src/strand-database.ts +50 -27
  117. package/src/strand-first-sync-gate.ts +258 -0
  118. package/src/strand-formation-manager.ts +140 -5
  119. package/src/strand-formation-protocol.ts +43 -1
  120. package/src/strand-instance-manager.ts +577 -37
  121. package/src/strand-member-key.ts +20 -12
  122. package/src/strand-member-registry.ts +3 -0
  123. package/src/strand-membership-reconciler.ts +680 -0
  124. package/src/strand-membership-writer.ts +527 -189
  125. package/src/strand-network-config.ts +102 -24
  126. package/src/strand-solicitation.ts +16 -1
  127. package/src/strand-wake-protocol.ts +21 -66
  128. package/src/strand-watcher.ts +6 -0
  129. package/src/timed-step.ts +28 -0
  130. package/src/timeout-scheduler.ts +28 -0
  131. package/src/types.ts +1789 -1554
@@ -305,6 +305,66 @@ declare schema CadreControl {
305
305
  )
306
306
  ) with context (OwnerKey text, Signature text);
307
307
 
308
+ -- ONE party's own membership identity for one strand: the private key whose public
309
+ -- half seats this party's \`Strand.Member\` / \`Strand.Manager\` rows. Deliberately a
310
+ -- SEPARATE key from Strand.MemberPrivateKey — that column is the strand-wide read
311
+ -- secret formation hands to EVERY joining party, so an identity derived from it is
312
+ -- one every joiner can forge (gotchoices/sereus#4). A side table rather than a
313
+ -- Strand column because a JOINER holds no control Strand row at all (reference apps
314
+ -- attach formed strands from an in-memory row) yet must persist its own party key
315
+ -- too; keyed by strand id it serves founder and joiner uniformly, and it leaves the
316
+ -- heavily-audited Strand insert digest untouched. Deliberately NO closed-strands-only
317
+ -- cross-table check: the joiner writes this row with no local Strand row to check
318
+ -- against. Party-private in the same sense as MemberPrivateKey — replicated in
319
+ -- plaintext to every machine the party owns (docs/strands.md → "Closed-Strand Member
320
+ -- Key Handling"; that replication is what lets any of the party's machines sign
321
+ -- membership writes) — but unlike MemberPrivateKey it is NEVER put on the formation
322
+ -- wire.
323
+ table StrandPartyKey (
324
+ Id text primary key, -- the strand id this key is for
325
+ PrivateKey text not null, -- THIS party's ed25519 strand member private key, base64
326
+ -- protobuf — the same encoding as Strand.MemberPrivateKey;
327
+ -- decode with cadre-core's strandMemberKeyPair.
328
+ StampId text not null unique, -- single-use authorization nonce (anti-replay).
329
+ -- \`unique\` holds over LIVE rows only; a removed row's stamp is
330
+ -- retired permanently into Revocation (NotRevoked below).
331
+ -- A removed row's StampId is retired into Revocation, and this refuses any insert
332
+ -- naming a retired stamp: the approval that seated this row can never re-seat it
333
+ -- after removal. Same rationale as OwnerKey.NotRevoked, stated in full there.
334
+ constraint NotRevoked check on insert (
335
+ not exists (select 1 from Revocation R where R.TableName = 'StrandPartyKey' and R.StampId = new.StampId)
336
+ ),
337
+ -- Retirement is MANDATORY: a delete must carry the matching Revocation row in the same
338
+ -- transaction, or the stamp would free up again. Deferred (subquery), so the sibling
339
+ -- insert is visible at commit regardless of statement order. The tombstone must also
340
+ -- name the removed ROW, not just its stamp — see Revocation.RowKey.
341
+ constraint RevocationRecorded check on delete (
342
+ exists (select 1 from Revocation R where R.TableName = 'StrandPartyKey' and R.StampId = old.StampId and R.RowKey = old.Id)
343
+ ),
344
+ -- Insert + delete only; a rotation is remove-then-insert (fresh stamp, fresh
345
+ -- signature). Required because AuthorizedInsert / AuthorizedDelete below cover
346
+ -- neither update — an in-place rewrite would swap the party's identity key with
347
+ -- no signature at all. Mirrors ValidationKey.NoUpdate.
348
+ constraint NoUpdate check on update (false),
349
+ constraint AuthorizedInsert check on insert (
350
+ -- Owners authorize by signing over THIS row (Id, PrivateKey, StampId);
351
+ -- single-use via unique StampId. Binding PrivateKey means a captured approval
352
+ -- can only ever reproduce the exact key it approved, never seat an
353
+ -- attacker-chosen identity under the party's name.
354
+ exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and verify(digest('CadreControl.StrandPartyKey', 'add', new.Id, new.PrivateKey, new.StampId), context.Signature, A.Key, 'ed25519'))
355
+ ),
356
+ constraint AuthorizedDelete check on delete (
357
+ -- Delete is authorized by a signature over the DISTINCT 'remove'-tagged digest
358
+ -- bound to the STORED row (Id, StampId), so the enrollment approval — which
359
+ -- never expires — can never be replayed as a removal.
360
+ -- DELIBERATELY no REAP branch (cf. ValidationKey.AuthorizedDelete): like
361
+ -- Strand.MemberPrivateKey, PrivateKey is a secret stored nowhere else, so this
362
+ -- delete's effect is unrecoverable and stays strictly owner-signed. Same
363
+ -- reasoning as Strand.AuthorizedDelete.
364
+ exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and verify(digest('CadreControl.StrandPartyKey', 'remove', old.Id, old.StampId), context.Signature, A.Key, 'ed25519'))
365
+ )
366
+ ) with context (OwnerKey text, Signature text);
367
+
308
368
  -- A peer (node) that is part of the cadre, carrying a self-published,
309
369
  -- freshness-stamped, self-signed address record (see PeerAddressRecord in cadre-core).
310
370
  -- The row IS the peer-address record: a resolver reads it, re-verifies Sig against
@@ -704,25 +764,26 @@ declare schema CadreControl {
704
764
 
705
765
  -- Token is no longer a primary-key prefix (the key is the redemption nonce), so the
706
766
  -- per-token filters — the cap count in Authorized above, and cadre-core's
707
- -- countFormationUsage / isTokenUsed / hasOutstandingFormationInvite — are served by a
708
- -- table scan: a bare equality on a non-key column is a shape the optimystic vtab declines
709
- -- to claim, so the engine applies the filter itself.
767
+ -- countFormationUsage / isTokenUsed / hasOutstandingFormationInvite — are served by this
768
+ -- index rather than by a full scan of a table that is append-only and grows for the life
769
+ -- of the party.
710
770
  --
711
- -- There WAS an \`index FormationUsageByToken on FormationUsage (Token)\` here, declared by
712
- -- \`formation-unique-token-redesign\` purely to spare that scan. It was removed 2026-08-25
713
- -- because the tripwire that change recorded ("an under-reporting descent would admit a
714
- -- seat the invitation did not pay for — a failure mode a scan could not produce") fired:
715
- -- a secondary-index descent on a second machine returns only the rows THAT machine wrote,
716
- -- so a cap counted through it under-reports without bound across machines. Measured with
717
- -- the engine's own \`index:seek\` trace — each machine's copy of the index sub-collection
718
- -- sits at a different revision while their copies of the table itself agree, and the
719
- -- refresh that runs immediately before the descent does not close the gap. Tracked in
720
- -- \`tickets/blocked/secondary-index-seek-blind-to-sibling-rows.md\`, and upstream as
721
- -- \`bug-index-subcollection-sits-one-revision-behind-on-the-sibling\`.
771
+ -- The cap count is only ever as correct as this index's convergence across machines. That
772
+ -- convergence failed once: from 2026-08-04 to 2026-08-25 a descent on a second machine
773
+ -- returned only the rows THAT machine wrote, so the index was removed and the reads went
774
+ -- back to the scan. The engine defect was fixed upstream and re-measured here on
775
+ -- 2026-09-17, so the index is declared again. Its guard is the integration-tests scenario
776
+ -- \`strand-formation-concurrent-redemption\`, which asserts BOTH machines' views of a raced
777
+ -- redemption and therefore goes red if that convergence regresses.
722
778
  --
723
- -- DO NOT re-declare it to speed the count up until that is fixed upstream. Re-adding this
724
- -- one line is also how the reproducer comes back: it is what makes
725
- -- \`strand-formation-concurrent-redemption\` fail.
779
+ -- NOTE: the write side has its own exposure. Every FormationUsage insert now maintains this
780
+ -- sub-collection as well as the table, and an intermittent engine failure once hit exactly that
781
+ -- step on other tables (a table's rows committed while its index did not; closed 2026-09-17 with
782
+ -- the upstream sync fixes, \`tickets/complete/strand-unique-index-sync-stale-revision\`). Nothing
783
+ -- has been observed here, and redemptions are rare writes; but if joins start failing
784
+ -- intermittently with an index-sync error naming FormationUsage, this declaration is the first
785
+ -- thing to suspect.
786
+ index FormationUsageByToken on FormationUsage (Token);
726
787
 
727
788
  -- Append-only retirement record for the one-off StampId nonces of removed OwnerKey /
728
789
  -- CadrePeer / ValidationKey / Strand rows. Without it a removal was undoable: the add
@@ -744,10 +805,20 @@ declare schema CadreControl {
744
805
  -- A tombstone committed while the node was alone is local-only (never broadcast); the
745
806
  -- ReissuedAt counter below exists so an owner can re-write — and therefore re-broadcast —
746
807
  -- such a tombstone once peers are reachable, without changing what it means.
808
+ -- One row retires nothing: the singleton ledger marker ('Revocation', 'ledger', 'opened'),
809
+ -- filed once by an owner's connected reconcile pass (ControlDatabase.openRevocationLedger).
810
+ -- It exists only so this table is never a never-written block. The storage layer consults
811
+ -- a block's cohort on EVERY read of a block it does not hold, and every membership lookup
812
+ -- and every guarded insert (NotRevoked) reads this table; once any row exists the block is
813
+ -- held and re-checked on the storage layer's normal read-repair schedule, like every other
814
+ -- populated table. It cannot read as a retirement: every reader of this table filters on
815
+ -- its own guarded TableName, 'Revocation' is not one, and ControlDatabase.queryRevocations
816
+ -- skips the marker, so nothing reaps or re-issues it.
747
817
  table Revocation (
748
- TableName text, -- 'OwnerKey' | 'CadrePeer' | 'ValidationKey' | 'Strand' | 'DeviceToken' (confined by RowIsGone below)
818
+ TableName text, -- 'OwnerKey' | 'CadrePeer' | 'ValidationKey' | 'Strand' | 'StrandPartyKey' | 'DeviceToken',
819
+ -- or 'Revocation' for the ledger marker only (all confined by RowIsGone below)
749
820
  RowKey text not null, -- primary key of the removed row: OwnerKey.Key / ValidationKey.Key /
750
- -- CadrePeer.PeerId / DeviceToken.PeerId / Strand.Id
821
+ -- CadrePeer.PeerId / DeviceToken.PeerId / Strand.Id / StrandPartyKey.Id
751
822
  -- Every guarded table's RevocationRecorded CHECK requires the
752
823
  -- accompanying tombstone to carry the removed row's key here, so
753
824
  -- a REMOVAL cannot file a misnamed tombstone. A tombstone with no
@@ -787,12 +858,19 @@ declare schema CadreControl {
787
858
  -- it, stamp1) then re-admitted (stamp2) would leave a node that converges on the
788
859
  -- re-add FIRST rejecting the stamp1 tombstone forever, never learning that stamp is
789
860
  -- retired. The stamp-only form passes there (stamp1 is not live).
861
+ -- The last branch admits the ledger marker (see the table comment) and nothing else under
862
+ -- TableName 'Revocation': the whole triple is pinned, so with the primary key
863
+ -- (TableName, StampId) it is a singleton and the append-only growth surface is unchanged.
864
+ -- 'opened' cannot collide with a real stamp (43 base64url characters, generateStampId),
865
+ -- and the primary key includes TableName anyway.
790
866
  constraint RowIsGone check on insert (
791
867
  (new.TableName = 'OwnerKey' and not exists (select 1 from OwnerKey K where K.StampId = new.StampId))
792
868
  or (new.TableName = 'CadrePeer' and not exists (select 1 from CadrePeer P where P.StampId = new.StampId))
793
869
  or (new.TableName = 'ValidationKey' and not exists (select 1 from ValidationKey V where V.StampId = new.StampId))
794
870
  or (new.TableName = 'Strand' and not exists (select 1 from Strand S where S.StampId = new.StampId))
871
+ or (new.TableName = 'StrandPartyKey' and not exists (select 1 from StrandPartyKey K where K.StampId = new.StampId))
795
872
  or (new.TableName = 'DeviceToken' and not exists (select 1 from DeviceToken D where D.StampId = new.StampId))
873
+ or (new.TableName = 'Revocation' and new.RowKey = 'ledger' and new.StampId = 'opened')
796
874
  ),
797
875
  -- Appending a tombstone is an OWNER action, like every other write in this schema.
798
876
  -- Ungated, any writer that could reach the control database could retire a stamp:
@@ -826,6 +904,12 @@ declare schema CadreControl {
826
904
  -- than that one.
827
905
  -- RowIsGone and NoDelete are unchanged: they guard an owner retiring a stamp early
828
906
  -- or withdrawing one later, which authorization does not cover.
907
+ -- The ledger marker is signed under this same rule, over
908
+ -- digest('CadreControl.Revocation', 'remove', 'Revocation', 'ledger', 'opened'). The
909
+ -- 'remove' tag on a row that retires nothing is deliberate: reusing the one append rule
910
+ -- keeps every append owner-signed without adding an unsigned or differently tagged
911
+ -- branch, and a captured marker signature can only re-file the identical row, whose
912
+ -- primary key is already taken.
829
913
  constraint Authorized check on insert (
830
914
  exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and verify(digest('CadreControl.Revocation', 'remove', new.TableName, new.RowKey, new.StampId), context.Signature, A.Key, 'ed25519'))
831
915
  ),