@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
package/README.md CHANGED
@@ -1,274 +1,293 @@
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 once per scope (each strand id, plus this
69
- // party's control database) for per-scope isolation
70
- provider: (scope) => new FileRawStorage(`/data/sereus/${scope}`),
71
- quotaBytes: 10 * 1024 * 1024 * 1024 // 10 GB
72
- }
73
- });
74
-
75
- // Start the node
76
- await node.start();
77
- console.log('Node started with Peer ID:', node.peerId?.toString());
78
-
79
- // Listen for strand events
80
- node.on('strand:started', ({ strandId }) => {
81
- console.log('Joined strand:', strandId);
82
- });
83
-
84
- // Check active strands
85
- for (const [id, strand] of node.getStrands()) {
86
- console.log(`Strand ${id}: ${strand.status}, ${strand.connectedPeers} peers`);
87
- }
88
-
89
- // Graceful shutdown
90
- await node.stop();
91
- ```
92
-
93
- ## Node Profiles
94
-
95
- | Profile | Storage Role | Use Case | Ring Zulu |
96
- |---------|--------------|----------|-----------|
97
- | **transaction** | Arachnode disabled; transaction verification via FRET only | Mobile devices, intermittent connectivity | No |
98
- | **storage** | Ring Zulu + storage rings (storage rings not yet implemented — stub) | Servers, NAS, always-on nodes | Yes |
99
-
100
- 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.
101
-
102
- > **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.
103
-
104
- ## Strand Filtering
105
-
106
- Mobile apps typically shouldn't participate in all strands. Use filters to control participation:
107
-
108
- ```typescript
109
- // Only participate in strands for a specific app
110
- strandFilter: { mode: 'sAppId', sAppId: 'com.example.chat' }
111
-
112
- // Only participate in one specific strand
113
- strandFilter: { mode: 'strandId', strandId: 'strand-abc123' }
114
-
115
- // Control network only, no strand participation
116
- strandFilter: { mode: 'none' }
117
-
118
- // Participate in all strands (default for servers)
119
- strandFilter: { mode: 'all' }
120
- ```
121
-
122
- ## Enrolling New Devices
123
-
124
- Adding a new device to your cadre uses the Seed Bootstrap API:
125
-
126
- ```typescript
127
- // On the new device: generate identity
128
- const enrollment = new EnrollmentService();
129
- const { peerId, privateKey } = await enrollment.createCadrePeer();
130
- // Store privateKey securely, send peerId + multiaddrs to owner
131
-
132
- // On owner device: authorize the new peer and create seed
133
- await node.authorizePeer(newDevicePeerId, newDeviceMultiaddrs);
134
- const seed = await node.createSeed();
135
-
136
- // Deliver seed to new device (via protocol, API, or out-of-band)
137
- await node.deliverSeed(newDeviceMultiaddr, seed);
138
- // Or encode for QR/link: const encoded = node.encodeSeed(seed);
139
-
140
- // On new device: apply seed to join cadre
141
- const result = await newNode.applySeed(seed);
142
- ```
143
-
144
- For provider-hosted drones, use the helper:
145
-
146
- ```typescript
147
- // Get drone info from provider API
148
- const droneInfo = await provider.createContainer(plan);
149
-
150
- // One call: authorize + create seed
151
- const { seed, encodedSeed } = await node.addDrone({
152
- dronePeerId: droneInfo.peerId,
153
- droneMultiaddrs: droneInfo.multiaddrs
154
- });
155
-
156
- // Send to provider for drone initialization
157
- await provider.initializeNode(droneInfo.containerId, encodedSeed);
158
-
159
- // Dial the drone now from the addresses addDrone kept (a drone cannot dial a
160
- // phone); otherwise the next reconcile pass, about every 15 s, does it
161
- await node.reconcileControlCohort();
162
- ```
163
-
164
- ## API Reference
165
-
166
- ### CadreNode
167
-
168
- The main entry point for cadre participation.
169
-
170
- | Method | Description |
171
- |--------|-------------|
172
- | `start()` | Connect to control network and begin strand participation |
173
- | `stop()` | Gracefully disconnect from all networks |
174
- | `getStrands()` | Get all active strand instances |
175
- | `getStrand(id)` | Get a specific strand instance |
176
- | `foundStrand(config)` | **Create a strand**: publish its `Strand` row and start it locally as founder, in one call that is safe to re-run after an interruption. Returns the instance plus the row it actually runs under — for a closed strand read the membership key from there, since a resumed founding keeps the stored key |
177
- | `addStrand(row)` | Attach only — start a local instance for a row that already exists (the join path; also testing/direct API). Does **not** publish |
178
- | `stopStrand(id)` | Stop a strand on this node only (the shared `Strand` row stays; rediscovered on restart) |
179
- | `publishStrand(id, type?, memberKey?)` | Owner-signed `Strand` row insert — makes the strand visible cadre-wide. Idempotent: a live row with the same `(Type, MemberPrivateKey)` is returned unwritten; different content on the same id throws. Prefer `foundStrand` when creating a strand |
180
- | `unpublishStrand(id)` | Owner-signed party-wide removal — deletes this party's `Strand` row; every node stops the strand. Destroys a closed strand's `MemberPrivateKey` |
181
- | `getEnrollmentService()` | Access peer enrollment API |
182
-
183
- ### Events
184
-
185
- | Event | Payload | Description |
186
- |-------|---------|-------------|
187
- | `control:connected` | - | Connected to control network |
188
- | `control:disconnected` | - | Disconnected from control network |
189
- | `strand:started` | `{ strandId }` | Strand instance started |
190
- | `strand:stopped` | `{ strandId }` | Strand instance stopped |
191
- | `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 |
192
-
193
- ## Related Documentation
194
-
195
- - **[Cadre Architecture](../../docs/architecture.md)** - Deep dive into the cadre system design
196
- - **[Strand Management](../../docs/strands.md)** - How strands connect multiple cadres
197
- - **[Schema Guide](../../docs/schema-guide.md)** - Optimystic schema definitions
198
-
199
- ## React Native Support
200
-
201
- This package is compatible with React Native. For mobile apps, use the RN-specific storage provider:
202
-
203
- ```typescript
204
- import { CadreNode } from '@serfab/cadre-core';
205
- import { RNRawStorage } from '@optimystic/db-p2p-storage-rn';
206
- import { webSockets } from '@libp2p/websockets';
207
- import { circuitRelayTransport } from '@libp2p/circuit-relay-v2';
208
-
209
- const node = new CadreNode({
210
- controlNetwork: {
211
- partyId: 'your-unique-party-id',
212
- bootstrapNodes: ['/dns4/relay.example.com/tcp/443/wss/p2p/12D3KooW...']
213
- },
214
- profile: 'transaction', // Mobile devices typically use transaction profile
215
- strandFilter: { mode: 'sAppId', sAppId: 'com.example.myapp' },
216
- storage: {
217
- // RN storage provider creates per-scope AsyncStorage
218
- provider: (scope) => new RNRawStorage(scope)
219
- },
220
- network: {
221
- // TCP doesn't work in React Native — use WebSocket transport instead
222
- transports: [webSockets(), circuitRelayTransport()],
223
- listenAddrs: [] // RN nodes typically cannot listen
224
- }
225
- });
226
- ```
227
-
228
- ### Storage Provider Pattern
229
-
230
- The `storage.provider` option accepts either:
231
- - **An `IRawStorage` instance** - One store shared by every scope, so it can serve only a single party
232
- - **A factory function** `(scope: string) => IRawStorage` - Creates isolated storage per scope (recommended)
233
-
234
- A **scope key** is what the factory receives. It is the strand id for each strand, and
235
- `controlStorageScope(partyId)` — `control-<lowercase hex of the party id>` — for the control
236
- database, which holds one party's own records and must not be shared between parties. Every
237
- key is opaque and already safe as a file, directory or database name (always within
238
- `[a-z0-9._-]`), and two different keys are two different names even on a filesystem that
239
- ignores case (Windows, macOS): use it verbatim, do not parse it. `isControlStorageScope(scope)`
240
- tells the two kinds apart. The control key holds the charset by lowercase hex encoding; a strand
241
- id holds it by check — `assertStrandScopeKey` runs on every strand launch, so a strand
242
- whose row replicated in from another node with an unusable id (including one with an
243
- uppercase letter) is refused rather than
244
- handed to the factory, and on `publishStrand`, so this node never writes such an id into
245
- the party's control database to begin with.
246
-
247
- Available storage implementations:
248
- | Package | Environment | Description |
249
- |---------|-------------|-------------|
250
- | `@optimystic/db-p2p` | All | `MemoryRawStorage` - In-memory (testing only) |
251
- | `@optimystic/db-p2p-storage-fs` | Node.js | `FileRawStorage` - File system storage |
252
- | `@optimystic/db-p2p-storage-rn` | React Native | `RNRawStorage` - AsyncStorage-based |
253
-
254
- ### React Native Considerations
255
-
256
- 1. **Network Transport**: libp2p's TCP transport doesn't work in RN. Pass `transports: [webSockets(), circuitRelayTransport()]` in the `network` config (see example above).
257
-
258
- 2. **Schema Loading**: The control schema is embedded in the package. Do not set `schemaPath` in React Native.
259
-
260
- 3. **Profile**: Mobile devices should use `profile: 'transaction'` for battery/bandwidth efficiency.
261
-
262
- 4. **Strand Filtering**: Always filter strands on mobile to avoid participating in unnecessary networks.
263
-
264
- ## Related Packages
265
-
266
- - **[@optimystic/db-core](https://github.com/gotchoices/optimystic)** - Distributed database core
267
- - **[@optimystic/db-p2p](https://github.com/gotchoices/optimystic)** - libp2p integration for Optimystic
268
- - **[@optimystic/db-p2p-storage-fs](https://github.com/gotchoices/optimystic)** - File system storage (Node.js)
269
- - **[@optimystic/db-p2p-storage-rn](https://github.com/gotchoices/optimystic)** - AsyncStorage (React Native)
270
-
271
- ## License
272
-
273
- MIT
274
-
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 once per scope (each strand id, plus this
69
+ // party's control database) for per-scope isolation
70
+ provider: (scope) => new FileRawStorage(`/data/sereus/${scope}`),
71
+ quotaBytes: 10 * 1024 * 1024 * 1024 // 10 GB
72
+ }
73
+ });
74
+
75
+ // Start the node
76
+ await node.start();
77
+ console.log('Node started with Peer ID:', node.peerId?.toString());
78
+
79
+ // Listen for strand events
80
+ node.on('strand:started', ({ strandId }) => {
81
+ console.log('Joined strand:', strandId);
82
+ });
83
+
84
+ // Check active strands
85
+ for (const [id, strand] of node.getStrands()) {
86
+ console.log(`Strand ${id}: ${strand.status}, ${strand.connectedPeers} peers`);
87
+ }
88
+
89
+ // Graceful shutdown
90
+ await node.stop();
91
+ ```
92
+
93
+ ## Node Profiles
94
+
95
+ | Profile | Storage Role | Use Case | Ring Zulu |
96
+ |---------|--------------|----------|-----------|
97
+ | **transaction** | Arachnode disabled; transaction verification via FRET only | Mobile devices, intermittent connectivity | No |
98
+ | **storage** | Ring Zulu + storage rings (storage rings not yet implemented — stub) | Servers, NAS, always-on nodes | Yes |
99
+
100
+ 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.
101
+
102
+ > **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.
103
+
104
+ ## Strand Filtering
105
+
106
+ Mobile apps typically shouldn't participate in all strands. Use filters to control participation:
107
+
108
+ ```typescript
109
+ // Only participate in strands for a specific app
110
+ strandFilter: { mode: 'sAppId', sAppId: 'com.example.chat' }
111
+
112
+ // Only participate in one specific strand
113
+ strandFilter: { mode: 'strandId', strandId: 'strand-abc123' }
114
+
115
+ // Control network only, no strand participation
116
+ strandFilter: { mode: 'none' }
117
+
118
+ // Participate in all strands (default for servers)
119
+ strandFilter: { mode: 'all' }
120
+ ```
121
+
122
+ ## Enrolling New Devices
123
+
124
+ Adding a new device to your cadre uses the Seed Bootstrap API:
125
+
126
+ ```typescript
127
+ // On the new device: generate identity
128
+ const enrollment = new EnrollmentService();
129
+ const { peerId, privateKey } = await enrollment.createCadrePeer();
130
+ // Store privateKey securely, send peerId + multiaddrs to owner
131
+
132
+ // On owner device: authorize the new peer and create seed
133
+ await node.authorizePeer(newDevicePeerId, newDeviceMultiaddrs);
134
+ const seed = await node.createSeed();
135
+
136
+ // Deliver seed to new device (via protocol, API, or out-of-band)
137
+ await node.deliverSeed(newDeviceMultiaddr, seed);
138
+ // Or encode for QR/link: const encoded = node.encodeSeed(seed);
139
+
140
+ // On new device: apply seed to join cadre
141
+ const result = await newNode.applySeed(seed);
142
+ ```
143
+
144
+ For a machine whose peer id and addresses you already have, use the helper (`cadre enroll add` runs it on the owner node):
145
+
146
+ ```typescript
147
+ // One call: authorize + create seed
148
+ const { seed, encodedSeed } = await node.addDrone({ dronePeerId, droneMultiaddrs });
149
+
150
+ // The new machine starts with it: `cadre start --seed <encodedSeed> --pin-owner-key <owner key>`
151
+
152
+ // Dial the drone now from the addresses addDrone kept (a drone cannot dial a
153
+ // phone); otherwise the next reconcile pass, about every 15 s, does it
154
+ await node.reconcileControlCohort();
155
+ ```
156
+
157
+ A provider-hosted node is added with `rentNode` instead (below).
158
+
159
+ ### Renting a provider node
160
+
161
+ An app rents an always-on node from a hosting provider with `rentNode`, which adds the container to this node's cadre by invitation when a member is reachable from anywhere and by claim otherwise ([architecture.md → Renting a node from an app](../../docs/architecture.md#renting-a-node-from-an-app)). Everything it needs loads on React Native and in the browser:
162
+
163
+ ```typescript
164
+ import { createProviderClient, createTokenSource, fetchProviderDescriptor, rentNode } from '@serfab/cadre-core';
165
+
166
+ const descriptor = await fetchProviderDescriptor('https://nodes.acme.example/api/v1/descriptor');
167
+ // `stored` comes from the PKCE sign-in (createPkce, authorizationUrl, readAuthorizationRedirect,
168
+ // exchangeAuthorizationCode) or from the app's own store; `save` writes each refreshed pair back
169
+ const tokens = createTokenSource({ descriptor, clientId: 'my-app', tokens: stored, persist: save });
170
+ const client = createProviderClient({ apiBaseUrl: descriptor.apiBaseUrl, tokens });
171
+
172
+ const rental = await rentNode(node, client, { onProgress: (step) => console.log(step) });
173
+ // rental.join: 'invitation' (the container joins by itself; watch client.getContainer(id).join)
174
+ // or 'claim' (the node is in the cadre when rentNode returns, rental.peerId names it).
175
+ // Keep rental.containerId: finishRental(node, client, containerId) resumes a rental the app
176
+ // was killed in the middle of, and the container is what the provider bills.
177
+ ```
178
+
179
+ ## API Reference
180
+
181
+ ### CadreNode
182
+
183
+ The main entry point for cadre participation.
184
+
185
+ | Method | Description |
186
+ |--------|-------------|
187
+ | `start()` | Connect to control network and begin strand participation |
188
+ | `stop()` | Gracefully disconnect from all networks |
189
+ | `getStrands()` | Get all active strand instances |
190
+ | `getStrand(id)` | Get a specific strand instance |
191
+ | `foundStrand(config)` | **Create a strand**: publish its `Strand` row and start it locally as founder, in one call that is safe to re-run after an interruption. Returns the instance plus the row it actually runs under — for a closed strand read the membership key from there, since a resumed founding keeps the stored key |
192
+ | `addStrand(row)` | Attach only — start a local instance for a row that already exists (the join path; also testing/direct API). Does **not** publish |
193
+ | `stopStrand(id)` | Stop a strand on this node only (the shared `Strand` row stays; rediscovered on restart) |
194
+ | `publishStrand(id, type?, memberKey?)` | Owner-signed `Strand` row insert — makes the strand visible cadre-wide. Idempotent: a live row with the same `(Type, MemberPrivateKey)` is returned unwritten; different content on the same id throws. Prefer `foundStrand` when creating a strand |
195
+ | `unpublishStrand(id)` | Owner-signed party-wide removal — deletes this party's `Strand` row; every node stops the strand. Destroys a closed strand's `MemberPrivateKey` |
196
+ | `getEnrollmentService()` | Access peer enrollment API |
197
+ | `isOwner()` | Whether this node can act as an owner of its cadre: its identity key is wired for signing and enrolled in `OwnerKey`. The gate the owner-only flows (`createCadreInvitation`, `claimNode`, `rentNode`) share with the apps |
198
+ | `invitationReach()` | Where a device could redeem an invitation minted now from, judged on the other members' addresses (`anywhere`, `local-network`, `issuer-only`), without minting one; `rentNode` chooses its join path by it |
199
+
200
+ ### Events
201
+
202
+ | Event | Payload | Description |
203
+ |-------|---------|-------------|
204
+ | `control:connected` | - | Connected to control network |
205
+ | `control:disconnected` | - | Disconnected from control network |
206
+ | `strand:started` | `{ strandId }` | Strand instance started |
207
+ | `strand:stopped` | `{ strandId }` | Strand instance stopped |
208
+ | `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 |
209
+ | `strand:commit-durability` | `StrandCommitDurability` | After every commit on a strand's database that wrote something: whether another member already holds it (`heldElsewhere`) and whether `strand:commit-held` will say when one does (`followed`). Also readable as `instance.database.commitDurability(tx)`; see [strands.md → A commit's durability](../../docs/strands.md#a-commits-durability) |
210
+ | `strand:commit-held` | `{ strandId, transactionId }` | A followed commit held only below full replication is now held by every member of its cohort |
211
+
212
+ ## Related Documentation
213
+
214
+ - **[Cadre Architecture](../../docs/architecture.md)** - Deep dive into the cadre system design
215
+ - **[Strand Management](../../docs/strands.md)** - How strands connect multiple cadres
216
+ - **[Schema Guide](../../docs/schema-guide.md)** - Optimystic schema definitions
217
+
218
+ ## React Native Support
219
+
220
+ This package is compatible with React Native. For mobile apps, use the RN-specific storage provider:
221
+
222
+ ```typescript
223
+ import { CadreNode } from '@serfab/cadre-core';
224
+ import { RNRawStorage } from '@optimystic/db-p2p-storage-rn';
225
+ import { webSockets } from '@libp2p/websockets';
226
+ import { circuitRelayTransport } from '@libp2p/circuit-relay-v2';
227
+
228
+ const node = new CadreNode({
229
+ controlNetwork: {
230
+ partyId: 'your-unique-party-id',
231
+ bootstrapNodes: ['/dns4/relay.example.com/tcp/443/wss/p2p/12D3KooW...']
232
+ },
233
+ profile: 'transaction', // Mobile devices typically use transaction profile
234
+ strandFilter: { mode: 'sAppId', sAppId: 'com.example.myapp' },
235
+ storage: {
236
+ // RN storage provider creates per-scope AsyncStorage
237
+ provider: (scope) => new RNRawStorage(scope)
238
+ },
239
+ network: {
240
+ // TCP doesn't work in React Native — use WebSocket transport instead
241
+ transports: [webSockets(), circuitRelayTransport()],
242
+ listenAddrs: [] // RN nodes typically cannot listen
243
+ }
244
+ });
245
+ ```
246
+
247
+ ### Storage Provider Pattern
248
+
249
+ The `storage.provider` option accepts either:
250
+ - **An `IRawStorage` instance** - One store shared by every scope, so it can serve only a single party
251
+ - **A factory function** `(scope: string) => IRawStorage` - Creates isolated storage per scope (recommended)
252
+
253
+ A **scope key** is what the factory receives. It is the strand id for each strand, and
254
+ `controlStorageScope(partyId)` — `control-<lowercase hex of the party id>` — for the control
255
+ database, which holds one party's own records and must not be shared between parties. Every
256
+ key is opaque and already safe as a file, directory or database name (always within
257
+ `[a-z0-9._-]`), and two different keys are two different names even on a filesystem that
258
+ ignores case (Windows, macOS): use it verbatim, do not parse it. `isControlStorageScope(scope)`
259
+ tells the two kinds apart. The control key holds the charset by lowercase hex encoding; a strand
260
+ id holds it by check — `assertStrandScopeKey` runs on every strand launch, so a strand
261
+ whose row replicated in from another node with an unusable id (including one with an
262
+ uppercase letter) is refused rather than
263
+ handed to the factory, and on `publishStrand`, so this node never writes such an id into
264
+ the party's control database to begin with.
265
+
266
+ Available storage implementations:
267
+ | Package | Environment | Description |
268
+ |---------|-------------|-------------|
269
+ | `@optimystic/db-p2p` | All | `MemoryRawStorage` - In-memory (testing only) |
270
+ | `@optimystic/db-p2p-storage-fs` | Node.js | `FileRawStorage` - File system storage |
271
+ | `@optimystic/db-p2p-storage-rn` | React Native | `RNRawStorage` - AsyncStorage-based |
272
+
273
+ ### React Native Considerations
274
+
275
+ 1. **Network Transport**: libp2p's TCP transport doesn't work in RN. Pass `transports: [webSockets(), circuitRelayTransport()]` in the `network` config (see example above).
276
+
277
+ 2. **Schema Loading**: The control schema is embedded in the package. Do not set `schemaPath` in React Native.
278
+
279
+ 3. **Profile**: Mobile devices should use `profile: 'transaction'` for battery/bandwidth efficiency.
280
+
281
+ 4. **Strand Filtering**: Always filter strands on mobile to avoid participating in unnecessary networks.
282
+
283
+ ## Related Packages
284
+
285
+ - **[@optimystic/db-core](https://github.com/gotchoices/optimystic)** - Distributed database core
286
+ - **[@optimystic/db-p2p](https://github.com/gotchoices/optimystic)** - libp2p integration for Optimystic
287
+ - **[@optimystic/db-p2p-storage-fs](https://github.com/gotchoices/optimystic)** - File system storage (Node.js)
288
+ - **[@optimystic/db-p2p-storage-rn](https://github.com/gotchoices/optimystic)** - AsyncStorage (React Native)
289
+
290
+ ## License
291
+
292
+ MIT
293
+