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