@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.
- package/README.md +293 -274
- package/dist/cadre-invite-protocol.d.ts +64 -29
- package/dist/cadre-invite-protocol.js +207 -38
- package/dist/cadre-invite-protocol.js.map +1 -1
- package/dist/cadre-join-request.d.ts +42 -0
- package/dist/cadre-join-request.js +144 -0
- package/dist/cadre-join-request.js.map +1 -0
- package/dist/cadre-node.d.ts +375 -79
- package/dist/cadre-node.js +758 -186
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-database.d.ts +92 -12
- package/dist/control-database.js +323 -142
- package/dist/control-database.js.map +1 -1
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +19 -3
- package/dist/control-schema.js.map +1 -1
- package/dist/ed25519-key.d.ts +7 -9
- package/dist/ed25519-key.js +20 -10
- package/dist/ed25519-key.js.map +1 -1
- package/dist/fetch-impl.d.ts +12 -0
- package/dist/fetch-impl.js +23 -0
- package/dist/fetch-impl.js.map +1 -0
- package/dist/file-durable-slot.js +2 -1
- package/dist/file-durable-slot.js.map +1 -1
- package/dist/file-safe-name.d.ts +25 -0
- package/dist/file-safe-name.js +29 -0
- package/dist/file-safe-name.js.map +1 -0
- package/dist/formation-approval.js +5 -11
- package/dist/formation-approval.js.map +1 -1
- package/dist/fs-atomic.d.ts +0 -19
- package/dist/fs-atomic.js +3 -24
- package/dist/fs-atomic.js.map +1 -1
- package/dist/index.d.ts +12 -5
- package/dist/index.js +31 -6
- package/dist/index.js.map +1 -1
- package/dist/invitation-bootstrap.d.ts +50 -12
- package/dist/invitation-bootstrap.js +76 -13
- package/dist/invitation-bootstrap.js.map +1 -1
- package/dist/joined-strand-store.d.ts +7 -0
- package/dist/joined-strand-store.js +9 -0
- package/dist/joined-strand-store.js.map +1 -1
- package/dist/key-store-file.js +2 -1
- package/dist/key-store-file.js.map +1 -1
- package/dist/link-budget.d.ts +9 -4
- package/dist/link-budget.js +9 -4
- package/dist/link-budget.js.map +1 -1
- package/dist/machine-removal.d.ts +78 -0
- package/dist/machine-removal.js +166 -0
- package/dist/machine-removal.js.map +1 -0
- package/dist/membership-connection-gater.d.ts +6 -5
- package/dist/membership-connection-gater.js +6 -5
- package/dist/membership-connection-gater.js.map +1 -1
- package/dist/node-claim-payload.d.ts +55 -0
- package/dist/node-claim-payload.js +108 -4
- package/dist/node-claim-payload.js.map +1 -1
- package/dist/node-default-transports.d.ts +27 -0
- package/dist/node-default-transports.js +35 -0
- package/dist/node-default-transports.js.map +1 -0
- package/dist/node-transports.d.ts +30 -0
- package/dist/node-transports.js +40 -0
- package/dist/node-transports.js.map +1 -0
- package/dist/peer-dial.d.ts +124 -27
- package/dist/peer-dial.js +254 -30
- package/dist/peer-dial.js.map +1 -1
- package/dist/peer-join-backfill.js +6 -0
- package/dist/peer-join-backfill.js.map +1 -1
- package/dist/peer-record.d.ts +6 -45
- package/dist/peer-record.js +8 -50
- package/dist/peer-record.js.map +1 -1
- package/dist/primary-lan-address.d.ts +8 -0
- package/dist/primary-lan-address.js +46 -0
- package/dist/primary-lan-address.js.map +1 -0
- package/dist/provider/client.d.ts +131 -0
- package/dist/provider/client.js +226 -0
- package/dist/provider/client.js.map +1 -0
- package/dist/provider/descriptor.d.ts +41 -0
- package/dist/provider/descriptor.js +103 -0
- package/dist/provider/descriptor.js.map +1 -0
- package/dist/provider/index.d.ts +9 -0
- package/dist/provider/index.js +10 -0
- package/dist/provider/index.js.map +1 -0
- package/dist/provider/oauth.d.ts +125 -0
- package/dist/provider/oauth.js +199 -0
- package/dist/provider/oauth.js.map +1 -0
- package/dist/provider/rent-node.d.ts +101 -0
- package/dist/provider/rent-node.js +220 -0
- package/dist/provider/rent-node.js.map +1 -0
- package/dist/relay-addrs.d.ts +9 -4
- package/dist/relay-addrs.js +9 -4
- package/dist/relay-addrs.js.map +1 -1
- package/dist/relay-reservation.d.ts +51 -16
- package/dist/relay-reservation.js +107 -24
- package/dist/relay-reservation.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +22 -5
- package/dist/seed-bootstrap.js +48 -25
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/strand-commit-durability.d.ts +54 -0
- package/dist/strand-commit-durability.js +74 -0
- package/dist/strand-commit-durability.js.map +1 -0
- package/dist/strand-database.d.ts +23 -8
- package/dist/strand-database.js +41 -13
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +7 -1
- package/dist/strand-first-sync-gate.js +7 -1
- package/dist/strand-first-sync-gate.js.map +1 -1
- package/dist/strand-formation-protocol.js +4 -1
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-fret-addrs.d.ts +19 -1
- package/dist/strand-fret-addrs.js +27 -9
- package/dist/strand-fret-addrs.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +81 -19
- package/dist/strand-instance-manager.js +216 -105
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-move.d.ts +124 -0
- package/dist/strand-move.js +438 -0
- package/dist/strand-move.js.map +1 -0
- package/dist/strand-network-config.js +7 -6
- package/dist/strand-network-config.js.map +1 -1
- package/dist/strand-peer-redial.d.ts +113 -0
- package/dist/strand-peer-redial.js +236 -0
- package/dist/strand-peer-redial.js.map +1 -0
- package/dist/strand-wake-protocol.js +10 -7
- package/dist/strand-wake-protocol.js.map +1 -1
- package/dist/types.d.ts +212 -4
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -1
- package/package.json +18 -10
- package/src/cadre-invite-protocol.ts +1052 -864
- package/src/cadre-join-request.ts +167 -0
- package/src/cadre-node.ts +10003 -9381
- package/src/control-database.ts +4663 -4451
- package/src/control-schema.ts +19 -3
- package/src/ed25519-key.ts +20 -10
- package/src/fetch-impl.ts +23 -0
- package/src/file-durable-slot.ts +64 -63
- package/src/file-safe-name.ts +32 -0
- package/src/formation-approval.ts +604 -610
- package/src/fs-atomic.ts +92 -117
- package/src/index.ts +71 -4
- package/src/invitation-bootstrap.ts +91 -13
- package/src/joined-strand-store.ts +508 -498
- package/src/key-store-file.ts +142 -141
- package/src/link-budget.ts +9 -4
- package/src/machine-removal.ts +216 -0
- package/src/membership-connection-gater.ts +638 -637
- package/src/node-claim-payload.ts +138 -4
- package/src/node-default-transports.ts +38 -0
- package/src/node-transports.ts +49 -0
- package/src/peer-dial.ts +604 -336
- package/src/peer-join-backfill.ts +6 -0
- package/src/peer-record.ts +159 -202
- package/src/primary-lan-address.ts +47 -0
- package/src/provider/client.ts +326 -0
- package/src/provider/descriptor.ts +129 -0
- package/src/provider/index.ts +59 -0
- package/src/provider/oauth.ts +285 -0
- package/src/provider/rent-node.ts +275 -0
- package/src/relay-addrs.ts +9 -4
- package/src/relay-reservation.ts +1211 -1121
- package/src/seed-bootstrap.ts +1502 -1471
- package/src/strand-commit-durability.ts +116 -0
- package/src/strand-database.ts +46 -14
- package/src/strand-first-sync-gate.ts +7 -1
- package/src/strand-formation-protocol.ts +1039 -1036
- package/src/strand-fret-addrs.ts +179 -145
- package/src/strand-instance-manager.ts +2026 -1883
- package/src/strand-move.ts +585 -0
- package/src/strand-network-config.ts +7 -6
- package/src/strand-peer-redial.ts +307 -0
- package/src/strand-wake-protocol.ts +10 -7
- 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
|
|
145
|
-
|
|
146
|
-
```typescript
|
|
147
|
-
//
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
|
186
|
-
|
|
187
|
-
| `
|
|
188
|
-
| `
|
|
189
|
-
| `
|
|
190
|
-
| `
|
|
191
|
-
| `
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
+
|