@serfab/cadre-core 0.10.0 → 0.12.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 (145) hide show
  1. package/README.md +255 -255
  2. package/dist/announce-addrs.d.ts +49 -0
  3. package/dist/announce-addrs.js +78 -0
  4. package/dist/announce-addrs.js.map +1 -0
  5. package/dist/cadre-node.d.ts +484 -58
  6. package/dist/cadre-node.js +953 -182
  7. package/dist/cadre-node.js.map +1 -1
  8. package/dist/control-cohort.d.ts +26 -0
  9. package/dist/control-cohort.js +26 -0
  10. package/dist/control-cohort.js.map +1 -1
  11. package/dist/control-database.d.ts +222 -125
  12. package/dist/control-database.js +356 -266
  13. package/dist/control-database.js.map +1 -1
  14. package/dist/control-formation-recorder.d.ts +8 -7
  15. package/dist/control-formation-recorder.js +8 -7
  16. package/dist/control-formation-recorder.js.map +1 -1
  17. package/dist/control-read-retry.d.ts +103 -0
  18. package/dist/control-read-retry.js +145 -0
  19. package/dist/control-read-retry.js.map +1 -0
  20. package/dist/control-retry.d.ts +101 -0
  21. package/dist/control-retry.js +108 -0
  22. package/dist/control-retry.js.map +1 -0
  23. package/dist/control-schema.d.ts +1 -1
  24. package/dist/control-schema.js +805 -774
  25. package/dist/control-schema.js.map +1 -1
  26. package/dist/control-write-retry.d.ts +65 -31
  27. package/dist/control-write-retry.js +33 -103
  28. package/dist/control-write-retry.js.map +1 -1
  29. package/dist/delegate-admission.d.ts +17 -15
  30. package/dist/delegate-admission.js +17 -15
  31. package/dist/delegate-admission.js.map +1 -1
  32. package/dist/index.d.ts +7 -5
  33. package/dist/index.js +18 -9
  34. package/dist/index.js.map +1 -1
  35. package/dist/membership-connection-gater.d.ts +211 -25
  36. package/dist/membership-connection-gater.js +309 -29
  37. package/dist/membership-connection-gater.js.map +1 -1
  38. package/dist/peer-addr-book.d.ts +92 -0
  39. package/dist/peer-addr-book.js +220 -0
  40. package/dist/peer-addr-book.js.map +1 -0
  41. package/dist/peer-record.d.ts +26 -0
  42. package/dist/peer-record.js +32 -0
  43. package/dist/peer-record.js.map +1 -1
  44. package/dist/relay-addrs.d.ts +76 -26
  45. package/dist/relay-addrs.js +115 -27
  46. package/dist/relay-addrs.js.map +1 -1
  47. package/dist/relay-reservation.d.ts +20 -15
  48. package/dist/relay-reservation.js +18 -14
  49. package/dist/relay-reservation.js.map +1 -1
  50. package/dist/seed-bootstrap.d.ts +7 -0
  51. package/dist/seed-bootstrap.js +15 -13
  52. package/dist/seed-bootstrap.js.map +1 -1
  53. package/dist/strand-addr-protocol.js +16 -0
  54. package/dist/strand-addr-protocol.js.map +1 -1
  55. package/dist/strand-backfill.js +13 -4
  56. package/dist/strand-backfill.js.map +1 -1
  57. package/dist/strand-cohort.d.ts +4 -37
  58. package/dist/strand-cohort.js +4 -15
  59. package/dist/strand-cohort.js.map +1 -1
  60. package/dist/strand-database.d.ts +11 -16
  61. package/dist/strand-database.js +19 -7
  62. package/dist/strand-database.js.map +1 -1
  63. package/dist/strand-formation-manager.d.ts +18 -17
  64. package/dist/strand-formation-manager.js +25 -27
  65. package/dist/strand-formation-manager.js.map +1 -1
  66. package/dist/strand-formation-protocol.d.ts +7 -7
  67. package/dist/strand-formation-protocol.js +8 -8
  68. package/dist/strand-formation-protocol.js.map +1 -1
  69. package/dist/strand-instance-manager.d.ts +53 -21
  70. package/dist/strand-instance-manager.js +128 -37
  71. package/dist/strand-instance-manager.js.map +1 -1
  72. package/dist/strand-membership-writer.d.ts +113 -19
  73. package/dist/strand-membership-writer.js +219 -51
  74. package/dist/strand-membership-writer.js.map +1 -1
  75. package/dist/strand-solicitation.d.ts +3 -3
  76. package/dist/strand-solicitation.js +4 -5
  77. package/dist/strand-solicitation.js.map +1 -1
  78. package/dist/strand-wake-protocol.d.ts +47 -2
  79. package/dist/strand-wake-protocol.js +76 -6
  80. package/dist/strand-wake-protocol.js.map +1 -1
  81. package/dist/types.d.ts +107 -54
  82. package/dist/types.js.map +1 -1
  83. package/package.json +8 -7
  84. package/src/announce-addrs.ts +93 -0
  85. package/src/arachnode-stub.ts +159 -159
  86. package/src/bootstrap-peer-store-file.ts +44 -44
  87. package/src/bootstrap-peer-store.ts +172 -172
  88. package/src/cadre-node.ts +1026 -194
  89. package/src/canonical-json.ts +30 -30
  90. package/src/control-authorization.ts +114 -114
  91. package/src/control-cohort.ts +130 -103
  92. package/src/control-database.ts +376 -298
  93. package/src/control-formation-recorder.ts +305 -304
  94. package/src/control-read-retry.ts +167 -0
  95. package/src/control-retry.ts +190 -0
  96. package/src/control-schema.ts +817 -786
  97. package/src/control-stream.ts +205 -205
  98. package/src/control-write-retry.ts +383 -473
  99. package/src/delegate-admission.ts +304 -302
  100. package/src/device-token.ts +94 -94
  101. package/src/diagnostics/connection-path.ts +277 -277
  102. package/src/diagnostics/webrtc-turn-tracker.ts +222 -222
  103. package/src/ed25519-key.ts +134 -134
  104. package/src/enrollment.ts +199 -199
  105. package/src/file-durable-slot.ts +61 -61
  106. package/src/formation-approval.ts +604 -604
  107. package/src/fs-atomic.ts +107 -107
  108. package/src/hibernation-manager.ts +437 -437
  109. package/src/identity-key.ts +109 -109
  110. package/src/index.ts +435 -406
  111. package/src/key-store-file.ts +132 -132
  112. package/src/key-store.ts +99 -99
  113. package/src/membership-connection-gater.ts +368 -39
  114. package/src/node-local-snapshot.ts +260 -260
  115. package/src/peer-addr-book.ts +250 -0
  116. package/src/peer-authorization.ts +259 -259
  117. package/src/peer-record.ts +197 -164
  118. package/src/push-credentials.ts +84 -84
  119. package/src/push-node.ts +64 -64
  120. package/src/push-notifier-apns.ts +247 -247
  121. package/src/push-notifier-fcm.ts +220 -220
  122. package/src/push-notifier-shared.ts +28 -28
  123. package/src/push-notifier.ts +52 -52
  124. package/src/relay-addrs.ts +134 -27
  125. package/src/relay-reservation.ts +22 -17
  126. package/src/seed-bootstrap.ts +15 -15
  127. package/src/seed-trust-policy.ts +140 -140
  128. package/src/strand-addr-protocol.ts +458 -441
  129. package/src/strand-backfill.ts +28 -5
  130. package/src/strand-cohort.ts +38 -77
  131. package/src/strand-database.ts +202 -204
  132. package/src/strand-formation-manager.ts +26 -28
  133. package/src/strand-formation-protocol.ts +692 -692
  134. package/src/strand-instance-manager.ts +636 -553
  135. package/src/strand-member-key.ts +39 -39
  136. package/src/strand-member-registry.ts +182 -182
  137. package/src/strand-membership-writer.ts +1627 -1430
  138. package/src/strand-solicitation.ts +471 -472
  139. package/src/strand-transport-key.ts +58 -58
  140. package/src/strand-wake-payload.ts +32 -32
  141. package/src/strand-wake-protocol.ts +400 -314
  142. package/src/strand-watcher.ts +314 -314
  143. package/src/trusted-owner-store-file.ts +41 -41
  144. package/src/trusted-owner-store.ts +171 -171
  145. package/src/types.ts +1354 -1303
package/README.md CHANGED
@@ -1,255 +1,255 @@
1
- # @serfab/cadre-core
2
-
3
- Core library for Sereus cadre nodes—the infrastructure that enables parties to manage their personal cluster of nodes participating in distributed strand networks.
4
-
5
- ## Why Sereus Cadre Core?
6
-
7
- In traditional distributed systems, users depend on centralized services to manage their data and identities. **Sereus** inverts this model: each user (or "party") controls their own **cadre**—a personal cluster of nodes ranging from always-on cloud servers to intermittently-connected mobile devices.
8
-
9
- The cadre architecture provides:
10
-
11
- - **Sovereignty**: Your data lives on your nodes, under your control
12
- - **Resilience**: Multiple nodes means no single point of failure
13
- - **Flexibility**: Mix cloud servers, home NAS, laptops, and phones
14
- - **Privacy**: Cryptographic authorization without central servers
15
-
16
- A cadre doesn't exist in isolation. Cadres participate in **strands**—shared data spaces where multiple parties collaborate. When you join a messaging app, your cadre joins that strand. When you share a document, you're creating a strand between your cadre and theirs.
17
-
18
- ## Architecture Overview
19
-
20
- ```
21
- ┌─────────────────────────────────────────────────────────────────────────┐
22
- │ Your Cadre │
23
- │ │
24
- │ ┌────────────────────────────────────────────────────────────────────┐ │
25
- │ │ Control Network │ │
26
- │ │ (Private Optimystic DB for cadre management) │ │
27
- │ │ │ │
28
- │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
29
- │ │ │ Phone │──│ Laptop │──│ Cloud │──│ NAS │ │ │
30
- │ │ │ (edge) │ │ (edge) │ │ (core) │ │ (core) │ │ │
31
- │ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
32
- │ └────────────────────────────────────────────────────────────────────┘ │
33
- │ │ │
34
- │ ┌─────────────────────┼─────────────────────┐ │
35
- │ ▼ ▼ ▼ │
36
- │ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
37
- │ │ Strand A │ │ Strand B │ │ Strand C │ │
38
- │ │ (Chat App) │ │ (Shared Docs) │ │ (Photo Backup) │ │
39
- │ └──────────────────┘ └──────────────────┘ └──────────────────┘ │
40
- └─────────────────────────────────────────────────────────────────────────┘
41
- ```
42
-
43
- Each `CadreNode` instance:
44
- 1. **Connects to the control network** - A private Optimystic database shared only by your nodes
45
- 2. **Watches for strand changes** - Automatically detects when you join or leave strands
46
- 3. **Manages strand instances** - Spins up isolated libp2p networks for each strand
47
- 4. **Handles peer enrollment** - Cryptographically authorizes new devices to join your cadre
48
-
49
- ## Installation
50
-
51
- ```bash
52
- npm install @serfab/cadre-core
53
- ```
54
-
55
- ## Quick Start
56
-
57
- ```typescript
58
- import { CadreNode } from '@serfab/cadre-core';
59
- import { FileRawStorage } from '@optimystic/db-p2p-storage-fs';
60
-
61
- const node = new CadreNode({
62
- controlNetwork: {
63
- partyId: 'your-unique-party-id',
64
- bootstrapNodes: ['/ip4/192.168.1.100/tcp/4001/p2p/12D3KooW...']
65
- },
66
- profile: 'storage', // 'storage' for servers, 'transaction' for mobile
67
- storage: {
68
- // Storage provider factory - called with strandId for strand-isolated storage
69
- provider: (strandId) => new FileRawStorage(`/data/sereus/${strandId}`),
70
- quotaBytes: 10 * 1024 * 1024 * 1024 // 10 GB
71
- }
72
- });
73
-
74
- // Start the node
75
- await node.start();
76
- console.log('Node started with Peer ID:', node.peerId?.toString());
77
-
78
- // Listen for strand events
79
- node.on('strand:started', ({ strandId }) => {
80
- console.log('Joined strand:', strandId);
81
- });
82
-
83
- // Check active strands
84
- for (const [id, strand] of node.getStrands()) {
85
- console.log(`Strand ${id}: ${strand.status}, ${strand.connectedPeers} peers`);
86
- }
87
-
88
- // Graceful shutdown
89
- await node.stop();
90
- ```
91
-
92
- ## Node Profiles
93
-
94
- | Profile | Storage Role | Use Case | Ring Zulu |
95
- |---------|--------------|----------|-----------|
96
- | **transaction** | Arachnode disabled; transaction verification via FRET only | Mobile devices, intermittent connectivity | No |
97
- | **storage** | Ring Zulu + storage rings (storage rings not yet implemented — stub) | Servers, NAS, always-on nodes | Yes |
98
-
99
- Transaction-profile nodes verify transactions via FRET only, with Arachnode disabled. Storage-profile nodes additionally join Ring Zulu and (when implemented) the concentric storage rings.
100
-
101
- > **Note:** The storage-ring subsystem is currently a no-op stub (`arachnode-stub.ts`). See [Node Profiles in `docs/architecture.md`](../../docs/architecture.md#node-profiles) for the full design.
102
-
103
- ## Strand Filtering
104
-
105
- Mobile apps typically shouldn't participate in all strands. Use filters to control participation:
106
-
107
- ```typescript
108
- // Only participate in strands for a specific app
109
- strandFilter: { mode: 'sAppId', sAppId: 'com.example.chat' }
110
-
111
- // Only participate in one specific strand
112
- strandFilter: { mode: 'strandId', strandId: 'strand-abc123' }
113
-
114
- // Control network only, no strand participation
115
- strandFilter: { mode: 'none' }
116
-
117
- // Participate in all strands (default for servers)
118
- strandFilter: { mode: 'all' }
119
- ```
120
-
121
- ## Enrolling New Devices
122
-
123
- Adding a new device to your cadre uses the Seed Bootstrap API:
124
-
125
- ```typescript
126
- // On the new device: generate identity
127
- const enrollment = new EnrollmentService();
128
- const { peerId, privateKey } = await enrollment.createCadrePeer();
129
- // Store privateKey securely, send peerId + multiaddrs to owner
130
-
131
- // On owner device: authorize the new peer and create seed
132
- await node.authorizePeer(newDevicePeerId, newDeviceMultiaddrs);
133
- const seed = await node.createSeed();
134
-
135
- // Deliver seed to new device (via protocol, API, or out-of-band)
136
- await node.deliverSeed(newDeviceMultiaddr, seed);
137
- // Or encode for QR/link: const encoded = node.encodeSeed(seed);
138
-
139
- // On new device: apply seed to join cadre
140
- const result = await newNode.applySeed(seed);
141
- ```
142
-
143
- For provider-hosted drones, use the helper:
144
-
145
- ```typescript
146
- // Get drone info from provider API
147
- const droneInfo = await provider.createContainer(plan);
148
-
149
- // One call: authorize + create seed
150
- const { seed, encodedSeed } = await node.addDrone({
151
- dronePeerId: droneInfo.peerId,
152
- droneMultiaddrs: droneInfo.multiaddrs
153
- });
154
-
155
- // Send to provider for drone initialization
156
- await provider.initializeNode(droneInfo.containerId, encodedSeed);
157
- ```
158
-
159
- ## API Reference
160
-
161
- ### CadreNode
162
-
163
- The main entry point for cadre participation.
164
-
165
- | Method | Description |
166
- |--------|-------------|
167
- | `start()` | Connect to control network and begin strand participation |
168
- | `stop()` | Gracefully disconnect from all networks |
169
- | `getStrands()` | Get all active strand instances |
170
- | `getStrand(id)` | Get a specific strand instance |
171
- | `addStrand(row)` | Manually add a strand (testing/direct API) |
172
- | `stopStrand(id)` | Stop a strand on this node only (the shared `Strand` row stays; rediscovered on restart) |
173
- | `publishStrand(id, type?, memberKey?)` | Owner-signed `Strand` row insert — makes the strand visible cadre-wide |
174
- | `unpublishStrand(id)` | Owner-signed party-wide removal — deletes this party's `Strand` row; every node stops the strand. Destroys a closed strand's `MemberPrivateKey` |
175
- | `getEnrollmentService()` | Access peer enrollment API |
176
-
177
- ### Events
178
-
179
- | Event | Payload | Description |
180
- |-------|---------|-------------|
181
- | `control:connected` | - | Connected to control network |
182
- | `control:disconnected` | - | Disconnected from control network |
183
- | `strand:started` | `{ strandId }` | Strand instance started |
184
- | `strand:stopped` | `{ strandId }` | Strand instance stopped |
185
- | `strand:error` | `{ strandId, error }` | Error in strand instance. A failed launch re-emits this on every retry (backing off up to 5 min), so treat it as a repeating signal, not a one-shot |
186
-
187
- ## Related Documentation
188
-
189
- - **[Cadre Architecture](../../docs/architecture.md)** - Deep dive into the cadre system design
190
- - **[Strand Management](../../docs/strands.md)** - How strands connect multiple cadres
191
- - **[Schema Guide](../../docs/schema-guide.md)** - Optimystic schema definitions
192
-
193
- ## React Native Support
194
-
195
- This package is compatible with React Native. For mobile apps, use the RN-specific storage provider:
196
-
197
- ```typescript
198
- import { CadreNode } from '@serfab/cadre-core';
199
- import { RNRawStorage } from '@optimystic/db-p2p-storage-rn';
200
- import { webSockets } from '@libp2p/websockets';
201
- import { circuitRelayTransport } from '@libp2p/circuit-relay-v2';
202
-
203
- const node = new CadreNode({
204
- controlNetwork: {
205
- partyId: 'your-unique-party-id',
206
- bootstrapNodes: ['/dns4/relay.example.com/tcp/443/wss/p2p/12D3KooW...']
207
- },
208
- profile: 'transaction', // Mobile devices typically use transaction profile
209
- strandFilter: { mode: 'sAppId', sAppId: 'com.example.myapp' },
210
- storage: {
211
- // RN storage provider creates strand-isolated AsyncStorage
212
- provider: (strandId) => new RNRawStorage(strandId)
213
- },
214
- network: {
215
- // TCP doesn't work in React Native — use WebSocket transport instead
216
- transports: [webSockets(), circuitRelayTransport()],
217
- listenAddrs: [] // RN nodes typically cannot listen
218
- }
219
- });
220
- ```
221
-
222
- ### Storage Provider Pattern
223
-
224
- The `storage.provider` option accepts either:
225
- - **An `IRawStorage` instance** - Shared storage for all strands
226
- - **A factory function** `(strandId: string) => IRawStorage` - Creates isolated storage per strand (recommended)
227
-
228
- Available storage implementations:
229
- | Package | Environment | Description |
230
- |---------|-------------|-------------|
231
- | `@optimystic/db-p2p` | All | `MemoryRawStorage` - In-memory (testing only) |
232
- | `@optimystic/db-p2p-storage-fs` | Node.js | `FileRawStorage` - File system storage |
233
- | `@optimystic/db-p2p-storage-rn` | React Native | `RNRawStorage` - AsyncStorage-based |
234
-
235
- ### React Native Considerations
236
-
237
- 1. **Network Transport**: libp2p's TCP transport doesn't work in RN. Pass `transports: [webSockets(), circuitRelayTransport()]` in the `network` config (see example above).
238
-
239
- 2. **Schema Loading**: The control schema is embedded in the package. Do not set `schemaPath` in React Native.
240
-
241
- 3. **Profile**: Mobile devices should use `profile: 'transaction'` for battery/bandwidth efficiency.
242
-
243
- 4. **Strand Filtering**: Always filter strands on mobile to avoid participating in unnecessary networks.
244
-
245
- ## Related Packages
246
-
247
- - **[@optimystic/db-core](https://github.com/gotchoices/optimystic)** - Distributed database core
248
- - **[@optimystic/db-p2p](https://github.com/gotchoices/optimystic)** - libp2p integration for Optimystic
249
- - **[@optimystic/db-p2p-storage-fs](https://github.com/gotchoices/optimystic)** - File system storage (Node.js)
250
- - **[@optimystic/db-p2p-storage-rn](https://github.com/gotchoices/optimystic)** - AsyncStorage (React Native)
251
-
252
- ## License
253
-
254
- MIT
255
-
1
+ # @serfab/cadre-core
2
+
3
+ Core library for Sereus cadre nodes—the infrastructure that enables parties to manage their personal cluster of nodes participating in distributed strand networks.
4
+
5
+ ## Why Sereus Cadre Core?
6
+
7
+ In traditional distributed systems, users depend on centralized services to manage their data and identities. **Sereus** inverts this model: each user (or "party") controls their own **cadre**—a personal cluster of nodes ranging from always-on cloud servers to intermittently-connected mobile devices.
8
+
9
+ The cadre architecture provides:
10
+
11
+ - **Sovereignty**: Your data lives on your nodes, under your control
12
+ - **Resilience**: Multiple nodes means no single point of failure
13
+ - **Flexibility**: Mix cloud servers, home NAS, laptops, and phones
14
+ - **Privacy**: Cryptographic authorization without central servers
15
+
16
+ A cadre doesn't exist in isolation. Cadres participate in **strands**—shared data spaces where multiple parties collaborate. When you join a messaging app, your cadre joins that strand. When you share a document, you're creating a strand between your cadre and theirs.
17
+
18
+ ## Architecture Overview
19
+
20
+ ```
21
+ ┌─────────────────────────────────────────────────────────────────────────┐
22
+ │ Your Cadre │
23
+ │ │
24
+ │ ┌────────────────────────────────────────────────────────────────────┐ │
25
+ │ │ Control Network │ │
26
+ │ │ (Private Optimystic DB for cadre management) │ │
27
+ │ │ │ │
28
+ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
29
+ │ │ │ Phone │──│ Laptop │──│ Cloud │──│ NAS │ │ │
30
+ │ │ │ (edge) │ │ (edge) │ │ (core) │ │ (core) │ │ │
31
+ │ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
32
+ │ └────────────────────────────────────────────────────────────────────┘ │
33
+ │ │ │
34
+ │ ┌─────────────────────┼─────────────────────┐ │
35
+ │ ▼ ▼ ▼ │
36
+ │ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
37
+ │ │ Strand A │ │ Strand B │ │ Strand C │ │
38
+ │ │ (Chat App) │ │ (Shared Docs) │ │ (Photo Backup) │ │
39
+ │ └──────────────────┘ └──────────────────┘ └──────────────────┘ │
40
+ └─────────────────────────────────────────────────────────────────────────┘
41
+ ```
42
+
43
+ Each `CadreNode` instance:
44
+ 1. **Connects to the control network** - A private Optimystic database shared only by your nodes
45
+ 2. **Watches for strand changes** - Automatically detects when you join or leave strands
46
+ 3. **Manages strand instances** - Spins up isolated libp2p networks for each strand
47
+ 4. **Handles peer enrollment** - Cryptographically authorizes new devices to join your cadre
48
+
49
+ ## Installation
50
+
51
+ ```bash
52
+ npm install @serfab/cadre-core
53
+ ```
54
+
55
+ ## Quick Start
56
+
57
+ ```typescript
58
+ import { CadreNode } from '@serfab/cadre-core';
59
+ import { FileRawStorage } from '@optimystic/db-p2p-storage-fs';
60
+
61
+ const node = new CadreNode({
62
+ controlNetwork: {
63
+ partyId: 'your-unique-party-id',
64
+ bootstrapNodes: ['/ip4/192.168.1.100/tcp/4001/p2p/12D3KooW...']
65
+ },
66
+ profile: 'storage', // 'storage' for servers, 'transaction' for mobile
67
+ storage: {
68
+ // Storage provider factory - called with strandId for strand-isolated storage
69
+ provider: (strandId) => new FileRawStorage(`/data/sereus/${strandId}`),
70
+ quotaBytes: 10 * 1024 * 1024 * 1024 // 10 GB
71
+ }
72
+ });
73
+
74
+ // Start the node
75
+ await node.start();
76
+ console.log('Node started with Peer ID:', node.peerId?.toString());
77
+
78
+ // Listen for strand events
79
+ node.on('strand:started', ({ strandId }) => {
80
+ console.log('Joined strand:', strandId);
81
+ });
82
+
83
+ // Check active strands
84
+ for (const [id, strand] of node.getStrands()) {
85
+ console.log(`Strand ${id}: ${strand.status}, ${strand.connectedPeers} peers`);
86
+ }
87
+
88
+ // Graceful shutdown
89
+ await node.stop();
90
+ ```
91
+
92
+ ## Node Profiles
93
+
94
+ | Profile | Storage Role | Use Case | Ring Zulu |
95
+ |---------|--------------|----------|-----------|
96
+ | **transaction** | Arachnode disabled; transaction verification via FRET only | Mobile devices, intermittent connectivity | No |
97
+ | **storage** | Ring Zulu + storage rings (storage rings not yet implemented — stub) | Servers, NAS, always-on nodes | Yes |
98
+
99
+ Transaction-profile nodes verify transactions via FRET only, with Arachnode disabled. Storage-profile nodes additionally join Ring Zulu and (when implemented) the concentric storage rings.
100
+
101
+ > **Note:** The storage-ring subsystem is currently a no-op stub (`arachnode-stub.ts`). See [Node Profiles in `docs/architecture.md`](../../docs/architecture.md#node-profiles) for the full design.
102
+
103
+ ## Strand Filtering
104
+
105
+ Mobile apps typically shouldn't participate in all strands. Use filters to control participation:
106
+
107
+ ```typescript
108
+ // Only participate in strands for a specific app
109
+ strandFilter: { mode: 'sAppId', sAppId: 'com.example.chat' }
110
+
111
+ // Only participate in one specific strand
112
+ strandFilter: { mode: 'strandId', strandId: 'strand-abc123' }
113
+
114
+ // Control network only, no strand participation
115
+ strandFilter: { mode: 'none' }
116
+
117
+ // Participate in all strands (default for servers)
118
+ strandFilter: { mode: 'all' }
119
+ ```
120
+
121
+ ## Enrolling New Devices
122
+
123
+ Adding a new device to your cadre uses the Seed Bootstrap API:
124
+
125
+ ```typescript
126
+ // On the new device: generate identity
127
+ const enrollment = new EnrollmentService();
128
+ const { peerId, privateKey } = await enrollment.createCadrePeer();
129
+ // Store privateKey securely, send peerId + multiaddrs to owner
130
+
131
+ // On owner device: authorize the new peer and create seed
132
+ await node.authorizePeer(newDevicePeerId, newDeviceMultiaddrs);
133
+ const seed = await node.createSeed();
134
+
135
+ // Deliver seed to new device (via protocol, API, or out-of-band)
136
+ await node.deliverSeed(newDeviceMultiaddr, seed);
137
+ // Or encode for QR/link: const encoded = node.encodeSeed(seed);
138
+
139
+ // On new device: apply seed to join cadre
140
+ const result = await newNode.applySeed(seed);
141
+ ```
142
+
143
+ For provider-hosted drones, use the helper:
144
+
145
+ ```typescript
146
+ // Get drone info from provider API
147
+ const droneInfo = await provider.createContainer(plan);
148
+
149
+ // One call: authorize + create seed
150
+ const { seed, encodedSeed } = await node.addDrone({
151
+ dronePeerId: droneInfo.peerId,
152
+ droneMultiaddrs: droneInfo.multiaddrs
153
+ });
154
+
155
+ // Send to provider for drone initialization
156
+ await provider.initializeNode(droneInfo.containerId, encodedSeed);
157
+ ```
158
+
159
+ ## API Reference
160
+
161
+ ### CadreNode
162
+
163
+ The main entry point for cadre participation.
164
+
165
+ | Method | Description |
166
+ |--------|-------------|
167
+ | `start()` | Connect to control network and begin strand participation |
168
+ | `stop()` | Gracefully disconnect from all networks |
169
+ | `getStrands()` | Get all active strand instances |
170
+ | `getStrand(id)` | Get a specific strand instance |
171
+ | `addStrand(row)` | Manually add a strand (testing/direct API) |
172
+ | `stopStrand(id)` | Stop a strand on this node only (the shared `Strand` row stays; rediscovered on restart) |
173
+ | `publishStrand(id, type?, memberKey?)` | Owner-signed `Strand` row insert — makes the strand visible cadre-wide |
174
+ | `unpublishStrand(id)` | Owner-signed party-wide removal — deletes this party's `Strand` row; every node stops the strand. Destroys a closed strand's `MemberPrivateKey` |
175
+ | `getEnrollmentService()` | Access peer enrollment API |
176
+
177
+ ### Events
178
+
179
+ | Event | Payload | Description |
180
+ |-------|---------|-------------|
181
+ | `control:connected` | - | Connected to control network |
182
+ | `control:disconnected` | - | Disconnected from control network |
183
+ | `strand:started` | `{ strandId }` | Strand instance started |
184
+ | `strand:stopped` | `{ strandId }` | Strand instance stopped |
185
+ | `strand:error` | `{ strandId, error }` | Error in strand instance. A failed launch re-emits this on every retry (backing off up to 5 min), so treat it as a repeating signal, not a one-shot |
186
+
187
+ ## Related Documentation
188
+
189
+ - **[Cadre Architecture](../../docs/architecture.md)** - Deep dive into the cadre system design
190
+ - **[Strand Management](../../docs/strands.md)** - How strands connect multiple cadres
191
+ - **[Schema Guide](../../docs/schema-guide.md)** - Optimystic schema definitions
192
+
193
+ ## React Native Support
194
+
195
+ This package is compatible with React Native. For mobile apps, use the RN-specific storage provider:
196
+
197
+ ```typescript
198
+ import { CadreNode } from '@serfab/cadre-core';
199
+ import { RNRawStorage } from '@optimystic/db-p2p-storage-rn';
200
+ import { webSockets } from '@libp2p/websockets';
201
+ import { circuitRelayTransport } from '@libp2p/circuit-relay-v2';
202
+
203
+ const node = new CadreNode({
204
+ controlNetwork: {
205
+ partyId: 'your-unique-party-id',
206
+ bootstrapNodes: ['/dns4/relay.example.com/tcp/443/wss/p2p/12D3KooW...']
207
+ },
208
+ profile: 'transaction', // Mobile devices typically use transaction profile
209
+ strandFilter: { mode: 'sAppId', sAppId: 'com.example.myapp' },
210
+ storage: {
211
+ // RN storage provider creates strand-isolated AsyncStorage
212
+ provider: (strandId) => new RNRawStorage(strandId)
213
+ },
214
+ network: {
215
+ // TCP doesn't work in React Native — use WebSocket transport instead
216
+ transports: [webSockets(), circuitRelayTransport()],
217
+ listenAddrs: [] // RN nodes typically cannot listen
218
+ }
219
+ });
220
+ ```
221
+
222
+ ### Storage Provider Pattern
223
+
224
+ The `storage.provider` option accepts either:
225
+ - **An `IRawStorage` instance** - Shared storage for all strands
226
+ - **A factory function** `(strandId: string) => IRawStorage` - Creates isolated storage per strand (recommended)
227
+
228
+ Available storage implementations:
229
+ | Package | Environment | Description |
230
+ |---------|-------------|-------------|
231
+ | `@optimystic/db-p2p` | All | `MemoryRawStorage` - In-memory (testing only) |
232
+ | `@optimystic/db-p2p-storage-fs` | Node.js | `FileRawStorage` - File system storage |
233
+ | `@optimystic/db-p2p-storage-rn` | React Native | `RNRawStorage` - AsyncStorage-based |
234
+
235
+ ### React Native Considerations
236
+
237
+ 1. **Network Transport**: libp2p's TCP transport doesn't work in RN. Pass `transports: [webSockets(), circuitRelayTransport()]` in the `network` config (see example above).
238
+
239
+ 2. **Schema Loading**: The control schema is embedded in the package. Do not set `schemaPath` in React Native.
240
+
241
+ 3. **Profile**: Mobile devices should use `profile: 'transaction'` for battery/bandwidth efficiency.
242
+
243
+ 4. **Strand Filtering**: Always filter strands on mobile to avoid participating in unnecessary networks.
244
+
245
+ ## Related Packages
246
+
247
+ - **[@optimystic/db-core](https://github.com/gotchoices/optimystic)** - Distributed database core
248
+ - **[@optimystic/db-p2p](https://github.com/gotchoices/optimystic)** - libp2p integration for Optimystic
249
+ - **[@optimystic/db-p2p-storage-fs](https://github.com/gotchoices/optimystic)** - File system storage (Node.js)
250
+ - **[@optimystic/db-p2p-storage-rn](https://github.com/gotchoices/optimystic)** - AsyncStorage (React Native)
251
+
252
+ ## License
253
+
254
+ MIT
255
+
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Translate `network.announceAddrs` / `network.appendAnnounceAddrs` — the operator-facing
3
+ * "advertise these addresses" settings — into the two `@optimystic/db-p2p` `NodeOptions`
4
+ * fields of the same names, which reach libp2p's `addresses.announce` /
5
+ * `addresses.appendAnnounce`.
6
+ *
7
+ * Two jobs, both of which the control node and every strand node need identically, hence
8
+ * one module rather than the same conditional spread written twice:
9
+ *
10
+ * - **Empty means unset.** libp2p treats `announce: []` as "no override", and forwarding
11
+ * `[]` would still be forwarding a field. An empty config array is dropped so it cannot
12
+ * land as an explicit empty announce set.
13
+ * - **Validate the multiaddrs here, at start.** libp2p does NOT validate announce addrs
14
+ * at construction: `AddressManager` stores them as raw strings and only parses them on
15
+ * the first `getAnnounceAddrs()`. A typo therefore produces a node that starts
16
+ * cleanly, then throws `InvalidMultiaddrError` out of every `getMultiaddrs()` call —
17
+ * including an UNHANDLED one from the debounced peer-store update libp2p runs on
18
+ * `transport:listening`. Verified against libp2p's `address-manager` (its constructor
19
+ * does `announce.map(ma => ma.toString())`; `getAnnounceAddrs()` does `multiaddr(a)`).
20
+ * Parsing each entry up front converts that into a loud failure at node start, which is
21
+ * what {@link relayCircuitAddrs} already does for `network.relayAddrs`.
22
+ *
23
+ * Parsing alone is not enough, though: `''` and `'/'` both parse into a component-less
24
+ * multiaddr, and a set holding only those is non-empty by the rule above — so it would
25
+ * replace every advertised address with nothing at all. An entry naming no address is
26
+ * therefore rejected alongside one that fails to parse.
27
+ */
28
+ import type { NetworkConfig } from './types.js';
29
+ /**
30
+ * The announce fields to spread into `createLibp2pNode` options. A field is present
31
+ * only when the operator configured a non-empty list for it, so a node that configures
32
+ * neither passes neither.
33
+ */
34
+ export interface ResolvedAnnounceAddrs {
35
+ announceAddrs?: string[];
36
+ appendAnnounceAddrs?: string[];
37
+ }
38
+ /**
39
+ * Resolve both announce fields off a `NetworkConfig`. Throws — naming the offending
40
+ * config field and entry — when any entry is not a parsable multiaddr.
41
+ */
42
+ export declare function resolveAnnounceAddrs(network: NetworkConfig | undefined): ResolvedAnnounceAddrs;
43
+ /**
44
+ * Whether this node's advertised set will be REPLACED rather than extended — i.e.
45
+ * whether {@link ResolvedAnnounceAddrs.announceAddrs} is in play at all. The single
46
+ * predicate behind `CadreNode.start()`'s operator warning, kept here so the warning
47
+ * and the forwarding agree on what "set" means (non-empty, not merely present).
48
+ */
49
+ export declare function replacesAdvertisedAddrs(network: NetworkConfig | undefined): boolean;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Translate `network.announceAddrs` / `network.appendAnnounceAddrs` — the operator-facing
3
+ * "advertise these addresses" settings — into the two `@optimystic/db-p2p` `NodeOptions`
4
+ * fields of the same names, which reach libp2p's `addresses.announce` /
5
+ * `addresses.appendAnnounce`.
6
+ *
7
+ * Two jobs, both of which the control node and every strand node need identically, hence
8
+ * one module rather than the same conditional spread written twice:
9
+ *
10
+ * - **Empty means unset.** libp2p treats `announce: []` as "no override", and forwarding
11
+ * `[]` would still be forwarding a field. An empty config array is dropped so it cannot
12
+ * land as an explicit empty announce set.
13
+ * - **Validate the multiaddrs here, at start.** libp2p does NOT validate announce addrs
14
+ * at construction: `AddressManager` stores them as raw strings and only parses them on
15
+ * the first `getAnnounceAddrs()`. A typo therefore produces a node that starts
16
+ * cleanly, then throws `InvalidMultiaddrError` out of every `getMultiaddrs()` call —
17
+ * including an UNHANDLED one from the debounced peer-store update libp2p runs on
18
+ * `transport:listening`. Verified against libp2p's `address-manager` (its constructor
19
+ * does `announce.map(ma => ma.toString())`; `getAnnounceAddrs()` does `multiaddr(a)`).
20
+ * Parsing each entry up front converts that into a loud failure at node start, which is
21
+ * what {@link relayCircuitAddrs} already does for `network.relayAddrs`.
22
+ *
23
+ * Parsing alone is not enough, though: `''` and `'/'` both parse into a component-less
24
+ * multiaddr, and a set holding only those is non-empty by the rule above — so it would
25
+ * replace every advertised address with nothing at all. An entry naming no address is
26
+ * therefore rejected alongside one that fails to parse.
27
+ */
28
+ import { multiaddr } from '@multiformats/multiaddr';
29
+ /**
30
+ * Resolve both announce fields off a `NetworkConfig`. Throws — naming the offending
31
+ * config field and entry — when any entry is not a parsable multiaddr.
32
+ */
33
+ export function resolveAnnounceAddrs(network) {
34
+ const announceAddrs = configuredAddrs('network.announceAddrs', network?.announceAddrs);
35
+ const appendAnnounceAddrs = configuredAddrs('network.appendAnnounceAddrs', network?.appendAnnounceAddrs);
36
+ return {
37
+ ...(announceAddrs && { announceAddrs }),
38
+ ...(appendAnnounceAddrs && { appendAnnounceAddrs })
39
+ };
40
+ }
41
+ /**
42
+ * Whether this node's advertised set will be REPLACED rather than extended — i.e.
43
+ * whether {@link ResolvedAnnounceAddrs.announceAddrs} is in play at all. The single
44
+ * predicate behind `CadreNode.start()`'s operator warning, kept here so the warning
45
+ * and the forwarding agree on what "set" means (non-empty, not merely present).
46
+ */
47
+ export function replacesAdvertisedAddrs(network) {
48
+ return (network?.announceAddrs?.length ?? 0) > 0;
49
+ }
50
+ /** The entries to forward for one announce field, or `undefined` when it is unset or empty. */
51
+ function configuredAddrs(field, addrs) {
52
+ if (!addrs || addrs.length === 0) {
53
+ return undefined;
54
+ }
55
+ return addrs.map((addr) => validated(field, addr));
56
+ }
57
+ /** `addr` unchanged, having proved it names a real address — config field named on failure. */
58
+ function validated(field, addr) {
59
+ if (parsed(field, addr).getComponents().length === 0) {
60
+ // `''` and `'/'` PARSE: both yield a component-less multiaddr that stringifies to
61
+ // `/`. Forwarded, they make an announce set that is non-empty but names nothing —
62
+ // which by the replacement rule above costs the node every address it would
63
+ // otherwise advertise, silently. A templated `cadre.yaml` whose address variable went
64
+ // unsubstituted is the realistic way in, so it is rejected rather than forwarded.
65
+ throw new Error(`${field} entry names no address: ${JSON.stringify(addr)}`);
66
+ }
67
+ return addr;
68
+ }
69
+ /** `addr` as a multiaddr, with the config field named on a parse failure. */
70
+ function parsed(field, addr) {
71
+ try {
72
+ return multiaddr(addr);
73
+ }
74
+ catch (err) {
75
+ throw new Error(`${field} entry is not a valid multiaddr: ${addr}`, { cause: err });
76
+ }
77
+ }
78
+ //# sourceMappingURL=announce-addrs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"announce-addrs.js","sourceRoot":"","sources":["../src/announce-addrs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,SAAS,EAAkB,MAAM,yBAAyB,CAAC;AAapE;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAkC;IACrE,MAAM,aAAa,GAAG,eAAe,CAAC,uBAAuB,EAAE,OAAO,EAAE,aAAa,CAAC,CAAC;IACvF,MAAM,mBAAmB,GAAG,eAAe,CAAC,6BAA6B,EAAE,OAAO,EAAE,mBAAmB,CAAC,CAAC;IACzG,OAAO;QACL,GAAG,CAAC,aAAa,IAAI,EAAE,aAAa,EAAE,CAAC;QACvC,GAAG,CAAC,mBAAmB,IAAI,EAAE,mBAAmB,EAAE,CAAC;KACpD,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CAAC,OAAkC;IACxE,OAAO,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;AACnD,CAAC;AAED,+FAA+F;AAC/F,SAAS,eAAe,CAAC,KAAa,EAAE,KAAoC;IAC1E,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACjC,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC;AACrD,CAAC;AAED,+FAA+F;AAC/F,SAAS,SAAS,CAAC,KAAa,EAAE,IAAY;IAC5C,IAAI,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,aAAa,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrD,kFAAkF;QAClF,kFAAkF;QAClF,4EAA4E;QAC5E,sFAAsF;QACtF,kFAAkF;QAClF,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,4BAA4B,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC9E,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,6EAA6E;AAC7E,SAAS,MAAM,CAAC,KAAa,EAAE,IAAY;IACzC,IAAI,CAAC;QACH,OAAO,SAAS,CAAC,IAAI,CAAC,CAAC;IACzB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,oCAAoC,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;IACtF,CAAC;AACH,CAAC"}