@serfab/cadre-core 0.9.0 → 0.11.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 (193) hide show
  1. package/README.md +255 -253
  2. package/dist/bootstrap-peer-store-file.d.ts +13 -0
  3. package/dist/bootstrap-peer-store-file.js +42 -0
  4. package/dist/bootstrap-peer-store-file.js.map +1 -0
  5. package/dist/bootstrap-peer-store.d.ts +73 -0
  6. package/dist/bootstrap-peer-store.js +128 -0
  7. package/dist/bootstrap-peer-store.js.map +1 -0
  8. package/dist/cadre-node.d.ts +1121 -81
  9. package/dist/cadre-node.js +2276 -262
  10. package/dist/cadre-node.js.map +1 -1
  11. package/dist/control-authorization.d.ts +93 -0
  12. package/dist/control-authorization.js +57 -0
  13. package/dist/control-authorization.js.map +1 -0
  14. package/dist/control-cohort.d.ts +26 -0
  15. package/dist/control-cohort.js +26 -0
  16. package/dist/control-cohort.js.map +1 -1
  17. package/dist/control-database.d.ts +959 -54
  18. package/dist/control-database.js +1421 -148
  19. package/dist/control-database.js.map +1 -1
  20. package/dist/control-formation-recorder.d.ts +90 -10
  21. package/dist/control-formation-recorder.js +130 -14
  22. package/dist/control-formation-recorder.js.map +1 -1
  23. package/dist/control-schema.d.ts +1 -1
  24. package/dist/control-schema.js +651 -72
  25. package/dist/control-schema.js.map +1 -1
  26. package/dist/control-stream.d.ts +38 -4
  27. package/dist/control-stream.js +70 -4
  28. package/dist/control-stream.js.map +1 -1
  29. package/dist/control-write-retry.d.ts +161 -0
  30. package/dist/control-write-retry.js +412 -0
  31. package/dist/control-write-retry.js.map +1 -0
  32. package/dist/delegate-admission.d.ts +134 -0
  33. package/dist/delegate-admission.js +244 -0
  34. package/dist/delegate-admission.js.map +1 -0
  35. package/dist/device-token.d.ts +11 -11
  36. package/dist/device-token.js +14 -13
  37. package/dist/device-token.js.map +1 -1
  38. package/dist/ed25519-key.d.ts +26 -0
  39. package/dist/ed25519-key.js +56 -1
  40. package/dist/ed25519-key.js.map +1 -1
  41. package/dist/file-durable-slot.d.ts +20 -0
  42. package/dist/file-durable-slot.js +53 -0
  43. package/dist/file-durable-slot.js.map +1 -0
  44. package/dist/formation-approval.d.ts +157 -0
  45. package/dist/formation-approval.js +375 -0
  46. package/dist/formation-approval.js.map +1 -0
  47. package/dist/fs-atomic.d.ts +38 -0
  48. package/dist/fs-atomic.js +97 -0
  49. package/dist/fs-atomic.js.map +1 -0
  50. package/dist/identity-key.d.ts +49 -0
  51. package/dist/identity-key.js +81 -0
  52. package/dist/identity-key.js.map +1 -0
  53. package/dist/index.d.ts +17 -7
  54. package/dist/index.js +40 -6
  55. package/dist/index.js.map +1 -1
  56. package/dist/key-store-file.d.ts +2 -24
  57. package/dist/key-store-file.js +10 -77
  58. package/dist/key-store-file.js.map +1 -1
  59. package/dist/membership-connection-gater.d.ts +146 -0
  60. package/dist/membership-connection-gater.js +180 -0
  61. package/dist/membership-connection-gater.js.map +1 -0
  62. package/dist/node-local-snapshot.d.ts +109 -0
  63. package/dist/node-local-snapshot.js +182 -0
  64. package/dist/node-local-snapshot.js.map +1 -0
  65. package/dist/peer-addr-book.d.ts +92 -0
  66. package/dist/peer-addr-book.js +220 -0
  67. package/dist/peer-addr-book.js.map +1 -0
  68. package/dist/peer-authorization.d.ts +135 -18
  69. package/dist/peer-authorization.js +152 -21
  70. package/dist/peer-authorization.js.map +1 -1
  71. package/dist/peer-record.d.ts +21 -8
  72. package/dist/peer-record.js +26 -10
  73. package/dist/peer-record.js.map +1 -1
  74. package/dist/relay-addrs.d.ts +45 -0
  75. package/dist/relay-addrs.js +103 -0
  76. package/dist/relay-addrs.js.map +1 -0
  77. package/dist/relay-reservation.d.ts +266 -0
  78. package/dist/relay-reservation.js +613 -0
  79. package/dist/relay-reservation.js.map +1 -0
  80. package/dist/seed-bootstrap.d.ts +228 -56
  81. package/dist/seed-bootstrap.js +340 -198
  82. package/dist/seed-bootstrap.js.map +1 -1
  83. package/dist/seed-trust-policy.d.ts +47 -21
  84. package/dist/seed-trust-policy.js +24 -15
  85. package/dist/seed-trust-policy.js.map +1 -1
  86. package/dist/strand-addr-protocol.d.ts +42 -13
  87. package/dist/strand-addr-protocol.js +58 -52
  88. package/dist/strand-addr-protocol.js.map +1 -1
  89. package/dist/strand-backfill.d.ts +109 -0
  90. package/dist/strand-backfill.js +279 -0
  91. package/dist/strand-backfill.js.map +1 -0
  92. package/dist/strand-cohort.d.ts +4 -37
  93. package/dist/strand-cohort.js +4 -15
  94. package/dist/strand-cohort.js.map +1 -1
  95. package/dist/strand-database.d.ts +11 -16
  96. package/dist/strand-database.js +19 -7
  97. package/dist/strand-database.js.map +1 -1
  98. package/dist/strand-formation-manager.d.ts +36 -7
  99. package/dist/strand-formation-manager.js +115 -15
  100. package/dist/strand-formation-manager.js.map +1 -1
  101. package/dist/strand-formation-protocol.d.ts +103 -4
  102. package/dist/strand-formation-protocol.js +232 -4
  103. package/dist/strand-formation-protocol.js.map +1 -1
  104. package/dist/strand-instance-manager.d.ts +88 -19
  105. package/dist/strand-instance-manager.js +178 -34
  106. package/dist/strand-instance-manager.js.map +1 -1
  107. package/dist/strand-member-registry.d.ts +5 -5
  108. package/dist/strand-member-registry.js +18 -10
  109. package/dist/strand-member-registry.js.map +1 -1
  110. package/dist/strand-membership-writer.d.ts +484 -88
  111. package/dist/strand-membership-writer.js +751 -128
  112. package/dist/strand-membership-writer.js.map +1 -1
  113. package/dist/strand-solicitation.d.ts +99 -33
  114. package/dist/strand-solicitation.js +100 -52
  115. package/dist/strand-solicitation.js.map +1 -1
  116. package/dist/strand-transport-key.d.ts +40 -0
  117. package/dist/strand-transport-key.js +56 -0
  118. package/dist/strand-transport-key.js.map +1 -0
  119. package/dist/strand-wake-protocol.d.ts +9 -7
  120. package/dist/strand-wake-protocol.js +22 -46
  121. package/dist/strand-wake-protocol.js.map +1 -1
  122. package/dist/strand-watcher.d.ts +17 -2
  123. package/dist/strand-watcher.js +47 -3
  124. package/dist/strand-watcher.js.map +1 -1
  125. package/dist/trusted-owner-store-file.d.ts +12 -0
  126. package/dist/trusted-owner-store-file.js +39 -0
  127. package/dist/trusted-owner-store-file.js.map +1 -0
  128. package/dist/trusted-owner-store.d.ts +64 -0
  129. package/dist/trusted-owner-store.js +126 -0
  130. package/dist/trusted-owner-store.js.map +1 -0
  131. package/dist/types.d.ts +307 -44
  132. package/dist/types.js +13 -0
  133. package/dist/types.js.map +1 -1
  134. package/package.json +20 -8
  135. package/src/arachnode-stub.ts +159 -159
  136. package/src/bootstrap-peer-store-file.ts +44 -0
  137. package/src/bootstrap-peer-store.ts +172 -0
  138. package/src/cadre-node.ts +5158 -2981
  139. package/src/canonical-json.ts +30 -30
  140. package/src/control-authorization.ts +114 -0
  141. package/src/control-cohort.ts +130 -103
  142. package/src/control-database.ts +2406 -948
  143. package/src/control-formation-recorder.ts +305 -151
  144. package/src/control-schema.ts +651 -72
  145. package/src/control-stream.ts +205 -128
  146. package/src/control-write-retry.ts +483 -0
  147. package/src/delegate-admission.ts +302 -0
  148. package/src/device-token.ts +94 -89
  149. package/src/diagnostics/connection-path.ts +277 -277
  150. package/src/diagnostics/webrtc-turn-tracker.ts +222 -222
  151. package/src/ed25519-key.ts +66 -1
  152. package/src/enrollment.ts +199 -199
  153. package/src/file-durable-slot.ts +61 -0
  154. package/src/formation-approval.ts +604 -0
  155. package/src/fs-atomic.ts +107 -0
  156. package/src/hibernation-manager.ts +437 -437
  157. package/src/identity-key.ts +109 -0
  158. package/src/index.ts +137 -5
  159. package/src/key-store-file.ts +132 -203
  160. package/src/key-store.ts +99 -99
  161. package/src/membership-connection-gater.ts +204 -0
  162. package/src/node-local-snapshot.ts +260 -0
  163. package/src/peer-addr-book.ts +250 -0
  164. package/src/peer-authorization.ts +259 -81
  165. package/src/peer-record.ts +164 -146
  166. package/src/push-credentials.ts +84 -84
  167. package/src/push-node.ts +64 -64
  168. package/src/push-notifier-apns.ts +247 -247
  169. package/src/push-notifier-fcm.ts +220 -220
  170. package/src/push-notifier-shared.ts +28 -28
  171. package/src/push-notifier.ts +52 -52
  172. package/src/relay-addrs.ts +111 -0
  173. package/src/relay-reservation.ts +823 -0
  174. package/src/seed-bootstrap.ts +409 -209
  175. package/src/seed-trust-policy.ts +140 -107
  176. package/src/strand-addr-protocol.ts +441 -410
  177. package/src/strand-backfill.ts +366 -0
  178. package/src/strand-cohort.ts +38 -77
  179. package/src/strand-database.ts +202 -204
  180. package/src/strand-formation-manager.ts +491 -368
  181. package/src/strand-formation-protocol.ts +692 -391
  182. package/src/strand-instance-manager.ts +201 -46
  183. package/src/strand-member-key.ts +39 -39
  184. package/src/strand-member-registry.ts +182 -177
  185. package/src/strand-membership-writer.ts +938 -146
  186. package/src/strand-solicitation.ts +472 -407
  187. package/src/strand-transport-key.ts +58 -0
  188. package/src/strand-wake-payload.ts +32 -32
  189. package/src/strand-wake-protocol.ts +314 -331
  190. package/src/strand-watcher.ts +314 -251
  191. package/src/trusted-owner-store-file.ts +41 -0
  192. package/src/trusted-owner-store.ts +171 -0
  193. package/src/types.ts +1309 -1032
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Merging verified cadre addresses into a libp2p node's **address book** (its
3
+ * peerStore), so that every layer below cadre-core which dials by bare peer id
4
+ * — Optimystic's cluster client, repo client, FRET ping/announce, all funnelling
5
+ * into `libp2p.dialProtocol(peerId, …)` — has an address to use for a sibling
6
+ * whose connection has dropped, instead of failing with `NoValidAddressesError`
7
+ * until the next reconcile pass happens to re-dial it.
8
+ *
9
+ * Shared by the control-cohort reconcile pass and the strand one (both in
10
+ * `CadreNode`): each resolves a peer's addresses its own way and then needs
11
+ * exactly this write, with exactly this workaround.
12
+ *
13
+ * Two exports, in the order a caller uses them: {@link groupAddrsByPeerId} turns
14
+ * a flat address list into per-peer groups (the strand path starts from the
15
+ * peer-agnostic union the strand-addr RPC returns), and {@link mergePeerAddrs}
16
+ * writes one peer's group.
17
+ */
18
+ import type { PeerId, PeerStore } from '@libp2p/interface';
19
+ import { type Multiaddr } from '@multiformats/multiaddr';
20
+ /** Outcome of one address-book merge, for logging and tests. */
21
+ export type MergeAddrsResult = 'merged' | 'restamped' | 'skipped' | 'failed';
22
+ /**
23
+ * The slice of a libp2p node {@link mergePeerAddrs} touches. Structurally
24
+ * satisfied by `Libp2p`; narrow so a caller (or a test) can pass a bare
25
+ * peerStore holder.
26
+ */
27
+ export interface PeerAddrBookHost {
28
+ peerStore: PeerStore;
29
+ }
30
+ /**
31
+ * Write `addrs` into `host`'s address book for `peerId`, best-effort. `peerId`
32
+ * may be the string form callers hold (a `CadrePeer.PeerId` column, a strand-addr
33
+ * RPC result); parsing it is part of the best-effort contract below, so no caller
34
+ * has to wrap this in a second try/catch of its own.
35
+ *
36
+ * - **Empty `addrs` writes nothing** and reports `'skipped'`. That is how a
37
+ * revoked / stale / untrusted peer's existing entry is allowed to age out on
38
+ * its own: the caller resolves `[]` for it, and nothing here refreshes it.
39
+ * - Otherwise the addresses are merged, and — see the timestamp trap below —
40
+ * re-saved when the merge alone leaves them invisible (`'restamped'`).
41
+ * - Any throw (unparsable peer id, datastore failure, a peerStore double without
42
+ * `merge`) is logged and folded to `'failed'`. Callers are best-effort loops:
43
+ * an address-book failure must never abort a reconcile pass.
44
+ *
45
+ * NOTE: `peerStore.merge` cannot refresh an address's `observed` timestamp in
46
+ * `@libp2p/peer-store` 12.0.10 — `to-peer-pb.js` shadows its own loop variable
47
+ * (`…addresses?.find(addr => uint8ArrayEquals(addr.multiaddr, addr.multiaddr))`,
48
+ * always true, so it returns the FIRST stored address's timestamp), and every
49
+ * read filters out addresses older than `MAX_ADDRESS_AGE` (1 hour). So an entry
50
+ * silently dies at the one-hour mark and no amount of re-merging revives it —
51
+ * not even merging a brand-new address. The `save` below is the workaround:
52
+ * `save` is the one write path that omits `existingPeer` and therefore stamps
53
+ * `Date.now()`. Drop it once upstream stamps `Date.now()` on merge.
54
+ *
55
+ * NOTE: raising the store's own `maxAddressAge` (libp2p forwards `init.peerStore`
56
+ * straight into `persistentPeerStore`) would sidestep the bug in one line, and is
57
+ * deliberately NOT done: expiry is load-bearing here. Only *verified* addresses
58
+ * come through this helper, and the design relies on everything else — the
59
+ * cold-start seed entries, identify-learned addresses — still ageing out on the
60
+ * stock schedule. A global age bump would keep those alive too.
61
+ *
62
+ * NOTE: if the store ever REJECTS an address we hand it, the restamp repeats
63
+ * every pass forever — `allVisible` stays false, and `save` re-submits the same
64
+ * rejected address. libp2p wires the store's `addressFilter` to
65
+ * `connectionGater.filterMultiaddrForPeer`, which nothing in this repo implements
66
+ * today (`createMembershipConnectionGater` gates dials, not addresses), so the
67
+ * loop is unreachable. If an app ever supplies that gater hook via
68
+ * `NetworkConfig.connectionGater`, bound the retry — the cost is one redundant
69
+ * datastore write per gated sibling per reconcile pass.
70
+ */
71
+ export declare function mergePeerAddrs(host: PeerAddrBookHost, peerId: PeerId | string, addrs: Multiaddr[]): Promise<MergeAddrsResult>;
72
+ /**
73
+ * Group multiaddr strings by the peer id in their final `/p2p/` component — the
74
+ * attribution step between a flat, peer-agnostic address list and the per-peer
75
+ * writes {@link mergePeerAddrs} takes.
76
+ *
77
+ * The **last** `/p2p/` component is the addressed peer, not the first: a relayed
78
+ * address (`…/p2p/<relay>/p2p-circuit/p2p/<dst>`) names the relay first and the
79
+ * destination last, while a direct address (`…/tcp/…/p2p/<dst>`) names only the
80
+ * destination. Taking the last therefore attributes both shapes to the peer the
81
+ * address actually reaches.
82
+ *
83
+ * Three shapes are dropped rather than attributed, because none of them names a
84
+ * peer the address reaches: one that does not parse, one carrying no `/p2p/`
85
+ * component at all (a peer advertising a bare listen addr), and one whose
86
+ * `p2p-circuit` comes AFTER its last `/p2p/` (`…/p2p/<relay>/p2p-circuit`) — that
87
+ * last one is a relay hop with the destination missing, so its trailing peer id
88
+ * is the RELAY's and filing it under the relay would claim the relay is reachable
89
+ * at a circuit leading nowhere. Duplicates collapse within a group. Insertion
90
+ * order is preserved, both between groups and inside one.
91
+ */
92
+ export declare function groupAddrsByPeerId(addrs: string[]): Map<string, Multiaddr[]>;
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Merging verified cadre addresses into a libp2p node's **address book** (its
3
+ * peerStore), so that every layer below cadre-core which dials by bare peer id
4
+ * — Optimystic's cluster client, repo client, FRET ping/announce, all funnelling
5
+ * into `libp2p.dialProtocol(peerId, …)` — has an address to use for a sibling
6
+ * whose connection has dropped, instead of failing with `NoValidAddressesError`
7
+ * until the next reconcile pass happens to re-dial it.
8
+ *
9
+ * Shared by the control-cohort reconcile pass and the strand one (both in
10
+ * `CadreNode`): each resolves a peer's addresses its own way and then needs
11
+ * exactly this write, with exactly this workaround.
12
+ *
13
+ * Two exports, in the order a caller uses them: {@link groupAddrsByPeerId} turns
14
+ * a flat address list into per-peer groups (the strand path starts from the
15
+ * peer-agnostic union the strand-addr RPC returns), and {@link mergePeerAddrs}
16
+ * writes one peer's group.
17
+ */
18
+ import debug from 'debug';
19
+ import { peerIdFromString } from '@libp2p/peer-id';
20
+ import { multiaddr, CODE_P2P, CODE_P2P_CIRCUIT } from '@multiformats/multiaddr';
21
+ const log = debug('sereus:cadre:peer-addr-book');
22
+ /**
23
+ * Write `addrs` into `host`'s address book for `peerId`, best-effort. `peerId`
24
+ * may be the string form callers hold (a `CadrePeer.PeerId` column, a strand-addr
25
+ * RPC result); parsing it is part of the best-effort contract below, so no caller
26
+ * has to wrap this in a second try/catch of its own.
27
+ *
28
+ * - **Empty `addrs` writes nothing** and reports `'skipped'`. That is how a
29
+ * revoked / stale / untrusted peer's existing entry is allowed to age out on
30
+ * its own: the caller resolves `[]` for it, and nothing here refreshes it.
31
+ * - Otherwise the addresses are merged, and — see the timestamp trap below —
32
+ * re-saved when the merge alone leaves them invisible (`'restamped'`).
33
+ * - Any throw (unparsable peer id, datastore failure, a peerStore double without
34
+ * `merge`) is logged and folded to `'failed'`. Callers are best-effort loops:
35
+ * an address-book failure must never abort a reconcile pass.
36
+ *
37
+ * NOTE: `peerStore.merge` cannot refresh an address's `observed` timestamp in
38
+ * `@libp2p/peer-store` 12.0.10 — `to-peer-pb.js` shadows its own loop variable
39
+ * (`…addresses?.find(addr => uint8ArrayEquals(addr.multiaddr, addr.multiaddr))`,
40
+ * always true, so it returns the FIRST stored address's timestamp), and every
41
+ * read filters out addresses older than `MAX_ADDRESS_AGE` (1 hour). So an entry
42
+ * silently dies at the one-hour mark and no amount of re-merging revives it —
43
+ * not even merging a brand-new address. The `save` below is the workaround:
44
+ * `save` is the one write path that omits `existingPeer` and therefore stamps
45
+ * `Date.now()`. Drop it once upstream stamps `Date.now()` on merge.
46
+ *
47
+ * NOTE: raising the store's own `maxAddressAge` (libp2p forwards `init.peerStore`
48
+ * straight into `persistentPeerStore`) would sidestep the bug in one line, and is
49
+ * deliberately NOT done: expiry is load-bearing here. Only *verified* addresses
50
+ * come through this helper, and the design relies on everything else — the
51
+ * cold-start seed entries, identify-learned addresses — still ageing out on the
52
+ * stock schedule. A global age bump would keep those alive too.
53
+ *
54
+ * NOTE: if the store ever REJECTS an address we hand it, the restamp repeats
55
+ * every pass forever — `allVisible` stays false, and `save` re-submits the same
56
+ * rejected address. libp2p wires the store's `addressFilter` to
57
+ * `connectionGater.filterMultiaddrForPeer`, which nothing in this repo implements
58
+ * today (`createMembershipConnectionGater` gates dials, not addresses), so the
59
+ * loop is unreachable. If an app ever supplies that gater hook via
60
+ * `NetworkConfig.connectionGater`, bound the retry — the cost is one redundant
61
+ * datastore write per gated sibling per reconcile pass.
62
+ */
63
+ export async function mergePeerAddrs(host, peerId, addrs) {
64
+ if (addrs.length === 0) {
65
+ return 'skipped';
66
+ }
67
+ try {
68
+ const id = typeof peerId === 'string' ? peerIdFromString(peerId) : peerId;
69
+ const peer = await host.peerStore.merge(id, { multiaddrs: addrs });
70
+ // The returned Peer is already expiry-filtered, so it says whether the merge
71
+ // is actually VISIBLE — which is the only question that matters downstream.
72
+ if (allVisible(peer.addresses, addrs, id)) {
73
+ return 'merged';
74
+ }
75
+ await host.peerStore.save(id, restampData(peer, addrs, id));
76
+ return 'restamped';
77
+ }
78
+ catch (error) {
79
+ log('mergePeerAddrs: address-book write for %s failed: %o', peerId.toString(), error);
80
+ return 'failed';
81
+ }
82
+ }
83
+ /**
84
+ * Group multiaddr strings by the peer id in their final `/p2p/` component — the
85
+ * attribution step between a flat, peer-agnostic address list and the per-peer
86
+ * writes {@link mergePeerAddrs} takes.
87
+ *
88
+ * The **last** `/p2p/` component is the addressed peer, not the first: a relayed
89
+ * address (`…/p2p/<relay>/p2p-circuit/p2p/<dst>`) names the relay first and the
90
+ * destination last, while a direct address (`…/tcp/…/p2p/<dst>`) names only the
91
+ * destination. Taking the last therefore attributes both shapes to the peer the
92
+ * address actually reaches.
93
+ *
94
+ * Three shapes are dropped rather than attributed, because none of them names a
95
+ * peer the address reaches: one that does not parse, one carrying no `/p2p/`
96
+ * component at all (a peer advertising a bare listen addr), and one whose
97
+ * `p2p-circuit` comes AFTER its last `/p2p/` (`…/p2p/<relay>/p2p-circuit`) — that
98
+ * last one is a relay hop with the destination missing, so its trailing peer id
99
+ * is the RELAY's and filing it under the relay would claim the relay is reachable
100
+ * at a circuit leading nowhere. Duplicates collapse within a group. Insertion
101
+ * order is preserved, both between groups and inside one.
102
+ */
103
+ export function groupAddrsByPeerId(addrs) {
104
+ const groups = new Map();
105
+ const seen = new Set();
106
+ for (const addr of addrs) {
107
+ const attributed = attributeAddr(addr);
108
+ if (!attributed) {
109
+ continue;
110
+ }
111
+ const key = `${attributed.peerId}\n${addr}`;
112
+ if (seen.has(key)) {
113
+ continue;
114
+ }
115
+ seen.add(key);
116
+ const group = groups.get(attributed.peerId);
117
+ if (group) {
118
+ group.push(attributed.multiaddr);
119
+ }
120
+ else {
121
+ groups.set(attributed.peerId, [attributed.multiaddr]);
122
+ }
123
+ }
124
+ return groups;
125
+ }
126
+ /**
127
+ * Parse one address and pull out the peer id it addresses (its last `/p2p/`
128
+ * value); null when the address is unparsable or names no destination. Every case
129
+ * is logged at the same level — an unattributable address is a sibling telling us
130
+ * something we cannot use, which is worth seeing but never worth throwing over.
131
+ */
132
+ function attributeAddr(addr) {
133
+ let parsed;
134
+ try {
135
+ parsed = multiaddr(addr);
136
+ }
137
+ catch (error) {
138
+ log('groupAddrsByPeerId: skipping unparsable addr %s: %o', addr, error);
139
+ return null;
140
+ }
141
+ const peerId = lastAddressedPeerId(parsed);
142
+ if (peerId === null) {
143
+ log('groupAddrsByPeerId: skipping addr naming no destination peer: %s', addr);
144
+ return null;
145
+ }
146
+ return { peerId, multiaddr: parsed };
147
+ }
148
+ /**
149
+ * The peer id a parsed address reaches: its last `/p2p/` value, or null when it
150
+ * has none — or when a `p2p-circuit` sits after it, which makes that value the
151
+ * relay's rather than a destination's (see {@link groupAddrsByPeerId}). Walking
152
+ * backwards, meeting the circuit first is exactly that case.
153
+ */
154
+ function lastAddressedPeerId(parsed) {
155
+ const components = parsed.getComponents();
156
+ for (let i = components.length - 1; i >= 0; i--) {
157
+ const component = components[i];
158
+ if (component.code === CODE_P2P_CIRCUIT) {
159
+ return null;
160
+ }
161
+ if (component.code === CODE_P2P && component.value) {
162
+ return component.value;
163
+ }
164
+ }
165
+ return null;
166
+ }
167
+ /**
168
+ * The peerStore strips a trailing `/p2p/<peerId>` from an address it stores when
169
+ * that id is the address's FIRST `/p2p/` component (`dedupe-addresses.js`), so a
170
+ * plain address round-trips without its suffix while a relayed one — whose first
171
+ * `/p2p/` names the relay — keeps it. Comparing and de-duplicating on this
172
+ * suffix-stripped form matches either shape.
173
+ */
174
+ function addrKey(addr, peerId) {
175
+ const suffix = `/p2p/${peerId.toString()}`;
176
+ return addr.endsWith(suffix) ? addr.slice(0, -suffix.length) : addr;
177
+ }
178
+ /** Is every address in `addrs` readable back from the stored, expiry-filtered set? */
179
+ function allVisible(stored, addrs, peerId) {
180
+ const keys = new Set(stored.map((a) => addrKey(a.multiaddr.toString(), peerId)));
181
+ return addrs.every((addr) => keys.has(addrKey(addr.toString(), peerId)));
182
+ }
183
+ /**
184
+ * The full `save` payload for a restamp: `save` re-uses the `patch` strategy with
185
+ * no existing peer, so every field it is not handed is DROPPED — and Optimystic
186
+ * classifies peers by their peerStore protocol list (`libp2p-key-network.ts`
187
+ * `membershipOf`), so losing protocols here would break cluster/repo routing.
188
+ * Everything therefore carries forward from the just-merged `peer`.
189
+ *
190
+ * The read-modify-write is NOT atomic against a concurrent `identify` write to
191
+ * the same entry. The loss window is milliseconds and costs at most one identify
192
+ * update, recovered by the next identify push — not worth a lock.
193
+ *
194
+ * A tag's remaining TTL is not carried: the public `Tag` type exposes only
195
+ * `value`, so the stored expiry is unreadable from here and the tag comes back
196
+ * non-expiring. `publicKey` is likewise not passed, which is lossless for the
197
+ * Ed25519 peer ids a cadre uses (the key is embedded in the id).
198
+ */
199
+ function restampData(peer, addrs, peerId) {
200
+ const addresses = new Map();
201
+ for (const address of peer.addresses) {
202
+ addresses.set(addrKey(address.multiaddr.toString(), peerId), address);
203
+ }
204
+ for (const addr of addrs) {
205
+ const key = addrKey(addr.toString(), peerId);
206
+ if (!addresses.has(key)) {
207
+ // Uncertified, matching what `merge`'s own `multiaddrs` path would have
208
+ // stored; an address already present keeps whatever flag it carried.
209
+ addresses.set(key, { multiaddr: addr, isCertified: false });
210
+ }
211
+ }
212
+ return {
213
+ addresses: [...addresses.values()],
214
+ protocols: peer.protocols,
215
+ metadata: new Map(peer.metadata),
216
+ tags: new Map([...peer.tags].map(([name, tag]) => [name, { value: tag.value }])),
217
+ peerRecordEnvelope: peer.peerRecordEnvelope
218
+ };
219
+ }
220
+ //# sourceMappingURL=peer-addr-book.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"peer-addr-book.js","sourceRoot":"","sources":["../src/peer-addr-book.ts"],"names":[],"mappings":"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"}
@@ -1,36 +1,110 @@
1
+ import type { RevocableTable } from './control-authorization.js';
1
2
  /**
2
- * Canonical digest an owner signs to authorize a peer.
3
+ * Canonical digest an owner signs to vouch a peer's ENROLLMENT — the offline
4
+ * credential `cadre enroll register` verifies. No table checks this digest, so it
5
+ * carries its own `'Cadre.Enrollment'` domain tag to stay disjoint from every
6
+ * CadreControl table rule (pre-tag it collided with the `DeviceToken` owner digests,
7
+ * so an enrollment vouch doubled as a push-token delete approval).
3
8
  *
4
- * This is the exact byte construction {@link SeedBootstrapService.authorizePeer}
5
- * signs over when it inserts an authorized `CadrePeer` row:
6
- * `digest([peerId], 'sha256', 'base64url')`. Factored into one place so the
7
- * producer (owner signing) and the verifier (the offline `cadre enroll
8
- * register` check) can never drift apart — change the digest here and both move
9
- * together. The SQL mirror is the bare single-field `digest(coalesce(new.PeerId, old.PeerId))`.
9
+ * Factored into one place so the producer (owner signing) and the verifier (the
10
+ * offline `cadre enroll register` check) can never drift apart — change the digest
11
+ * here and both move together.
10
12
  */
11
13
  export declare function peerAuthorizationDigest(peerId: string): string;
14
+ /**
15
+ * The `CadreControl.DeviceToken` columns {@link deviceTokenAddDigest} binds, in the
16
+ * schema's order. A whole-row struct rather than six positional strings, so a caller
17
+ * cannot silently transpose `platform` and `token` (both opaque strings) and mint a
18
+ * signature over a row it did not mean to approve.
19
+ *
20
+ * Structurally satisfied by a `DeviceTokenRecord` spread with the row's `stampId`;
21
+ * `types.ts:DeviceTokenRecord` deliberately does NOT carry the stamp — it is the
22
+ * SELF-signed record shape, and the peer's own `Sig` does not cover `StampId`.
23
+ * `updatedAt` / `sig` are nullable because the columns are: both sign as `''` when
24
+ * absent, mirroring the schema's `coalesce(...)`.
25
+ */
26
+ export interface DeviceTokenAuthorizedRow {
27
+ peerId: string;
28
+ platform: string;
29
+ token: string;
30
+ updatedAt: number | null;
31
+ sig: string | null;
32
+ stampId: string;
33
+ }
34
+ /**
35
+ * Canonical digest an owner signs to authorize a `DeviceToken` INSERT — the WHOLE row,
36
+ * ending in its single-use `StampId` nonce. SQL mirror:
37
+ * `digest('CadreControl.DeviceToken', 'add', new.PeerId, new.Platform, new.Token,
38
+ * coalesce(cast(new.UpdatedAt as text), ''), coalesce(new.Sig, ''), new.StampId)` in
39
+ * `DeviceToken.AuthorizedInsert`.
40
+ *
41
+ * Binding every column means a captured approval can only ever reproduce the exact row
42
+ * it approved — never one carrying attacker-chosen `Platform`/`Token`/`UpdatedAt` — and
43
+ * binding the stamp makes it single-use: while the row lives the `unique` column blocks
44
+ * a replay, and after a clear the stamp is retired permanently into
45
+ * `CadreControl.Revocation` (`DeviceToken.NotRevoked`). This matters more here than for
46
+ * `CadrePeer`: a resurrected push token has NO freshness ceiling to retire it
47
+ * (`CadreNode.resolveDeviceToken` defaults `maxAgeMs` to infinity by design), so stamp
48
+ * retirement is the only thing that sticks.
49
+ *
50
+ * Distinct from {@link deviceTokenRemoveDigest} so a captured insert approval can never
51
+ * be replayed to delete the token, and vice versa.
52
+ */
53
+ export declare function deviceTokenAddDigest(row: DeviceTokenAuthorizedRow): string;
54
+ /**
55
+ * Canonical digest an owner signs to authorize a `DeviceToken` DELETE, bound to the
56
+ * STORED row's (PeerId, StampId). SQL mirror:
57
+ * `digest('CadreControl.DeviceToken', 'remove', old.PeerId, old.StampId)` in
58
+ * `DeviceToken.AuthorizedDelete`.
59
+ *
60
+ * A narrower vector than {@link deviceTokenAddDigest} on purpose (the same split
61
+ * `CadrePeer` makes): the clear approval names only which row is being retired, so it
62
+ * cannot be re-cut into an insert approval, and it is dead the moment the stamp it
63
+ * names is tombstoned.
64
+ */
65
+ export declare function deviceTokenRemoveDigest(peerId: string, stampId: string): string;
12
66
  /**
13
67
  * Canonical digest an owner signs to VOUCH a `CadrePeer` membership row (insert
14
- * and the owner re-touch update). Binds the peer id to the row's single-use
15
- * `StampId` nonce, so a captured signed insert cannot be replayed (the `StampId` is
16
- * unique) and — because {@link cadrePeerRemoveDigest} scopes a DIFFERENT payload — the
17
- * stored voucher (`VouchSig`) cannot be replayed to authorize a delete.
68
+ * and the owner re-touch update — same semantics, deliberately the same digest).
69
+ * Binds the peer id to the row's single-use `StampId` nonce, so a captured signed
70
+ * insert cannot be replayed — while the row lives the `unique` column blocks it, and
71
+ * after a removal the stamp is retired permanently into `CadreControl.Revocation`
72
+ * (`CadrePeer.NotRevoked`) — and, because {@link cadrePeerRemoveDigest} scopes a
73
+ * DIFFERENT payload, the stored voucher (`VouchSig`) cannot be replayed to authorize
74
+ * a delete. The domain tag keeps the stored, replicated `VouchSig` useless against
75
+ * every OTHER table's rules.
18
76
  *
19
- * The SQL mirror is `digest(new.PeerId, new.StampId)` (two TEXT fields), matching the
20
- * canonical multi-field framing in control-database.ts:buildAuthorizationMessage.
77
+ * SQL mirror: `digest('CadreControl.CadrePeer', 'vouch', new.PeerId, new.StampId)`.
21
78
  */
22
79
  export declare function cadrePeerVoucherDigest(peerId: string, stampId: string): string;
23
80
  /**
24
81
  * Canonical digest an owner signs to REMOVE a `CadrePeer` row. Deliberately a
25
- * distinct payload from {@link cadrePeerVoucherDigest} (adds the `'remove'` action
26
- * tag) so the row's stored voucher — a signature over the voucher digest — can never
82
+ * distinct payload from {@link cadrePeerVoucherDigest} (the `'remove'` action tag)
83
+ * so the row's stored voucher — a signature over the voucher digest — can never
27
84
  * satisfy this delete check. The signature is supplied in write context and never
28
- * stored, so no reader can replay it; binding the live row's `StampId` also invalidates
29
- * any captured remove after a delete+reinsert (the nonce rotates).
85
+ * stored, so no reader can replay it; a captured remove is also dead after the
86
+ * delete lands, because a re-added row carries a FRESH `StampId` (the removed row's
87
+ * stamp is retired into `CadreControl.Revocation` and never reused).
30
88
  *
31
- * SQL mirror: `digest(old.PeerId, old.StampId, 'remove')`.
89
+ * SQL mirror: `digest('CadreControl.CadrePeer', 'remove', old.PeerId, old.StampId)`.
32
90
  */
33
91
  export declare function cadrePeerRemoveDigest(peerId: string, stampId: string): string;
92
+ /**
93
+ * Canonical digest an owner signs to APPEND a `CadreControl.Revocation` tombstone —
94
+ * the row retiring `stampId` for the named guarded table, and recording `rowKey`
95
+ * (the removed row's primary key: OwnerKey.Key / ValidationKey.Key /
96
+ * CadrePeer.PeerId / DeviceToken.PeerId / Strand.Id) as which row was retired.
97
+ * SQL mirror:
98
+ * `digest('CadreControl.Revocation', 'remove', new.TableName, new.RowKey, new.StampId)`
99
+ * in `Revocation.Authorized`.
100
+ *
101
+ * Its own `'CadreControl.Revocation'` domain tag makes it disjoint from the
102
+ * `'CadreControl.CadrePeer'` (or `OwnerKey` / `ValidationKey` / `Strand`)
103
+ * `'remove'` digest the same owner signs in the SAME transaction for the delete
104
+ * this tombstone accompanies — a removal signature is not a retirement
105
+ * signature and cannot be replayed as one.
106
+ */
107
+ export declare function revocationDigest(tableName: RevocableTable, rowKey: string, stampId: string): string;
34
108
  /**
35
109
  * Verify that `signature` is a valid owner ed25519 signature over `peerId`'s
36
110
  * authorization digest, using `ownerPublicKey` (base64url).
@@ -46,3 +120,46 @@ export declare function cadrePeerRemoveDigest(peerId: string, stampId: string):
46
120
  * exception). The catch is logged at debug.
47
121
  */
48
122
  export declare function verifyPeerAuthorization(peerId: string, ownerPublicKey: string, signature: string): boolean;
123
+ /**
124
+ * Verify that `signature` is a valid owner ed25519 signature over the
125
+ * `CadrePeer` voucher digest for (`peerId`, `stampId`) — the read-side mirror
126
+ * of the voucher {@link ControlDatabase.insertCadrePeer} signs and persists into
127
+ * `VouchOwner`/`VouchSig` (see {@link cadrePeerVoucherDigest}).
128
+ *
129
+ * A `true` result means the holder of `ownerPublicKey` vouched THIS membership
130
+ * row (the peer id bound to the row's single-use `StampId` nonce). It says
131
+ * nothing about whether that owner key is itself trustworthy — the caller must
132
+ * separately check the key against the node-local trusted-owner anchor
133
+ * (`TrustedOwnerStore`), never the replicated `OwnerKey` table.
134
+ *
135
+ * Returns a boolean and never throws (same contract as
136
+ * {@link verifyPeerAuthorization}): malformed input or any crypto failure
137
+ * resolves to `false`, logged at debug.
138
+ */
139
+ export declare function verifyCadrePeerVoucher(peerId: string, stampId: string, ownerPublicKey: string, signature: string): boolean;
140
+ /**
141
+ * Canonical digest a JOINING peer signs to consent to ONE `FormationUsage`
142
+ * redemption — the read-side mirror of `formationConsentMessage` in
143
+ * control-database.ts, base64url-encoded instead of raw bytes (see
144
+ * {@link taggedDigest}). That doc comment carries the field-vector rationale
145
+ * (notably why `strandId` is not signed); the two vectors must not drift, which
146
+ * peer-authorization.spec.ts pins by signing one form and verifying the other.
147
+ */
148
+ export declare function formationConsentDigest(token: string, usageStampId: string, peerKey: string, disclosure: string): string;
149
+ /**
150
+ * Verify that `row.peerSig` is a valid ed25519 signature over the joining peer's
151
+ * consent digest (see {@link formationConsentDigest}) — the row's OWN `peerKey`.
152
+ *
153
+ * Unlike {@link verifyCadrePeerVoucher} there is no separate enrolled/owner row to
154
+ * look up: the identity IS the key carried on the row, so a forged consent would
155
+ * need that joiner's own private key. Returns a boolean and never throws (same
156
+ * contract as the siblings above): malformed input or any crypto failure resolves
157
+ * to `false`, logged at debug.
158
+ */
159
+ export declare function verifyFormationConsent(row: {
160
+ token: string;
161
+ usageStampId: string;
162
+ peerKey: string;
163
+ disclosure: string;
164
+ peerSig: string;
165
+ }): boolean;