@serfab/cadre-core 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +255 -253
- package/dist/bootstrap-peer-store-file.d.ts +13 -0
- package/dist/bootstrap-peer-store-file.js +42 -0
- package/dist/bootstrap-peer-store-file.js.map +1 -0
- package/dist/bootstrap-peer-store.d.ts +73 -0
- package/dist/bootstrap-peer-store.js +128 -0
- package/dist/bootstrap-peer-store.js.map +1 -0
- package/dist/cadre-node.d.ts +1121 -81
- package/dist/cadre-node.js +2276 -262
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-authorization.d.ts +93 -0
- package/dist/control-authorization.js +57 -0
- package/dist/control-authorization.js.map +1 -0
- package/dist/control-cohort.d.ts +26 -0
- package/dist/control-cohort.js +26 -0
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +959 -54
- package/dist/control-database.js +1421 -148
- package/dist/control-database.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +90 -10
- package/dist/control-formation-recorder.js +130 -14
- package/dist/control-formation-recorder.js.map +1 -1
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +651 -72
- package/dist/control-schema.js.map +1 -1
- package/dist/control-stream.d.ts +38 -4
- package/dist/control-stream.js +70 -4
- package/dist/control-stream.js.map +1 -1
- package/dist/control-write-retry.d.ts +161 -0
- package/dist/control-write-retry.js +412 -0
- package/dist/control-write-retry.js.map +1 -0
- package/dist/delegate-admission.d.ts +134 -0
- package/dist/delegate-admission.js +244 -0
- package/dist/delegate-admission.js.map +1 -0
- package/dist/device-token.d.ts +11 -11
- package/dist/device-token.js +14 -13
- package/dist/device-token.js.map +1 -1
- package/dist/ed25519-key.d.ts +26 -0
- package/dist/ed25519-key.js +56 -1
- package/dist/ed25519-key.js.map +1 -1
- package/dist/file-durable-slot.d.ts +20 -0
- package/dist/file-durable-slot.js +53 -0
- package/dist/file-durable-slot.js.map +1 -0
- package/dist/formation-approval.d.ts +157 -0
- package/dist/formation-approval.js +375 -0
- package/dist/formation-approval.js.map +1 -0
- package/dist/fs-atomic.d.ts +38 -0
- package/dist/fs-atomic.js +97 -0
- package/dist/fs-atomic.js.map +1 -0
- package/dist/identity-key.d.ts +49 -0
- package/dist/identity-key.js +81 -0
- package/dist/identity-key.js.map +1 -0
- package/dist/index.d.ts +17 -7
- package/dist/index.js +40 -6
- package/dist/index.js.map +1 -1
- package/dist/key-store-file.d.ts +2 -24
- package/dist/key-store-file.js +10 -77
- package/dist/key-store-file.js.map +1 -1
- package/dist/membership-connection-gater.d.ts +146 -0
- package/dist/membership-connection-gater.js +180 -0
- package/dist/membership-connection-gater.js.map +1 -0
- package/dist/node-local-snapshot.d.ts +109 -0
- package/dist/node-local-snapshot.js +182 -0
- package/dist/node-local-snapshot.js.map +1 -0
- package/dist/peer-addr-book.d.ts +92 -0
- package/dist/peer-addr-book.js +220 -0
- package/dist/peer-addr-book.js.map +1 -0
- package/dist/peer-authorization.d.ts +135 -18
- package/dist/peer-authorization.js +152 -21
- package/dist/peer-authorization.js.map +1 -1
- package/dist/peer-record.d.ts +21 -8
- package/dist/peer-record.js +26 -10
- package/dist/peer-record.js.map +1 -1
- package/dist/relay-addrs.d.ts +45 -0
- package/dist/relay-addrs.js +103 -0
- package/dist/relay-addrs.js.map +1 -0
- package/dist/relay-reservation.d.ts +266 -0
- package/dist/relay-reservation.js +613 -0
- package/dist/relay-reservation.js.map +1 -0
- package/dist/seed-bootstrap.d.ts +228 -56
- package/dist/seed-bootstrap.js +340 -198
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/seed-trust-policy.d.ts +47 -21
- package/dist/seed-trust-policy.js +24 -15
- package/dist/seed-trust-policy.js.map +1 -1
- package/dist/strand-addr-protocol.d.ts +42 -13
- package/dist/strand-addr-protocol.js +58 -52
- package/dist/strand-addr-protocol.js.map +1 -1
- package/dist/strand-backfill.d.ts +109 -0
- package/dist/strand-backfill.js +279 -0
- package/dist/strand-backfill.js.map +1 -0
- package/dist/strand-cohort.d.ts +4 -37
- package/dist/strand-cohort.js +4 -15
- package/dist/strand-cohort.js.map +1 -1
- package/dist/strand-database.d.ts +11 -16
- package/dist/strand-database.js +19 -7
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-formation-manager.d.ts +36 -7
- package/dist/strand-formation-manager.js +115 -15
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +103 -4
- package/dist/strand-formation-protocol.js +232 -4
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +88 -19
- package/dist/strand-instance-manager.js +178 -34
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-member-registry.d.ts +5 -5
- package/dist/strand-member-registry.js +18 -10
- package/dist/strand-member-registry.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +484 -88
- package/dist/strand-membership-writer.js +751 -128
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-solicitation.d.ts +99 -33
- package/dist/strand-solicitation.js +100 -52
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-transport-key.d.ts +40 -0
- package/dist/strand-transport-key.js +56 -0
- package/dist/strand-transport-key.js.map +1 -0
- package/dist/strand-wake-protocol.d.ts +9 -7
- package/dist/strand-wake-protocol.js +22 -46
- package/dist/strand-wake-protocol.js.map +1 -1
- package/dist/strand-watcher.d.ts +17 -2
- package/dist/strand-watcher.js +47 -3
- package/dist/strand-watcher.js.map +1 -1
- package/dist/trusted-owner-store-file.d.ts +12 -0
- package/dist/trusted-owner-store-file.js +39 -0
- package/dist/trusted-owner-store-file.js.map +1 -0
- package/dist/trusted-owner-store.d.ts +64 -0
- package/dist/trusted-owner-store.js +126 -0
- package/dist/trusted-owner-store.js.map +1 -0
- package/dist/types.d.ts +307 -44
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -1
- package/package.json +20 -8
- package/src/arachnode-stub.ts +159 -159
- package/src/bootstrap-peer-store-file.ts +44 -0
- package/src/bootstrap-peer-store.ts +172 -0
- package/src/cadre-node.ts +5158 -2981
- package/src/canonical-json.ts +30 -30
- package/src/control-authorization.ts +114 -0
- package/src/control-cohort.ts +130 -103
- package/src/control-database.ts +2406 -948
- package/src/control-formation-recorder.ts +305 -151
- package/src/control-schema.ts +651 -72
- package/src/control-stream.ts +205 -128
- package/src/control-write-retry.ts +483 -0
- package/src/delegate-admission.ts +302 -0
- package/src/device-token.ts +94 -89
- package/src/diagnostics/connection-path.ts +277 -277
- package/src/diagnostics/webrtc-turn-tracker.ts +222 -222
- package/src/ed25519-key.ts +66 -1
- package/src/enrollment.ts +199 -199
- package/src/file-durable-slot.ts +61 -0
- package/src/formation-approval.ts +604 -0
- package/src/fs-atomic.ts +107 -0
- package/src/hibernation-manager.ts +437 -437
- package/src/identity-key.ts +109 -0
- package/src/index.ts +137 -5
- package/src/key-store-file.ts +132 -203
- package/src/key-store.ts +99 -99
- package/src/membership-connection-gater.ts +204 -0
- package/src/node-local-snapshot.ts +260 -0
- package/src/peer-addr-book.ts +250 -0
- package/src/peer-authorization.ts +259 -81
- package/src/peer-record.ts +164 -146
- package/src/push-credentials.ts +84 -84
- package/src/push-node.ts +64 -64
- package/src/push-notifier-apns.ts +247 -247
- package/src/push-notifier-fcm.ts +220 -220
- package/src/push-notifier-shared.ts +28 -28
- package/src/push-notifier.ts +52 -52
- package/src/relay-addrs.ts +111 -0
- package/src/relay-reservation.ts +823 -0
- package/src/seed-bootstrap.ts +409 -209
- package/src/seed-trust-policy.ts +140 -107
- package/src/strand-addr-protocol.ts +441 -410
- package/src/strand-backfill.ts +366 -0
- package/src/strand-cohort.ts +38 -77
- package/src/strand-database.ts +202 -204
- package/src/strand-formation-manager.ts +491 -368
- package/src/strand-formation-protocol.ts +692 -391
- package/src/strand-instance-manager.ts +201 -46
- package/src/strand-member-key.ts +39 -39
- package/src/strand-member-registry.ts +182 -177
- package/src/strand-membership-writer.ts +938 -146
- package/src/strand-solicitation.ts +472 -407
- package/src/strand-transport-key.ts +58 -0
- package/src/strand-wake-payload.ts +32 -32
- package/src/strand-wake-protocol.ts +314 -331
- package/src/strand-watcher.ts +314 -251
- package/src/trusted-owner-store-file.ts +41 -0
- package/src/trusted-owner-store.ts +171 -0
- package/src/types.ts +1309 -1032
package/README.md
CHANGED
|
@@ -1,253 +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
|
-
| `
|
|
173
|
-
| `
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
|
180
|
-
|
|
181
|
-
| `
|
|
182
|
-
| `
|
|
183
|
-
| `strand:
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
- **[
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
import {
|
|
199
|
-
import {
|
|
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
|
-
| `@optimystic/db-p2p
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
- **[@optimystic/db-
|
|
248
|
-
- **[@optimystic/db-p2p
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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,13 @@
|
|
|
1
|
+
import type { BootstrapPeerStore } from './bootstrap-peer-store.js';
|
|
2
|
+
/**
|
|
3
|
+
* File-backed {@link BootstrapPeerStore}. Open via {@link open}, which loads the
|
|
4
|
+
* existing file and returns the cross-platform store over it; an absent, corrupt, or wrong-party file is a cold
|
|
5
|
+
* start (empty store) and structurally junk entries are dropped, while a
|
|
6
|
+
* present-but-unreadable file throws. Those rules and their reasoning live on
|
|
7
|
+
* `NodeLocalSnapshot.open` — including the in-process-only write serialisation,
|
|
8
|
+
* which two nodes sharing one directory for one party would defeat.
|
|
9
|
+
*/
|
|
10
|
+
export declare const FileBootstrapPeerStore: {
|
|
11
|
+
/** Load (or cold-start) the party's retained dial targets from a file in `dir`. */
|
|
12
|
+
open(dir: string, partyId: string): Promise<BootstrapPeerStore>;
|
|
13
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node-only file-backed {@link BootstrapPeerStore}: one JSON file per party under
|
|
3
|
+
* a configured directory (the node's state directory — `cadre-cli` passes
|
|
4
|
+
* `ResolvedConfig.nodeStateDir`), holding the node-local, non-replicated
|
|
5
|
+
* cold-start dial targets a seed nominated.
|
|
6
|
+
*
|
|
7
|
+
* Nothing but the file itself lives here: the envelope, load policy, per-entry
|
|
8
|
+
* validation (shape only — never signatures; see the `bootstrap-peer-store.ts`
|
|
9
|
+
* module comment for why nothing persisted here is trust-bearing) and the
|
|
10
|
+
* serialised snapshot writes are cross-platform in
|
|
11
|
+
* `PersistentBootstrapPeerStore` / `node-local-snapshot.ts`, and the file is a
|
|
12
|
+
* `FileDurableSlot`. This module exists only so the `node:fs` edge stays out of
|
|
13
|
+
* the package's cross-platform default entry (`./index.js`) — import it from the
|
|
14
|
+
* dedicated subpath instead:
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { FileBootstrapPeerStore } from '@serfab/cadre-core/bootstrap-peer-store-file';
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* A React Native / browser entry graph never resolves this path, so the
|
|
21
|
+
* `node:fs` edge never reaches a bundler that cannot satisfy it (same isolation
|
|
22
|
+
* pattern as `trusted-owner-store-file.ts` / `key-store-file.ts`).
|
|
23
|
+
*/
|
|
24
|
+
import { FileDurableSlot } from './file-durable-slot.js';
|
|
25
|
+
import { PersistentBootstrapPeerStore } from './bootstrap-peer-store.js';
|
|
26
|
+
/** Base name of the store file: `<dir>/bootstrap-peers.<encoded partyId>.json`. */
|
|
27
|
+
const SLOT_NAME = 'bootstrap-peers';
|
|
28
|
+
/**
|
|
29
|
+
* File-backed {@link BootstrapPeerStore}. Open via {@link open}, which loads the
|
|
30
|
+
* existing file and returns the cross-platform store over it; an absent, corrupt, or wrong-party file is a cold
|
|
31
|
+
* start (empty store) and structurally junk entries are dropped, while a
|
|
32
|
+
* present-but-unreadable file throws. Those rules and their reasoning live on
|
|
33
|
+
* `NodeLocalSnapshot.open` — including the in-process-only write serialisation,
|
|
34
|
+
* which two nodes sharing one directory for one party would defeat.
|
|
35
|
+
*/
|
|
36
|
+
export const FileBootstrapPeerStore = {
|
|
37
|
+
/** Load (or cold-start) the party's retained dial targets from a file in `dir`. */
|
|
38
|
+
async open(dir, partyId) {
|
|
39
|
+
return PersistentBootstrapPeerStore.open(new FileDurableSlot(dir, SLOT_NAME, partyId), partyId);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=bootstrap-peer-store-file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bootstrap-peer-store-file.js","sourceRoot":"","sources":["../src/bootstrap-peer-store-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,4BAA4B,EAAE,MAAM,2BAA2B,CAAC;AAGzE,mFAAmF;AACnF,MAAM,SAAS,GAAG,iBAAiB,CAAC;AAEpC;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACrC,mFAAmF;IACnF,KAAK,CAAC,IAAI,CAAC,GAAW,EAAE,OAAe;QACtC,OAAO,4BAA4B,CAAC,IAAI,CAAC,IAAI,eAAe,CAAC,GAAG,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,CAAC;IACjG,CAAC;CACD,CAAC"}
|