@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
@@ -1,146 +1,164 @@
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 of a delimited string of the
7
- * authenticated fields. It is intentionally NOT JSON-canonicalized: a delimited
8
- * digest is trivially deterministic across node/browser/RN (plain string concat
9
- * + sha256, no key ordering) and — crucially — is reconstructable inside the
10
- * `CadrePeer.AuthorizedUpdate` SQL constraint from the row's own columns:
11
- *
12
- * digest(new.PeerId || '|' || new.Multiaddr || '|' || cast(new.UpdatedAt as text))
13
- *
14
- * Keep {@link peerRecordSignedPayload} and that constraint byte-for-byte in
15
- * sync. The `'|'` delimiter is safe: a base58btc PeerId, a multiaddr (which uses
16
- * `/`), and a base-10 integer never contain it.
17
- *
18
- * `publicKey` is deliberately excluded from the payload — it is the key the
19
- * signature is verified *with* (so a signature already commits to exactly one
20
- * key), and its binding to `peerId` is checked separately at resolve time.
21
- */
22
-
23
- import { digest, sign, verify } from '@optimystic/quereus-plugin-crypto';
24
- import type { PeerAddressRecord, PeerResolveTrustPolicy } from './types.js';
25
-
26
- /**
27
- * Default freshness ceiling for resolved peer-address records (15 minutes).
28
- * A record older than this is treated as stale and filtered out, so a resolver
29
- * never hands back a dead relay reservation. The publish path re-stamps at
30
- * roughly half this interval (see {@link DEFAULT_PEER_RECORD_HEARTBEAT_MS}).
31
- */
32
- export const DEFAULT_PEER_RECORD_MAX_AGE_MS = 15 * 60 * 1000;
33
-
34
- /**
35
- * TTL heartbeat interval for re-publishing the self record — half the freshness
36
- * ceiling, so a record is refreshed well before it can go stale.
37
- */
38
- export const DEFAULT_PEER_RECORD_HEARTBEAT_MS = DEFAULT_PEER_RECORD_MAX_AGE_MS / 2;
39
-
40
- /** The signaling/relay prefix a WebRTC (or any relayed) dial path consumes. */
41
- const SIGNALING_PREFIX = '/p2p-circuit';
42
-
43
- /**
44
- * Build the base64url SHA-256 digest that the self-signature covers. Mirrors the
45
- * `CadrePeer.AuthorizedUpdate` constraint exactly; both sides take the default
46
- * base64url output of a single `digest(...)`, which round-trips cleanly (unlike
47
- * concatenating several base64url digests).
48
- *
49
- * @param peerId - base58btc libp2p peer id (the row key)
50
- * @param multiaddr - the comma-joined `Multiaddr` string EXACTLY as stored
51
- * @param updatedAt - epoch-ms freshness stamp
52
- */
53
- export function peerRecordSignedPayload(peerId: string, multiaddr: string, updatedAt: number): string {
54
- const joined = `${peerId}|${multiaddr}|${updatedAt}`;
55
- return digest([joined], 'sha256', 'base64url') as string;
56
- }
57
-
58
- /**
59
- * Sign a peer-address record with the ed25519 private key behind its `peerId`.
60
- * Returns a fully-populated {@link PeerAddressRecord} (the `addrs` order is
61
- * preserved and is the order signed over).
62
- *
63
- * @param fields - the record fields to sign (signaling addr first by convention)
64
- * @param privateKeyB64 - base64url ed25519 seed (see `ed25519KeyPairFromLibp2p`)
65
- */
66
- export function signPeerRecord(
67
- fields: { peerId: string; publicKey: string; addrs: string[]; updatedAt: number },
68
- privateKeyB64: string
69
- ): PeerAddressRecord {
70
- const payloadDigest = peerRecordSignedPayload(fields.peerId, fields.addrs.join(','), fields.updatedAt);
71
- const sig = sign(
72
- payloadDigest,
73
- privateKeyB64,
74
- 'ed25519',
75
- 'base64url',
76
- 'base64url',
77
- 'base64url'
78
- ) as string;
79
- return { peerId: fields.peerId, publicKey: fields.publicKey, addrs: [...fields.addrs], updatedAt: fields.updatedAt, sig };
80
- }
81
-
82
- /**
83
- * Verify a record's self-signature against its own `publicKey`. Reconstructs the
84
- * signed bytes from the record exactly as {@link signPeerRecord} produced them.
85
- * Returns false on a missing key/sig or any verification failure.
86
- */
87
- export function verifyPeerRecordSignature(record: PeerAddressRecord): boolean {
88
- if (!record.publicKey || !record.sig) {
89
- return false;
90
- }
91
- const payloadDigest = peerRecordSignedPayload(record.peerId, record.addrs.join(','), record.updatedAt);
92
- return verify(
93
- payloadDigest,
94
- record.sig,
95
- record.publicKey,
96
- 'ed25519',
97
- 'base64url',
98
- 'base64url',
99
- 'base64url'
100
- );
101
- }
102
-
103
- /**
104
- * Freshness predicate: true when `updatedAt` is a positive stamp within
105
- * `maxAgeMs` of `now`. A non-positive `updatedAt` (never self-published) is
106
- * never fresh.
107
- */
108
- export function isPeerRecordFresh(updatedAt: number, maxAgeMs: number, now: number): boolean {
109
- if (!(updatedAt > 0)) {
110
- return false;
111
- }
112
- return now - updatedAt <= maxAgeMs;
113
- }
114
-
115
- /** True if `addr` is a `/p2p-circuit` signaling/relay multiaddr. */
116
- export function isSignalingAddr(addr: string): boolean {
117
- return addr.includes(SIGNALING_PREFIX);
118
- }
119
-
120
- /**
121
- * Return `addrs` with signaling (`/p2p-circuit`) addrs first, otherwise stable.
122
- * Used to present the WebRTC dial input ahead of direct addrs. Does NOT mutate
123
- * the input and must not be used before signature verification (verification
124
- * uses the original on-record order).
125
- */
126
- export function orderSignalingFirst(addrs: string[]): string[] {
127
- const signaling = addrs.filter(isSignalingAddr);
128
- const rest = addrs.filter(a => !isSignalingAddr(a));
129
- return [...signaling, ...rest];
130
- }
131
-
132
- /**
133
- * Default resolve trust gate: trust any peer that has a `CadrePeer` row.
134
- *
135
- * A row only exists because an owner signed its `AuthorizedInsert`, so row
136
- * presence already means "owner-vouched member". This is the seam where a
137
- * stricter policy (trust circle, pinned keys) plugs in — see
138
- * {@link PeerResolveTrustPolicy}.
139
- */
140
- export function currentMemberTrustPolicy(): PeerResolveTrustPolicy {
141
- return {
142
- evaluate() {
143
- return true;
144
- },
145
- };
146
- }
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:
12
+ *
13
+ * digest('CadreControl.CadrePeer', 'publish',
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.
20
+ *
21
+ * `publicKey` is deliberately excluded from the payload — it is the key the
22
+ * signature is verified *with* (so a signature already commits to exactly one
23
+ * key), and its binding to `peerId` is checked separately at resolve time.
24
+ */
25
+
26
+ import type { Multiaddr } from '@multiformats/multiaddr';
27
+ import { digest, sign, verify } from '@optimystic/quereus-plugin-crypto';
28
+ import { controlAuthorizationFields } from './control-authorization.js';
29
+ import type { PeerAddressRecord, PeerResolveTrustPolicy } from './types.js';
30
+
31
+ /**
32
+ * Default freshness ceiling for resolved peer-address records (15 minutes).
33
+ * A record older than this is treated as stale and filtered out, so a resolver
34
+ * never hands back a dead relay reservation. The publish path re-stamps at
35
+ * roughly half this interval (see {@link DEFAULT_PEER_RECORD_HEARTBEAT_MS}).
36
+ */
37
+ export const DEFAULT_PEER_RECORD_MAX_AGE_MS = 15 * 60 * 1000;
38
+
39
+ /**
40
+ * TTL heartbeat interval for re-publishing the self record — half the freshness
41
+ * ceiling, so a record is refreshed well before it can go stale.
42
+ */
43
+ export const DEFAULT_PEER_RECORD_HEARTBEAT_MS = DEFAULT_PEER_RECORD_MAX_AGE_MS / 2;
44
+
45
+ /** The signaling/relay prefix a WebRTC (or any relayed) dial path consumes. */
46
+ const SIGNALING_PREFIX = '/p2p-circuit';
47
+
48
+ /**
49
+ * Build the base64url SHA-256 digest that the self-signature covers. Mirrors the
50
+ * `CadrePeer.AuthorizedUpdate` constraint exactly; both sides take the default
51
+ * base64url output of a single `digest(...)`, which round-trips cleanly (unlike
52
+ * concatenating several base64url digests).
53
+ *
54
+ * @param peerId - base58btc libp2p peer id (the row key)
55
+ * @param multiaddr - the comma-joined `Multiaddr` string EXACTLY as stored
56
+ * @param updatedAt - epoch-ms freshness stamp
57
+ */
58
+ export function peerRecordSignedPayload(peerId: string, multiaddr: string, updatedAt: number): string {
59
+ const fields = controlAuthorizationFields('CadreControl.CadrePeer', 'publish', [peerId, multiaddr, String(updatedAt)]);
60
+ return digest(fields, 'sha256', 'base64url') as string;
61
+ }
62
+
63
+ /**
64
+ * Sign a peer-address record with the ed25519 private key behind its `peerId`.
65
+ * Returns a fully-populated {@link PeerAddressRecord} (the `addrs` order is
66
+ * preserved and is the order signed over).
67
+ *
68
+ * @param fields - the record fields to sign (signaling addr first by convention)
69
+ * @param privateKeyB64 - base64url ed25519 seed (see `ed25519KeyPairFromLibp2p`)
70
+ */
71
+ export function signPeerRecord(
72
+ fields: { peerId: string; publicKey: string; addrs: string[]; updatedAt: number },
73
+ privateKeyB64: string
74
+ ): PeerAddressRecord {
75
+ const payloadDigest = peerRecordSignedPayload(fields.peerId, fields.addrs.join(','), fields.updatedAt);
76
+ const sig = sign(
77
+ payloadDigest,
78
+ privateKeyB64,
79
+ 'ed25519',
80
+ 'base64url',
81
+ 'base64url',
82
+ 'base64url'
83
+ ) as string;
84
+ return { peerId: fields.peerId, publicKey: fields.publicKey, addrs: [...fields.addrs], updatedAt: fields.updatedAt, sig };
85
+ }
86
+
87
+ /**
88
+ * Verify a record's self-signature against its own `publicKey`. Reconstructs the
89
+ * signed bytes from the record exactly as {@link signPeerRecord} produced them.
90
+ * Returns false on a missing key/sig or any verification failure.
91
+ */
92
+ export function verifyPeerRecordSignature(record: PeerAddressRecord): boolean {
93
+ if (!record.publicKey || !record.sig) {
94
+ return false;
95
+ }
96
+ const payloadDigest = peerRecordSignedPayload(record.peerId, record.addrs.join(','), record.updatedAt);
97
+ return verify(
98
+ payloadDigest,
99
+ record.sig,
100
+ record.publicKey,
101
+ 'ed25519',
102
+ 'base64url',
103
+ 'base64url',
104
+ 'base64url'
105
+ );
106
+ }
107
+
108
+ /**
109
+ * Freshness predicate: true when `updatedAt` is a positive stamp within
110
+ * `maxAgeMs` of `now`. A non-positive `updatedAt` (never self-published) is
111
+ * never fresh.
112
+ */
113
+ export function isPeerRecordFresh(updatedAt: number, maxAgeMs: number, now: number): boolean {
114
+ if (!(updatedAt > 0)) {
115
+ return false;
116
+ }
117
+ return now - updatedAt <= maxAgeMs;
118
+ }
119
+
120
+ /** True if `addr` is a `/p2p-circuit` signaling/relay multiaddr. */
121
+ export function isSignalingAddr(addr: string): boolean {
122
+ return addr.includes(SIGNALING_PREFIX);
123
+ }
124
+
125
+ /**
126
+ * The peer id a multiaddr terminates in, or `null` when it names none.
127
+ *
128
+ * The LAST `/p2p/` component is the dial target: a circuit address
129
+ * (`…/p2p/<relay>/p2p-circuit/p2p/<target>`) names the relay first and the peer
130
+ * being reached last. Replaces the deprecated `Multiaddr.getPeerId()`, which
131
+ * cannot express that distinction.
132
+ */
133
+ export function trailingPeerId(addr: Multiaddr): string | null {
134
+ const p2p = addr.getComponents().filter((c) => c.name === 'p2p');
135
+ return p2p.length > 0 ? (p2p[p2p.length - 1].value ?? null) : null;
136
+ }
137
+
138
+ /**
139
+ * Return `addrs` with signaling (`/p2p-circuit`) addrs first, otherwise stable.
140
+ * Used to present the WebRTC dial input ahead of direct addrs. Does NOT mutate
141
+ * the input and must not be used before signature verification (verification
142
+ * uses the original on-record order).
143
+ */
144
+ export function orderSignalingFirst(addrs: string[]): string[] {
145
+ const signaling = addrs.filter(isSignalingAddr);
146
+ const rest = addrs.filter(a => !isSignalingAddr(a));
147
+ return [...signaling, ...rest];
148
+ }
149
+
150
+ /**
151
+ * Default resolve trust gate: trust any peer that has a `CadrePeer` row.
152
+ *
153
+ * A row only exists because an owner signed its `AuthorizedInsert`, so row
154
+ * presence already means "owner-vouched member". This is the seam where a
155
+ * stricter policy (trust circle, pinned keys) plugs in — see
156
+ * {@link PeerResolveTrustPolicy}.
157
+ */
158
+ export function currentMemberTrustPolicy(): PeerResolveTrustPolicy {
159
+ return {
160
+ evaluate() {
161
+ return true;
162
+ },
163
+ };
164
+ }
@@ -1,84 +1,84 @@
1
- /**
2
- * push-credentials.ts — pure validation + log-redaction for {@link PushCredentials}.
3
- *
4
- * The platform-push credentials a deployment provisions (`cadre-host` from its
5
- * secret store, `cadre-provider` from per-tenant config) are written into a
6
- * spawned node's config under `CadreNodeConfig.push`. This module is the
7
- * dependency-free seam those provisioners share to (a) reject a *partial* set
8
- * before it reaches a node — a present platform block must carry all its required
9
- * fields, or it fails fast at provisioning rather than at first push — and (b)
10
- * produce a log-safe view that never leaks a private key.
11
- *
12
- * It imports only the *types* (erased at emit), so it pulls no node:crypto /
13
- * node:http2 edge — a provisioner can import these functions without dragging in
14
- * the FCM/APNs senders.
15
- */
16
-
17
- import type { PushCredentials, FcmCredentials, ApnsCredentials } from './types.js';
18
-
19
- /** Marker substituted for every secret field by {@link redactPushCredentials}. */
20
- export const REDACTED = '[redacted]';
21
-
22
- /**
23
- * Validate a resolved push-credential bundle, returning human-readable errors
24
- * (empty ⇒ valid). Each platform block is optional, but a *present* block must
25
- * carry every required field — a partial set (e.g. APNs `keyId` without
26
- * `privateKey`) is a misconfiguration that must surface at provisioning time, not
27
- * silently at the first wake. A bundle with neither platform is valid (push is
28
- * opt-in); callers that require at least one platform check that separately.
29
- */
30
- export function validatePushCredentials(push: PushCredentials): string[] {
31
- const errors: string[] = [];
32
- if (push.fcm) errors.push(...validateFcm(push.fcm));
33
- if (push.apns) errors.push(...validateApns(push.apns));
34
- return errors;
35
- }
36
-
37
- function validateFcm(fcm: FcmCredentials): string[] {
38
- return missingFields('fcm', fcm as unknown as Record<string, unknown>, [
39
- 'projectId',
40
- 'clientEmail',
41
- 'privateKey',
42
- ]);
43
- }
44
-
45
- function validateApns(apns: ApnsCredentials): string[] {
46
- return missingFields('apns', apns as unknown as Record<string, unknown>, [
47
- 'keyId',
48
- 'teamId',
49
- 'bundleId',
50
- 'privateKey',
51
- ]);
52
- }
53
-
54
- /** Names the required fields of `platform` that are missing or blank. */
55
- function missingFields(platform: string, obj: Record<string, unknown>, required: string[]): string[] {
56
- return required
57
- .filter((f) => typeof obj[f] !== 'string' || (obj[f] as string).trim().length === 0)
58
- .map((f) => `push.${platform}.${f} is required when a ${platform} credential block is present`);
59
- }
60
-
61
- /**
62
- * Return a log-safe shallow copy of a push bundle with every private key replaced
63
- * by {@link REDACTED}. Use this for ANY debug/log line that touches push config —
64
- * `privateKey` fields are secrets and must never reach a log sink (the same rule
65
- * the startup/seed tokens follow).
66
- */
67
- export function redactPushCredentials(push: PushCredentials): Record<string, unknown> {
68
- const out: Record<string, unknown> = {};
69
- if (push.fcm) {
70
- out.fcm = { projectId: push.fcm.projectId, clientEmail: push.fcm.clientEmail, privateKey: REDACTED };
71
- }
72
- if (push.apns) {
73
- out.apns = {
74
- keyId: push.apns.keyId,
75
- teamId: push.apns.teamId,
76
- bundleId: push.apns.bundleId,
77
- ...(push.apns.production !== undefined ? { production: push.apns.production } : {}),
78
- privateKey: REDACTED,
79
- };
80
- }
81
- if (push.cooldownMs !== undefined) out.cooldownMs = push.cooldownMs;
82
- if (push.debounceMs !== undefined) out.debounceMs = push.debounceMs;
83
- return out;
84
- }
1
+ /**
2
+ * push-credentials.ts — pure validation + log-redaction for {@link PushCredentials}.
3
+ *
4
+ * The platform-push credentials a deployment provisions (`cadre-host` from its
5
+ * secret store, `cadre-provider` from per-tenant config) are written into a
6
+ * spawned node's config under `CadreNodeConfig.push`. This module is the
7
+ * dependency-free seam those provisioners share to (a) reject a *partial* set
8
+ * before it reaches a node — a present platform block must carry all its required
9
+ * fields, or it fails fast at provisioning rather than at first push — and (b)
10
+ * produce a log-safe view that never leaks a private key.
11
+ *
12
+ * It imports only the *types* (erased at emit), so it pulls no node:crypto /
13
+ * node:http2 edge — a provisioner can import these functions without dragging in
14
+ * the FCM/APNs senders.
15
+ */
16
+
17
+ import type { PushCredentials, FcmCredentials, ApnsCredentials } from './types.js';
18
+
19
+ /** Marker substituted for every secret field by {@link redactPushCredentials}. */
20
+ export const REDACTED = '[redacted]';
21
+
22
+ /**
23
+ * Validate a resolved push-credential bundle, returning human-readable errors
24
+ * (empty ⇒ valid). Each platform block is optional, but a *present* block must
25
+ * carry every required field — a partial set (e.g. APNs `keyId` without
26
+ * `privateKey`) is a misconfiguration that must surface at provisioning time, not
27
+ * silently at the first wake. A bundle with neither platform is valid (push is
28
+ * opt-in); callers that require at least one platform check that separately.
29
+ */
30
+ export function validatePushCredentials(push: PushCredentials): string[] {
31
+ const errors: string[] = [];
32
+ if (push.fcm) errors.push(...validateFcm(push.fcm));
33
+ if (push.apns) errors.push(...validateApns(push.apns));
34
+ return errors;
35
+ }
36
+
37
+ function validateFcm(fcm: FcmCredentials): string[] {
38
+ return missingFields('fcm', fcm as unknown as Record<string, unknown>, [
39
+ 'projectId',
40
+ 'clientEmail',
41
+ 'privateKey',
42
+ ]);
43
+ }
44
+
45
+ function validateApns(apns: ApnsCredentials): string[] {
46
+ return missingFields('apns', apns as unknown as Record<string, unknown>, [
47
+ 'keyId',
48
+ 'teamId',
49
+ 'bundleId',
50
+ 'privateKey',
51
+ ]);
52
+ }
53
+
54
+ /** Names the required fields of `platform` that are missing or blank. */
55
+ function missingFields(platform: string, obj: Record<string, unknown>, required: string[]): string[] {
56
+ return required
57
+ .filter((f) => typeof obj[f] !== 'string' || (obj[f] as string).trim().length === 0)
58
+ .map((f) => `push.${platform}.${f} is required when a ${platform} credential block is present`);
59
+ }
60
+
61
+ /**
62
+ * Return a log-safe shallow copy of a push bundle with every private key replaced
63
+ * by {@link REDACTED}. Use this for ANY debug/log line that touches push config —
64
+ * `privateKey` fields are secrets and must never reach a log sink (the same rule
65
+ * the startup/seed tokens follow).
66
+ */
67
+ export function redactPushCredentials(push: PushCredentials): Record<string, unknown> {
68
+ const out: Record<string, unknown> = {};
69
+ if (push.fcm) {
70
+ out.fcm = { projectId: push.fcm.projectId, clientEmail: push.fcm.clientEmail, privateKey: REDACTED };
71
+ }
72
+ if (push.apns) {
73
+ out.apns = {
74
+ keyId: push.apns.keyId,
75
+ teamId: push.apns.teamId,
76
+ bundleId: push.apns.bundleId,
77
+ ...(push.apns.production !== undefined ? { production: push.apns.production } : {}),
78
+ privateKey: REDACTED,
79
+ };
80
+ }
81
+ if (push.cooldownMs !== undefined) out.cooldownMs = push.cooldownMs;
82
+ if (push.debounceMs !== undefined) out.debounceMs = push.debounceMs;
83
+ return out;
84
+ }
package/src/push-node.ts CHANGED
@@ -1,64 +1,64 @@
1
- /**
2
- * push-node.ts — Node-only subpath entry for platform push **construction**.
3
- *
4
- * Import from the dedicated subpath, never the package root:
5
- *
6
- * ```ts
7
- * import { createPushNotifier } from '@serfab/cadre-core/push-node';
8
- * ```
9
- *
10
- * The FCM/APNs implementations reach for `node:crypto` / `node:http2`, so they
11
- * are quarantined here — unreachable from the cross-platform `./index.js` graph
12
- * (same isolation pattern as `./key-store-file`). A React Native / browser entry
13
- * never resolves this path, so the Node builtins never reach a bundler that
14
- * cannot satisfy them. The core references only the {@link PushNotifier}
15
- * *interface* (`push-notifier.ts`) and a Node host injects the instance built
16
- * here into `CadreNodeConfig.push.notifier`.
17
- */
18
-
19
- import type { PushCredentials, PushPlatform } from './types.js';
20
- import type { PushMessage, PushNotifier, PushSendResult } from './push-notifier.js';
21
- import { createFcmPushNotifier, type FcmPushDeps } from './push-notifier-fcm.js';
22
- import { createApnsPushNotifier, type ApnsPushDeps } from './push-notifier-apns.js';
23
-
24
- /**
25
- * Per-platform transport/clock/log seams, injected by tests. Defaults are the
26
- * real implementations (global `fetch` for FCM, a `node:http2` session for APNs).
27
- */
28
- export interface PushNotifierDeps {
29
- fcm?: FcmPushDeps;
30
- apns?: ApnsPushDeps;
31
- }
32
-
33
- /**
34
- * Build a {@link PushNotifier} router over the configured credentials. Only the
35
- * platforms whose credentials are present get a backing implementation; a `send`
36
- * for an unconfigured platform returns a best-effort `no <platform> credentials`
37
- * failure rather than throwing.
38
- */
39
- export function createPushNotifier(creds: PushCredentials, deps: PushNotifierDeps = {}): PushNotifier {
40
- const fcm = creds.fcm ? createFcmPushNotifier(creds.fcm, deps.fcm) : undefined;
41
- const apns = creds.apns ? createApnsPushNotifier(creds.apns, deps.apns) : undefined;
42
-
43
- async function send(msg: PushMessage): Promise<PushSendResult> {
44
- if (msg.platform === 'fcm') return fcm ? fcm.send(msg) : noCreds('fcm');
45
- if (msg.platform === 'apns') return apns ? apns.send(msg) : noCreds('apns');
46
- return { ok: false, unregistered: false, error: `unknown platform ${String(msg.platform)}` };
47
- }
48
-
49
- async function close(): Promise<void> {
50
- await Promise.all([fcm?.close(), apns?.close()]);
51
- }
52
-
53
- return { send, close };
54
- }
55
-
56
- /** Best-effort no-op result for a platform with no configured credentials. */
57
- function noCreds(platform: PushPlatform): PushSendResult {
58
- return { ok: false, unregistered: false, error: `no ${platform} credentials` };
59
- }
60
-
61
- // Per-platform builders + their injected-dep types, for a host that wants a
62
- // single-platform notifier or to inject a fake transport directly.
63
- export { createFcmPushNotifier, createApnsPushNotifier };
64
- export type { FcmPushDeps, ApnsPushDeps };
1
+ /**
2
+ * push-node.ts — Node-only subpath entry for platform push **construction**.
3
+ *
4
+ * Import from the dedicated subpath, never the package root:
5
+ *
6
+ * ```ts
7
+ * import { createPushNotifier } from '@serfab/cadre-core/push-node';
8
+ * ```
9
+ *
10
+ * The FCM/APNs implementations reach for `node:crypto` / `node:http2`, so they
11
+ * are quarantined here — unreachable from the cross-platform `./index.js` graph
12
+ * (same isolation pattern as `./key-store-file`). A React Native / browser entry
13
+ * never resolves this path, so the Node builtins never reach a bundler that
14
+ * cannot satisfy them. The core references only the {@link PushNotifier}
15
+ * *interface* (`push-notifier.ts`) and a Node host injects the instance built
16
+ * here into `CadreNodeConfig.push.notifier`.
17
+ */
18
+
19
+ import type { PushCredentials, PushPlatform } from './types.js';
20
+ import type { PushMessage, PushNotifier, PushSendResult } from './push-notifier.js';
21
+ import { createFcmPushNotifier, type FcmPushDeps } from './push-notifier-fcm.js';
22
+ import { createApnsPushNotifier, type ApnsPushDeps } from './push-notifier-apns.js';
23
+
24
+ /**
25
+ * Per-platform transport/clock/log seams, injected by tests. Defaults are the
26
+ * real implementations (global `fetch` for FCM, a `node:http2` session for APNs).
27
+ */
28
+ export interface PushNotifierDeps {
29
+ fcm?: FcmPushDeps;
30
+ apns?: ApnsPushDeps;
31
+ }
32
+
33
+ /**
34
+ * Build a {@link PushNotifier} router over the configured credentials. Only the
35
+ * platforms whose credentials are present get a backing implementation; a `send`
36
+ * for an unconfigured platform returns a best-effort `no <platform> credentials`
37
+ * failure rather than throwing.
38
+ */
39
+ export function createPushNotifier(creds: PushCredentials, deps: PushNotifierDeps = {}): PushNotifier {
40
+ const fcm = creds.fcm ? createFcmPushNotifier(creds.fcm, deps.fcm) : undefined;
41
+ const apns = creds.apns ? createApnsPushNotifier(creds.apns, deps.apns) : undefined;
42
+
43
+ async function send(msg: PushMessage): Promise<PushSendResult> {
44
+ if (msg.platform === 'fcm') return fcm ? fcm.send(msg) : noCreds('fcm');
45
+ if (msg.platform === 'apns') return apns ? apns.send(msg) : noCreds('apns');
46
+ return { ok: false, unregistered: false, error: `unknown platform ${String(msg.platform)}` };
47
+ }
48
+
49
+ async function close(): Promise<void> {
50
+ await Promise.all([fcm?.close(), apns?.close()]);
51
+ }
52
+
53
+ return { send, close };
54
+ }
55
+
56
+ /** Best-effort no-op result for a platform with no configured credentials. */
57
+ function noCreds(platform: PushPlatform): PushSendResult {
58
+ return { ok: false, unregistered: false, error: `no ${platform} credentials` };
59
+ }
60
+
61
+ // Per-platform builders + their injected-dep types, for a host that wants a
62
+ // single-platform notifier or to inject a fake transport directly.
63
+ export { createFcmPushNotifier, createApnsPushNotifier };
64
+ export type { FcmPushDeps, ApnsPushDeps };