@serfab/cadre-core 1.14.0 → 1.16.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 (171) hide show
  1. package/README.md +293 -274
  2. package/dist/cadre-invite-protocol.d.ts +64 -29
  3. package/dist/cadre-invite-protocol.js +207 -38
  4. package/dist/cadre-invite-protocol.js.map +1 -1
  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 +375 -79
  9. package/dist/cadre-node.js +758 -186
  10. package/dist/cadre-node.js.map +1 -1
  11. package/dist/control-database.d.ts +92 -12
  12. package/dist/control-database.js +323 -142
  13. package/dist/control-database.js.map +1 -1
  14. package/dist/control-schema.d.ts +1 -1
  15. package/dist/control-schema.js +19 -3
  16. package/dist/control-schema.js.map +1 -1
  17. package/dist/ed25519-key.d.ts +7 -9
  18. package/dist/ed25519-key.js +20 -10
  19. package/dist/ed25519-key.js.map +1 -1
  20. package/dist/fetch-impl.d.ts +12 -0
  21. package/dist/fetch-impl.js +23 -0
  22. package/dist/fetch-impl.js.map +1 -0
  23. package/dist/file-durable-slot.js +2 -1
  24. package/dist/file-durable-slot.js.map +1 -1
  25. package/dist/file-safe-name.d.ts +25 -0
  26. package/dist/file-safe-name.js +29 -0
  27. package/dist/file-safe-name.js.map +1 -0
  28. package/dist/formation-approval.js +5 -11
  29. package/dist/formation-approval.js.map +1 -1
  30. package/dist/fs-atomic.d.ts +0 -19
  31. package/dist/fs-atomic.js +3 -24
  32. package/dist/fs-atomic.js.map +1 -1
  33. package/dist/index.d.ts +12 -5
  34. package/dist/index.js +31 -6
  35. package/dist/index.js.map +1 -1
  36. package/dist/invitation-bootstrap.d.ts +50 -12
  37. package/dist/invitation-bootstrap.js +76 -13
  38. package/dist/invitation-bootstrap.js.map +1 -1
  39. package/dist/joined-strand-store.d.ts +7 -0
  40. package/dist/joined-strand-store.js +9 -0
  41. package/dist/joined-strand-store.js.map +1 -1
  42. package/dist/key-store-file.js +2 -1
  43. package/dist/key-store-file.js.map +1 -1
  44. package/dist/link-budget.d.ts +9 -4
  45. package/dist/link-budget.js +9 -4
  46. package/dist/link-budget.js.map +1 -1
  47. package/dist/machine-removal.d.ts +78 -0
  48. package/dist/machine-removal.js +166 -0
  49. package/dist/machine-removal.js.map +1 -0
  50. package/dist/membership-connection-gater.d.ts +6 -5
  51. package/dist/membership-connection-gater.js +6 -5
  52. package/dist/membership-connection-gater.js.map +1 -1
  53. package/dist/node-claim-payload.d.ts +55 -0
  54. package/dist/node-claim-payload.js +108 -4
  55. package/dist/node-claim-payload.js.map +1 -1
  56. package/dist/node-default-transports.d.ts +27 -0
  57. package/dist/node-default-transports.js +35 -0
  58. package/dist/node-default-transports.js.map +1 -0
  59. package/dist/node-transports.d.ts +30 -0
  60. package/dist/node-transports.js +40 -0
  61. package/dist/node-transports.js.map +1 -0
  62. package/dist/peer-dial.d.ts +124 -27
  63. package/dist/peer-dial.js +254 -30
  64. package/dist/peer-dial.js.map +1 -1
  65. package/dist/peer-join-backfill.js +6 -0
  66. package/dist/peer-join-backfill.js.map +1 -1
  67. package/dist/peer-record.d.ts +6 -45
  68. package/dist/peer-record.js +8 -50
  69. package/dist/peer-record.js.map +1 -1
  70. package/dist/primary-lan-address.d.ts +8 -0
  71. package/dist/primary-lan-address.js +46 -0
  72. package/dist/primary-lan-address.js.map +1 -0
  73. package/dist/provider/client.d.ts +131 -0
  74. package/dist/provider/client.js +226 -0
  75. package/dist/provider/client.js.map +1 -0
  76. package/dist/provider/descriptor.d.ts +41 -0
  77. package/dist/provider/descriptor.js +103 -0
  78. package/dist/provider/descriptor.js.map +1 -0
  79. package/dist/provider/index.d.ts +9 -0
  80. package/dist/provider/index.js +10 -0
  81. package/dist/provider/index.js.map +1 -0
  82. package/dist/provider/oauth.d.ts +125 -0
  83. package/dist/provider/oauth.js +199 -0
  84. package/dist/provider/oauth.js.map +1 -0
  85. package/dist/provider/rent-node.d.ts +101 -0
  86. package/dist/provider/rent-node.js +220 -0
  87. package/dist/provider/rent-node.js.map +1 -0
  88. package/dist/relay-addrs.d.ts +9 -4
  89. package/dist/relay-addrs.js +9 -4
  90. package/dist/relay-addrs.js.map +1 -1
  91. package/dist/relay-reservation.d.ts +51 -16
  92. package/dist/relay-reservation.js +107 -24
  93. package/dist/relay-reservation.js.map +1 -1
  94. package/dist/seed-bootstrap.d.ts +22 -5
  95. package/dist/seed-bootstrap.js +48 -25
  96. package/dist/seed-bootstrap.js.map +1 -1
  97. package/dist/strand-commit-durability.d.ts +54 -0
  98. package/dist/strand-commit-durability.js +74 -0
  99. package/dist/strand-commit-durability.js.map +1 -0
  100. package/dist/strand-database.d.ts +23 -8
  101. package/dist/strand-database.js +41 -13
  102. package/dist/strand-database.js.map +1 -1
  103. package/dist/strand-first-sync-gate.d.ts +7 -1
  104. package/dist/strand-first-sync-gate.js +7 -1
  105. package/dist/strand-first-sync-gate.js.map +1 -1
  106. package/dist/strand-formation-protocol.js +4 -1
  107. package/dist/strand-formation-protocol.js.map +1 -1
  108. package/dist/strand-fret-addrs.d.ts +19 -1
  109. package/dist/strand-fret-addrs.js +27 -9
  110. package/dist/strand-fret-addrs.js.map +1 -1
  111. package/dist/strand-instance-manager.d.ts +81 -19
  112. package/dist/strand-instance-manager.js +216 -105
  113. package/dist/strand-instance-manager.js.map +1 -1
  114. package/dist/strand-move.d.ts +124 -0
  115. package/dist/strand-move.js +438 -0
  116. package/dist/strand-move.js.map +1 -0
  117. package/dist/strand-network-config.js +7 -6
  118. package/dist/strand-network-config.js.map +1 -1
  119. package/dist/strand-peer-redial.d.ts +113 -0
  120. package/dist/strand-peer-redial.js +236 -0
  121. package/dist/strand-peer-redial.js.map +1 -0
  122. package/dist/strand-wake-protocol.js +10 -7
  123. package/dist/strand-wake-protocol.js.map +1 -1
  124. package/dist/types.d.ts +212 -4
  125. package/dist/types.js +10 -0
  126. package/dist/types.js.map +1 -1
  127. package/package.json +18 -10
  128. package/src/cadre-invite-protocol.ts +1052 -864
  129. package/src/cadre-join-request.ts +167 -0
  130. package/src/cadre-node.ts +10003 -9381
  131. package/src/control-database.ts +4663 -4451
  132. package/src/control-schema.ts +19 -3
  133. package/src/ed25519-key.ts +20 -10
  134. package/src/fetch-impl.ts +23 -0
  135. package/src/file-durable-slot.ts +64 -63
  136. package/src/file-safe-name.ts +32 -0
  137. package/src/formation-approval.ts +604 -610
  138. package/src/fs-atomic.ts +92 -117
  139. package/src/index.ts +71 -4
  140. package/src/invitation-bootstrap.ts +91 -13
  141. package/src/joined-strand-store.ts +508 -498
  142. package/src/key-store-file.ts +142 -141
  143. package/src/link-budget.ts +9 -4
  144. package/src/machine-removal.ts +216 -0
  145. package/src/membership-connection-gater.ts +638 -637
  146. package/src/node-claim-payload.ts +138 -4
  147. package/src/node-default-transports.ts +38 -0
  148. package/src/node-transports.ts +49 -0
  149. package/src/peer-dial.ts +604 -336
  150. package/src/peer-join-backfill.ts +6 -0
  151. package/src/peer-record.ts +159 -202
  152. package/src/primary-lan-address.ts +47 -0
  153. package/src/provider/client.ts +326 -0
  154. package/src/provider/descriptor.ts +129 -0
  155. package/src/provider/index.ts +59 -0
  156. package/src/provider/oauth.ts +285 -0
  157. package/src/provider/rent-node.ts +275 -0
  158. package/src/relay-addrs.ts +9 -4
  159. package/src/relay-reservation.ts +1211 -1121
  160. package/src/seed-bootstrap.ts +1502 -1471
  161. package/src/strand-commit-durability.ts +116 -0
  162. package/src/strand-database.ts +46 -14
  163. package/src/strand-first-sync-gate.ts +7 -1
  164. package/src/strand-formation-protocol.ts +1039 -1036
  165. package/src/strand-fret-addrs.ts +179 -145
  166. package/src/strand-instance-manager.ts +2026 -1883
  167. package/src/strand-move.ts +585 -0
  168. package/src/strand-network-config.ts +7 -6
  169. package/src/strand-peer-redial.ts +307 -0
  170. package/src/strand-wake-protocol.ts +10 -7
  171. package/src/types.ts +220 -4
@@ -34,6 +34,12 @@ import { peerJoinPushBudget } from './link-budget.js';
34
34
  // If Optimystic ever grows a cohort-join catch-up of its own, delete this module rather
35
35
  // than running two.
36
36
  //
37
+ // NOTE: strand nodes pass db-p2p no `kvStore`, so Optimystic's under-replication ledger (the
38
+ // copies a node owes peers) dies at every restart. This module's whole-store push on connect is
39
+ // what delivers a write a machine made alone after it restarts (`joiner-first-commit-over-slow-relay`,
40
+ // restart arm). If this module is narrowed (the cohort filter above) or deleted, give strand
41
+ // nodes a persistent kvStore first.
42
+ //
37
43
  // Membership — the one place the two networks differ, expressed as the optional
38
44
  // `authorizePeer` dep:
39
45
  //
@@ -1,202 +1,159 @@
1
- /**
2
- * Peer-address record helpers: the single source of truth for the bytes a node
3
- * signs when publishing its own `CadrePeer` row and re-verifies when resolving
4
- * another peer's row.
5
- *
6
- * The signed payload is a single SHA-256 digest over the shared domain-tagged
7
- * field vector (see control-authorization.ts) — the crypto plugin's injective
8
- * multi-field encoding, so no field split is ambiguous. It is intentionally NOT
9
- * JSON-canonicalized (deterministic across node/browser/RN, no key ordering)
10
- * and — crucially — is reconstructable inside the `CadrePeer.AuthorizedUpdate`
11
- * SQL constraint from the row's own columns and the node-local `party_id()`:
12
- *
13
- * digest('CadreControl.CadrePeer', 'publish', party_id(),
14
- * new.PeerId, new.Multiaddr, cast(new.UpdatedAt as text))
15
- *
16
- * Keep {@link peerRecordSignedPayload} and that constraint byte-for-byte in
17
- * sync. The `'publish'` action tag marks this as a peer's SELF-signed record
18
- * (signed with the peer's own key, not an owner key) and keeps it disjoint from
19
- * every owner-signed digest; the party id keeps a record published into one
20
- * party's table from verifying in another's. A node signs and verifies with its
21
- * own party: these records come only from its own party's control database.
22
- *
23
- * `publicKey` is deliberately excluded from the payload — it is the key the
24
- * signature is verified *with* (so a signature already commits to exactly one
25
- * key), and its binding to `peerId` is checked separately at resolve time.
26
- */
27
-
28
- import type { Multiaddr } from '@multiformats/multiaddr';
29
- import { digest, sign, verify } from '@optimystic/quereus-plugin-crypto';
30
- import { controlAuthorizationFields } from './control-authorization.js';
31
- import type { PeerAddressRecord, PeerResolveTrustPolicy } from './types.js';
32
-
33
- /**
34
- * Default freshness ceiling for resolved peer-address records (15 minutes).
35
- * A record older than this is treated as stale and filtered out, so a resolver
36
- * never hands back a dead relay reservation. The publish path re-stamps at
37
- * roughly half this interval (see {@link DEFAULT_PEER_RECORD_HEARTBEAT_MS}).
38
- */
39
- export const DEFAULT_PEER_RECORD_MAX_AGE_MS = 15 * 60 * 1000;
40
-
41
- /**
42
- * TTL heartbeat interval for re-publishing the self record — half the freshness
43
- * ceiling, so a record is refreshed well before it can go stale.
44
- */
45
- export const DEFAULT_PEER_RECORD_HEARTBEAT_MS = DEFAULT_PEER_RECORD_MAX_AGE_MS / 2;
46
-
47
- /** The signaling/relay prefix a WebRTC (or any relayed) dial path consumes. */
48
- const SIGNALING_PREFIX = '/p2p-circuit';
49
-
50
- /**
51
- * Build the base64url SHA-256 digest that the self-signature covers. Mirrors the
52
- * `CadrePeer.AuthorizedUpdate` constraint exactly; both sides take the default
53
- * base64url output of a single `digest(...)`, which round-trips cleanly (unlike
54
- * concatenating several base64url digests).
55
- *
56
- * @param partyId - the party whose `CadrePeer` table the record is published into
57
- * @param peerId - base58btc libp2p peer id (the row key)
58
- * @param multiaddr - the comma-joined `Multiaddr` string EXACTLY as stored
59
- * @param updatedAt - epoch-ms freshness stamp
60
- */
61
- export function peerRecordSignedPayload(partyId: string, peerId: string, multiaddr: string, updatedAt: number): string {
62
- const fields = controlAuthorizationFields('CadreControl.CadrePeer', 'publish', partyId, [peerId, multiaddr, String(updatedAt)]);
63
- return digest(fields, 'sha256', 'base64url') as string;
64
- }
65
-
66
- /**
67
- * Sign a peer-address record with the ed25519 private key behind its `peerId`.
68
- * Returns a fully-populated {@link PeerAddressRecord} (the `addrs` order is
69
- * preserved and is the order signed over).
70
- *
71
- * @param partyId - the party whose `CadrePeer` table the record is published into
72
- * @param fields - the record fields to sign (signaling addr first by convention)
73
- * @param privateKeyB64 - base64url ed25519 seed (see `ed25519KeyPairFromLibp2p`)
74
- */
75
- export function signPeerRecord(
76
- partyId: string,
77
- fields: { peerId: string; publicKey: string; addrs: string[]; updatedAt: number },
78
- privateKeyB64: string
79
- ): PeerAddressRecord {
80
- const payloadDigest = peerRecordSignedPayload(partyId, fields.peerId, fields.addrs.join(','), fields.updatedAt);
81
- const sig = sign(
82
- payloadDigest,
83
- privateKeyB64,
84
- 'ed25519',
85
- 'base64url',
86
- 'base64url',
87
- 'base64url'
88
- ) as string;
89
- return { peerId: fields.peerId, publicKey: fields.publicKey, addrs: [...fields.addrs], updatedAt: fields.updatedAt, sig };
90
- }
91
-
92
- /**
93
- * Verify a record's self-signature against its own `publicKey`, for `partyId`.
94
- * Reconstructs the signed bytes from the record exactly as {@link signPeerRecord}
95
- * produced them. Returns false on a missing key/sig or any verification failure.
96
- */
97
- export function verifyPeerRecordSignature(partyId: string, record: PeerAddressRecord): boolean {
98
- if (!record.publicKey || !record.sig) {
99
- return false;
100
- }
101
- const payloadDigest = peerRecordSignedPayload(partyId, record.peerId, record.addrs.join(','), record.updatedAt);
102
- return verify(
103
- payloadDigest,
104
- record.sig,
105
- record.publicKey,
106
- 'ed25519',
107
- 'base64url',
108
- 'base64url',
109
- 'base64url'
110
- );
111
- }
112
-
113
- /**
114
- * Freshness predicate: true when `updatedAt` is a positive stamp within
115
- * `maxAgeMs` of `now`. A non-positive `updatedAt` (never self-published) is
116
- * never fresh.
117
- */
118
- export function isPeerRecordFresh(updatedAt: number, maxAgeMs: number, now: number): boolean {
119
- if (!(updatedAt > 0)) {
120
- return false;
121
- }
122
- return now - updatedAt <= maxAgeMs;
123
- }
124
-
125
- /** True if `addr` is a `/p2p-circuit` signaling/relay multiaddr. */
126
- export function isSignalingAddr(addr: string): boolean {
127
- return addr.includes(SIGNALING_PREFIX);
128
- }
129
-
130
- /**
131
- * The peer id a multiaddr terminates in, or `null` when it names none.
132
- *
133
- * The LAST `/p2p/` component is the dial target: a circuit address
134
- * (`…/p2p/<relay>/p2p-circuit/p2p/<target>`) names the relay first and the peer
135
- * being reached last. Replaces the deprecated `Multiaddr.getPeerId()`, which
136
- * cannot express that distinction.
137
- */
138
- export function trailingPeerId(addr: Multiaddr): string | null {
139
- const p2p = addr.getComponents().filter((c) => c.name === 'p2p');
140
- return p2p.length > 0 ? (p2p[p2p.length - 1].value ?? null) : null;
141
- }
142
-
143
- /**
144
- * `addr` guaranteed to terminate in `/p2p/<peerId>` — the shape every dial path
145
- * needs — or `null` when it cannot, because the address already terminates in a
146
- * DIFFERENT peer id and therefore does not reach `peerId` at all.
147
- *
148
- * Three input shapes, matching what a `CadrePeer` record can legitimately carry:
149
- *
150
- * - no `/p2p/` component (`/ip4/1.2.3.4/tcp/4001`) — the suffix is appended;
151
- * - a relay hop with the destination missing (`…/p2p/<relay>/p2p-circuit`) — the
152
- * last component is the circuit, not a peer id, so the suffix is appended and
153
- * the address becomes the full `…/p2p/<relay>/p2p-circuit/p2p/<peerId>`;
154
- * - already terminating in `/p2p/<X>` — returned untouched when `X` is `peerId`,
155
- * dropped otherwise.
156
- *
157
- * This is deliberately the same rule libp2p applies internally in
158
- * `calculateMultiaddrs` (append when the LAST component is not `p2p`) followed by
159
- * its wrong-peer-id filter, so a normalized list matches what libp2p would have
160
- * built anyway. Normalizing before the dial is what keeps a list from mixing
161
- * suffixed and unsuffixed entries, which `getPeerAddress` rejects outright
162
- * ("Multiaddrs must all have the same peer id or have no peer id") — taking the
163
- * whole peer down with it, not just the odd address.
164
- *
165
- * Never use before signature verification: verification runs against the
166
- * original on-record strings.
167
- */
168
- export function withTrailingPeerId(addr: Multiaddr, peerId: string): Multiaddr | null {
169
- const components = addr.getComponents();
170
- if (components[components.length - 1]?.name !== 'p2p') {
171
- return addr.encapsulate(`/p2p/${peerId}`);
172
- }
173
- return trailingPeerId(addr) === peerId ? addr : null;
174
- }
175
-
176
- /**
177
- * Return `addrs` with signaling (`/p2p-circuit`) addrs first, otherwise stable.
178
- * Used to present the WebRTC dial input ahead of direct addrs. Does NOT mutate
179
- * the input and must not be used before signature verification (verification
180
- * uses the original on-record order).
181
- */
182
- export function orderSignalingFirst(addrs: string[]): string[] {
183
- const signaling = addrs.filter(isSignalingAddr);
184
- const rest = addrs.filter(a => !isSignalingAddr(a));
185
- return [...signaling, ...rest];
186
- }
187
-
188
- /**
189
- * Default resolve trust gate: trust any peer that has a `CadrePeer` row.
190
- *
191
- * A row only exists because an owner signed its `AuthorizedInsert`, so row
192
- * presence already means "owner-vouched member". This is the seam where a
193
- * stricter policy (trust circle, pinned keys) plugs in — see
194
- * {@link PeerResolveTrustPolicy}.
195
- */
196
- export function currentMemberTrustPolicy(): PeerResolveTrustPolicy {
197
- return {
198
- evaluate() {
199
- return true;
200
- },
201
- };
202
- }
1
+ /**
2
+ * Peer-address record helpers: the single source of truth for the bytes a node
3
+ * signs when publishing its own `CadrePeer` row and re-verifies when resolving
4
+ * another peer's row.
5
+ *
6
+ * The signed payload is a single SHA-256 digest over the shared domain-tagged
7
+ * field vector (see control-authorization.ts) — the crypto plugin's injective
8
+ * multi-field encoding, so no field split is ambiguous. It is intentionally NOT
9
+ * JSON-canonicalized (deterministic across node/browser/RN, no key ordering)
10
+ * and — crucially — is reconstructable inside the `CadrePeer.AuthorizedUpdate`
11
+ * SQL constraint from the row's own columns and the node-local `party_id()`:
12
+ *
13
+ * digest('CadreControl.CadrePeer', 'publish', party_id(),
14
+ * new.PeerId, new.Multiaddr, cast(new.UpdatedAt as text),
15
+ * coalesce(new.Profile, ''))
16
+ *
17
+ * Keep {@link peerRecordSignedPayload} and that constraint byte-for-byte in
18
+ * sync. The `'publish'` action tag marks this as a peer's SELF-signed record
19
+ * (signed with the peer's own key, not an owner key) and keeps it disjoint from
20
+ * every owner-signed digest; the party id keeps a record published into one
21
+ * party's table from verifying in another's. A node signs and verifies with its
22
+ * own party: these records come only from its own party's control database.
23
+ *
24
+ * `publicKey` is deliberately excluded from the payload — it is the key the
25
+ * signature is verified *with* (so a signature already commits to exactly one
26
+ * key), and its binding to `peerId` is checked separately at resolve time.
27
+ */
28
+
29
+ import { digest, sign, verify } from '@optimystic/quereus-plugin-crypto';
30
+ import { controlAuthorizationFields } from './control-authorization.js';
31
+ import type { NodeProfile, PeerAddressRecord, PeerResolveTrustPolicy } from './types.js';
32
+
33
+ /**
34
+ * Default freshness ceiling for resolved peer-address records (15 minutes).
35
+ * A record older than this is treated as stale and filtered out, so a resolver
36
+ * never hands back a dead relay reservation. The publish path re-stamps at
37
+ * roughly half this interval (see {@link DEFAULT_PEER_RECORD_HEARTBEAT_MS}).
38
+ */
39
+ export const DEFAULT_PEER_RECORD_MAX_AGE_MS = 15 * 60 * 1000;
40
+
41
+ /**
42
+ * TTL heartbeat interval for re-publishing the self record — half the freshness
43
+ * ceiling, so a record is refreshed well before it can go stale.
44
+ */
45
+ export const DEFAULT_PEER_RECORD_HEARTBEAT_MS = DEFAULT_PEER_RECORD_MAX_AGE_MS / 2;
46
+
47
+ /** The signaling/relay prefix a WebRTC (or any relayed) dial path consumes. */
48
+ const SIGNALING_PREFIX = '/p2p-circuit';
49
+
50
+ /**
51
+ * Build the base64url SHA-256 digest that the self-signature covers. Mirrors the
52
+ * `CadrePeer.AuthorizedUpdate` constraint exactly; both sides take the default
53
+ * base64url output of a single `digest(...)`, which round-trips cleanly (unlike
54
+ * concatenating several base64url digests).
55
+ *
56
+ * @param partyId - the party whose `CadrePeer` table the record is published into
57
+ * @param peerId - base58btc libp2p peer id (the row key)
58
+ * @param multiaddr - the comma-joined `Multiaddr` string EXACTLY as stored
59
+ * @param updatedAt - epoch-ms freshness stamp
60
+ * @param profile - the publisher's node profile; null signs as `''`, as the constraint coalesces it
61
+ */
62
+ export function peerRecordSignedPayload(
63
+ partyId: string, peerId: string, multiaddr: string, updatedAt: number, profile: NodeProfile | null
64
+ ): string {
65
+ const fields = controlAuthorizationFields('CadreControl.CadrePeer', 'publish', partyId, [peerId, multiaddr, String(updatedAt), profile ?? '']);
66
+ return digest(fields, 'sha256', 'base64url') as string;
67
+ }
68
+
69
+ /**
70
+ * Sign a peer-address record with the ed25519 private key behind its `peerId`.
71
+ * Returns a fully-populated {@link PeerAddressRecord} (the `addrs` order is
72
+ * preserved and is the order signed over).
73
+ *
74
+ * @param partyId - the party whose `CadrePeer` table the record is published into
75
+ * @param fields - the record fields to sign (signaling addr first by convention)
76
+ * @param privateKeyB64 - base64url ed25519 seed (see `ed25519KeyPairFromLibp2p`)
77
+ */
78
+ export function signPeerRecord(
79
+ partyId: string,
80
+ fields: Omit<PeerAddressRecord, 'sig'>,
81
+ privateKeyB64: string
82
+ ): PeerAddressRecord {
83
+ const payloadDigest = peerRecordSignedPayload(partyId, fields.peerId, fields.addrs.join(','), fields.updatedAt, fields.profile);
84
+ const sig = sign(
85
+ payloadDigest,
86
+ privateKeyB64,
87
+ 'ed25519',
88
+ 'base64url',
89
+ 'base64url',
90
+ 'base64url'
91
+ ) as string;
92
+ return { peerId: fields.peerId, publicKey: fields.publicKey, addrs: [...fields.addrs], updatedAt: fields.updatedAt, profile: fields.profile, sig };
93
+ }
94
+
95
+ /**
96
+ * Verify a record's self-signature against its own `publicKey`, for `partyId`.
97
+ * Reconstructs the signed bytes from the record exactly as {@link signPeerRecord}
98
+ * produced them. Returns false on a missing key/sig or any verification failure.
99
+ */
100
+ export function verifyPeerRecordSignature(partyId: string, record: PeerAddressRecord): boolean {
101
+ if (!record.publicKey || !record.sig) {
102
+ return false;
103
+ }
104
+ const payloadDigest = peerRecordSignedPayload(partyId, record.peerId, record.addrs.join(','), record.updatedAt, record.profile);
105
+ return verify(
106
+ payloadDigest,
107
+ record.sig,
108
+ record.publicKey,
109
+ 'ed25519',
110
+ 'base64url',
111
+ 'base64url',
112
+ 'base64url'
113
+ );
114
+ }
115
+
116
+ /**
117
+ * Freshness predicate: true when `updatedAt` is a positive stamp within
118
+ * `maxAgeMs` of `now`. A non-positive `updatedAt` (never self-published) is
119
+ * never fresh.
120
+ */
121
+ export function isPeerRecordFresh(updatedAt: number, maxAgeMs: number, now: number): boolean {
122
+ if (!(updatedAt > 0)) {
123
+ return false;
124
+ }
125
+ return now - updatedAt <= maxAgeMs;
126
+ }
127
+
128
+ /** True if `addr` is a `/p2p-circuit` signaling/relay multiaddr. */
129
+ export function isSignalingAddr(addr: string): boolean {
130
+ return addr.includes(SIGNALING_PREFIX);
131
+ }
132
+
133
+ /**
134
+ * Return `addrs` with signaling (`/p2p-circuit`) addrs first, otherwise stable.
135
+ * Used to present the WebRTC dial input ahead of direct addrs. Does NOT mutate
136
+ * the input and must not be used before signature verification (verification
137
+ * uses the original on-record order).
138
+ */
139
+ export function orderSignalingFirst(addrs: string[]): string[] {
140
+ const signaling = addrs.filter(isSignalingAddr);
141
+ const rest = addrs.filter(a => !isSignalingAddr(a));
142
+ return [...signaling, ...rest];
143
+ }
144
+
145
+ /**
146
+ * Default resolve trust gate: trust any peer that has a `CadrePeer` row.
147
+ *
148
+ * A row only exists because an owner signed its `AuthorizedInsert`, so row
149
+ * presence already means "owner-vouched member". This is the seam where a
150
+ * stricter policy (trust circle, pinned keys) plugs in — see
151
+ * {@link PeerResolveTrustPolicy}.
152
+ */
153
+ export function currentMemberTrustPolicy(): PeerResolveTrustPolicy {
154
+ return {
155
+ evaluate() {
156
+ return true;
157
+ },
158
+ };
159
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Node-only: the machine's main LAN address, for {@link selectNodeClaimAddresses}'s `lan`.
3
+ *
4
+ * The address the OS would send from to reach the internet, learned by "connecting" a UDP
5
+ * socket to a public address: a UDP connect only picks a route and sends nothing. On a
6
+ * server that also has Docker bridges, a VPN or other interfaces, that is the one a phone on
7
+ * the same network reaches. A subpath module (`@serfab/cadre-core/primary-lan-address`), like
8
+ * `key-store-file`, so `node:dgram` never reaches a React Native bundle.
9
+ */
10
+ import dgram from 'node:dgram';
11
+ import debug from 'debug';
12
+
13
+ const log = debug('sereus:cadre:primary-lan-address');
14
+
15
+ /** Any public IPv4 address works: nothing is sent to it. */
16
+ const ROUTE_PROBE = { host: '1.1.1.1', port: 53 } as const;
17
+
18
+ /**
19
+ * The machine's primary IPv4 address, or undefined when there is no route (offline) or the
20
+ * probe fails. Callers then fall back to keeping every LAN address. `probe` is for tests.
21
+ */
22
+ export async function primaryLanAddress(probe: { host: string; port: number } = ROUTE_PROBE): Promise<string | undefined> {
23
+ return new Promise((resolve) => {
24
+ const socket = dgram.createSocket('udp4');
25
+ let settled = false;
26
+ const finish = (address?: string): void => {
27
+ if (settled) return;
28
+ settled = true;
29
+ socket.close();
30
+ resolve(address);
31
+ };
32
+ socket.on('error', (err) => {
33
+ log('route probe failed: %s', err.message);
34
+ finish();
35
+ });
36
+ socket.connect(probe.port, probe.host, (err?: Error) => {
37
+ // A failed connect reports here, not as 'error', and leaves the socket bound to
38
+ // 0.0.0.0, so its address() must not be read.
39
+ if (err) {
40
+ log('no route to probe: %s', err.message);
41
+ finish();
42
+ return;
43
+ }
44
+ finish(socket.address().address);
45
+ });
46
+ });
47
+ }