@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
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A running strand node redialing the strand's other members itself, when it holds a
|
|
3
|
+
* connection to none of them, with the dial budget the host's declared link gives a
|
|
4
|
+
* relayed connection.
|
|
5
|
+
*
|
|
6
|
+
* Why. Below cadre-core the only thing that redials a strand peer is FRET's maintenance:
|
|
7
|
+
* its pings and neighbour announcements open the connection themselves (`dial: true`) under
|
|
8
|
+
* a fixed 2 000 ms limit on the whole call (`MAINTENANCE_RPC_TIMEOUT_MS` in `p2p-fret`'s
|
|
9
|
+
* `fret-service.ts`). Opening a relayed connection takes longer than that on a slow link:
|
|
10
|
+
* about 9 s on a 20 ms per-frame cap with a 70%-busy event loop
|
|
11
|
+
* (`joiner-first-commit-over-slow-relay`, whose slow-restart arm reproduces this), where
|
|
12
|
+
* {@link relayedDialBudgetMs} allows 21.5 s at the default declaration. Each such redial is
|
|
13
|
+
* cut off part-way through the handshake, from both sides, for as long as the link stays that
|
|
14
|
+
* slow. And after a restart nothing else dials a strand peer: a cross-party strand's launch
|
|
15
|
+
* seed is empty, and cadre-core's address passes only fill the address book. Two phones that
|
|
16
|
+
* restarted then never reconnect, and a write either made alone never reaches the other.
|
|
17
|
+
*
|
|
18
|
+
* What it does. One loop per strand runtime, started in `StrandInstanceManager`'s runtime
|
|
19
|
+
* build and stopped with the runtime (stop, quiesce, failed build), so a hibernating strand
|
|
20
|
+
* never dials. Each look at the node:
|
|
21
|
+
* - a member connected: nothing to do; look again in {@link STRAND_REDIAL_CHECK_MS}, or as
|
|
22
|
+
* soon as a peer disconnects.
|
|
23
|
+
* - no member connected and none known: likewise nothing (a founder alone, a storage replica
|
|
24
|
+
* before anyone arrives).
|
|
25
|
+
* - no member connected and some known: dial every known member at once
|
|
26
|
+
* ({@link dialPeerAddrs}, direct addresses before relayed ones), each address with the
|
|
27
|
+
* relayed-dial budget. A round that connects to one goes back to watching; one that does
|
|
28
|
+
* not waits a backoff starting at that budget and doubling to
|
|
29
|
+
* {@link STRAND_REDIAL_MAX_BACKOFF_MS}.
|
|
30
|
+
*
|
|
31
|
+
* Who is known: every peer the node's FRET table holds a verified signed address record for
|
|
32
|
+
* (`strand-fret-addrs.ts`), plus the launch seed (sibling strand addresses and a formation's
|
|
33
|
+
* carried addresses). Who counts as connected: a peer FRET labels `'member'` (it has seen it
|
|
34
|
+
* speak this strand's protocols), or a seed peer. Neither ever includes a configured relay, a
|
|
35
|
+
* peer FRET labels `'foreign'`, or a peer the revocation gate refuses. The relay matters most:
|
|
36
|
+
* the strand node is connected to it throughout and FRET files it with a record like any
|
|
37
|
+
* peer, so counting it would leave this loop idle exactly when it is needed.
|
|
38
|
+
*
|
|
39
|
+
* NOTE: this compensates for FRET's 2 000 ms maintenance dial limit, which cannot open a
|
|
40
|
+
* relayed connection on a slow link (upstream half:
|
|
41
|
+
* `blocked/fret-maintenance-ping-dial-budget-below-a-relayed-dial`). If FRET's maintenance
|
|
42
|
+
* dial ever takes a limit derived from the declared link, this loop becomes a backstop and
|
|
43
|
+
* may be deleted, as `peer-join-backfill.ts` says of itself.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import debug from 'debug';
|
|
47
|
+
import type { Multiaddr } from '@multiformats/multiaddr';
|
|
48
|
+
import { relayedDialBudgetMs } from './link-budget.js';
|
|
49
|
+
import { CONTROL_COHORT_DIAL_ADDRESS_ATTEMPTS, SelfRelayOnlyError, dialPeerAddrs, type AddrDialer, type PeerDialBudget } from './peer-dial.js';
|
|
50
|
+
import { groupAddrsByPeerId } from './peer-addr-book.js';
|
|
51
|
+
import { strandFretMembership, strandFretPeerAddrs, type FretAddrHost, type FretMembership } from './strand-fret-addrs.js';
|
|
52
|
+
import { defaultTimeoutScheduler, type TimeoutScheduler } from './timeout-scheduler.js';
|
|
53
|
+
|
|
54
|
+
const log = debug('sereus:cadre:strand-redial');
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Gap between looks while a member is connected or none is known. While a member is
|
|
58
|
+
* connected a look reads FRET's labels and the connection list and checks no signature
|
|
59
|
+
* ({@link strandFretMembership}); a disconnect also triggers a look at once.
|
|
60
|
+
*/
|
|
61
|
+
export const STRAND_REDIAL_CHECK_MS = 5_000;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Ceiling on the wait between failed rounds. A member that is gone for good is dialed this
|
|
65
|
+
* often for as long as FRET keeps its record (up to 14 days after it was last confirmed).
|
|
66
|
+
*/
|
|
67
|
+
export const STRAND_REDIAL_MAX_BACKOFF_MS = 5 * 60_000;
|
|
68
|
+
|
|
69
|
+
/** The loop's dial budget and cadence. */
|
|
70
|
+
export interface StrandRedialTimings {
|
|
71
|
+
/** Limit on one address's dial, and on one member's addresses together. */
|
|
72
|
+
budget: PeerDialBudget;
|
|
73
|
+
/** Wait after the first failed round; doubled after each further one. */
|
|
74
|
+
minBackoffMs: number;
|
|
75
|
+
/** Ceiling the backoff doubles up to. */
|
|
76
|
+
maxBackoffMs: number;
|
|
77
|
+
/** Gap between looks while nothing needs dialing. */
|
|
78
|
+
checkMs: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The timings at a declared link round trip: {@link relayedDialBudgetMs} per address, the
|
|
83
|
+
* same whole-budget count per member as the control cohort's dials (dead direct addresses
|
|
84
|
+
* a record lists ahead of the relayed one that works), and a first backoff of one per-address
|
|
85
|
+
* budget.
|
|
86
|
+
*/
|
|
87
|
+
export function strandRedialTimings(linkRoundTripMs?: number): StrandRedialTimings {
|
|
88
|
+
const perAddressMs = relayedDialBudgetMs(linkRoundTripMs);
|
|
89
|
+
return {
|
|
90
|
+
budget: { perAddressMs, totalMs: CONTROL_COHORT_DIAL_ADDRESS_ATTEMPTS * perAddressMs },
|
|
91
|
+
minBackoffMs: perAddressMs,
|
|
92
|
+
maxBackoffMs: STRAND_REDIAL_MAX_BACKOFF_MS,
|
|
93
|
+
checkMs: STRAND_REDIAL_CHECK_MS
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The slice of a strand's libp2p node the loop uses. Structurally satisfied by db-p2p's node. */
|
|
98
|
+
export interface RedialNode extends AddrDialer {
|
|
99
|
+
services?: FretAddrHost['services'];
|
|
100
|
+
getConnections(): ReadonlyArray<{ remotePeer: { toString(): string } }>;
|
|
101
|
+
addEventListener(type: 'peer:disconnect', listener: () => void): void;
|
|
102
|
+
removeEventListener(type: 'peer:disconnect', listener: () => void): void;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface StrandRedialDeps {
|
|
106
|
+
/** Names the strand in logs. */
|
|
107
|
+
label: string;
|
|
108
|
+
node: RedialNode;
|
|
109
|
+
/**
|
|
110
|
+
* The launch seed: multiaddr strings, each naming its peer in a trailing `/p2p/`.
|
|
111
|
+
*
|
|
112
|
+
* NOTE: read once, at runtime build. Addresses a re-formation carries while the strand runs
|
|
113
|
+
* reach the peerStore (`recordFormationStrandAddrs`) but not this loop until the runtime is
|
|
114
|
+
* rebuilt; if a re-formed strand is seen not reconnecting until relaunch, read the seed per round.
|
|
115
|
+
*/
|
|
116
|
+
seed: readonly string[];
|
|
117
|
+
/** Peer ids never dialed nor counted as members: the relays the node reserves on. */
|
|
118
|
+
excluded: ReadonlySet<string>;
|
|
119
|
+
/** The revocation gate's current verdict, asked of peers and of this node itself; absent for an open strand. */
|
|
120
|
+
isRevoked?: (peerId: string) => boolean;
|
|
121
|
+
/** Omit for real (unref'd) timers. */
|
|
122
|
+
scheduler?: TimeoutScheduler;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** A running redial loop; its only imperative is {@link stop}. */
|
|
126
|
+
export interface StrandPeerRedial {
|
|
127
|
+
/** Idempotent. No look or round starts after it; dials in flight run out on their own deadlines, and their result is discarded. */
|
|
128
|
+
stop(): void;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** How a look ended: nothing needed dialing or a round connected (`'watch'`), or a round reached no member (`'retry'`). */
|
|
132
|
+
type LookOutcome = 'watch' | 'retry';
|
|
133
|
+
|
|
134
|
+
/** Start the loop; its first look runs at once. */
|
|
135
|
+
export function superviseStrandPeerRedial(deps: StrandRedialDeps, timings: StrandRedialTimings): StrandPeerRedial {
|
|
136
|
+
return new StrandPeerRedialLoop(deps, timings);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
class StrandPeerRedialLoop implements StrandPeerRedial {
|
|
140
|
+
private readonly scheduler: TimeoutScheduler;
|
|
141
|
+
private readonly seed: Map<string, Multiaddr[]>;
|
|
142
|
+
private readonly selfId: string;
|
|
143
|
+
private backoffMs: number;
|
|
144
|
+
private timer: unknown = null;
|
|
145
|
+
/**
|
|
146
|
+
* Whether the pending wait is a plain gap between looks, which a disconnect cuts short. A
|
|
147
|
+
* backoff after a failed round is not cut short, or a flapping relay connection would turn
|
|
148
|
+
* the backoff into a dial on every flap; and while a look or round runs there is no wait.
|
|
149
|
+
*/
|
|
150
|
+
private watching = false;
|
|
151
|
+
private stopped = false;
|
|
152
|
+
private readonly onDisconnect = (): void => {
|
|
153
|
+
if (this.watching && !this.stopped) {
|
|
154
|
+
this.cancelTimer();
|
|
155
|
+
void this.tick();
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
constructor(private readonly deps: StrandRedialDeps, private readonly timings: StrandRedialTimings) {
|
|
160
|
+
this.scheduler = deps.scheduler ?? defaultTimeoutScheduler;
|
|
161
|
+
this.seed = groupAddrsByPeerId([...deps.seed]);
|
|
162
|
+
this.selfId = deps.node.peerId.toString();
|
|
163
|
+
this.backoffMs = timings.minBackoffMs;
|
|
164
|
+
deps.node.addEventListener('peer:disconnect', this.onDisconnect);
|
|
165
|
+
void this.tick();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
stop(): void {
|
|
169
|
+
if (this.stopped) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
this.stopped = true;
|
|
173
|
+
this.cancelTimer();
|
|
174
|
+
this.deps.node.removeEventListener('peer:disconnect', this.onDisconnect);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private async tick(): Promise<void> {
|
|
178
|
+
if (this.stopped) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
this.watching = false;
|
|
182
|
+
let outcome: LookOutcome = 'watch';
|
|
183
|
+
try {
|
|
184
|
+
outcome = await this.lookAndDial();
|
|
185
|
+
} catch (error) {
|
|
186
|
+
// A node torn down under the look throws from its table export or connection list.
|
|
187
|
+
log('strand %s: redial look failed (looking again in %dms): %o', this.deps.label, this.timings.checkMs, error);
|
|
188
|
+
}
|
|
189
|
+
if (this.stopped) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
this.watching = outcome === 'watch';
|
|
193
|
+
const waitMs = outcome === 'watch' ? this.resetBackoff() : this.nextBackoff();
|
|
194
|
+
this.timer = this.scheduler.setTimeout(() => {
|
|
195
|
+
this.timer = null;
|
|
196
|
+
void this.tick();
|
|
197
|
+
}, waitMs);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* One look, and a round of dials when it finds no member connected but some known. A node
|
|
202
|
+
* whose own party was removed dials nobody: the strand keeps running for the session, but a
|
|
203
|
+
* removed party must not keep dialing the strand's peers (`CadreNode.forgetRevokedJoin`).
|
|
204
|
+
*/
|
|
205
|
+
private async lookAndDial(): Promise<LookOutcome> {
|
|
206
|
+
if (this.deps.isRevoked?.(this.selfId) === true) {
|
|
207
|
+
return 'watch';
|
|
208
|
+
}
|
|
209
|
+
const membership = strandFretMembership(this.deps.node);
|
|
210
|
+
if (this.memberConnected(membership)) {
|
|
211
|
+
return 'watch';
|
|
212
|
+
}
|
|
213
|
+
const known = await this.knownMembers(membership);
|
|
214
|
+
if (known.size === 0 || this.stopped) {
|
|
215
|
+
return 'watch';
|
|
216
|
+
}
|
|
217
|
+
log('strand %s: no member connected; dialing %d known member(s)', this.deps.label, known.size);
|
|
218
|
+
if (await this.dialAny(known)) {
|
|
219
|
+
log('strand %s: reconnected to a member', this.deps.label);
|
|
220
|
+
return 'watch';
|
|
221
|
+
}
|
|
222
|
+
log('strand %s: no known member answered; next round in %dms', this.deps.label, this.backoffMs);
|
|
223
|
+
return 'retry';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** The gap between looks, with the backoff started over. */
|
|
227
|
+
private resetBackoff(): number {
|
|
228
|
+
this.backoffMs = this.timings.minBackoffMs;
|
|
229
|
+
return this.timings.checkMs;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The wait after a round that reached no member, doubling the next one up to the cap.
|
|
234
|
+
*
|
|
235
|
+
* NOTE: a round whose dials failed within milliseconds (a relay connection that is still
|
|
236
|
+
* closing, the member's relay reservation not back yet) waits as long as one whose dials ran
|
|
237
|
+
* out their budget. In `joiner-first-commit-over-slow-relay`'s third arm that first wait is what
|
|
238
|
+
* leaves the joiner writable with no member connected. If reconnecting within seconds of a cut
|
|
239
|
+
* ever matters, retry a round that failed that fast after a short delay instead.
|
|
240
|
+
*
|
|
241
|
+
* NOTE: one such fast failure is `ConnectionClosedError: The connection muxer is "closed"`. A
|
|
242
|
+
* relay that aborts its connection to this node cannot tell it so (`@libp2p/websockets`'
|
|
243
|
+
* listener resets with close code 1006, which the `ws` package refuses to send;
|
|
244
|
+
* `blocked/report-libp2p-websockets-buffered-amount`, second arm), so this node keeps a dead
|
|
245
|
+
* relay connection until its own monitor drops it, up to 65 s, and every relayed dial reuses
|
|
246
|
+
* it. If rounds keep failing that way, close a relay connection whose muxer is closed before
|
|
247
|
+
* the next round instead of waiting it out.
|
|
248
|
+
*/
|
|
249
|
+
private nextBackoff(): number {
|
|
250
|
+
const waitMs = this.backoffMs;
|
|
251
|
+
this.backoffMs = Math.min(waitMs * 2, this.timings.maxBackoffMs);
|
|
252
|
+
return waitMs;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private eligible(peerId: string): boolean {
|
|
256
|
+
return peerId !== this.selfId && !this.deps.excluded.has(peerId) && this.deps.isRevoked?.(peerId) !== true;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Whether an open connection reaches a peer FRET has seen speak this strand's protocols, or a seed peer. */
|
|
260
|
+
private memberConnected(membership: ReadonlyMap<string, FretMembership>): boolean {
|
|
261
|
+
return this.deps.node.getConnections().some((connection) => {
|
|
262
|
+
const peerId = connection.remotePeer.toString();
|
|
263
|
+
return this.eligible(peerId) && (membership.get(peerId) === 'member' || this.seed.has(peerId));
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** The members to dial, each with its candidate addresses: verified FRET records first, then the seed's. */
|
|
268
|
+
private async knownMembers(membership: ReadonlyMap<string, FretMembership>): Promise<Map<string, Multiaddr[]>> {
|
|
269
|
+
const { peers } = await strandFretPeerAddrs(this.deps.node);
|
|
270
|
+
const known = new Map<string, Multiaddr[]>();
|
|
271
|
+
for (const [peerId, addrs] of [...peers, ...this.seed]) {
|
|
272
|
+
if (!this.eligible(peerId) || membership.get(peerId) === 'foreign') {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
known.set(peerId, unionAddrs(known.get(peerId) ?? [], addrs));
|
|
276
|
+
}
|
|
277
|
+
return known;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Dial every member in `known` at once; resolves to whether any connected. */
|
|
281
|
+
private async dialAny(known: ReadonlyMap<string, Multiaddr[]>): Promise<boolean> {
|
|
282
|
+
const members = [...known];
|
|
283
|
+
const outcomes = await Promise.allSettled(members.map(([peerId, addrs]) =>
|
|
284
|
+
dialPeerAddrs(this.deps.node, peerId, addrs, this.timings.budget, `strand ${this.deps.label} redial of ${peerId}`)
|
|
285
|
+
));
|
|
286
|
+
outcomes.forEach((outcome, i) => {
|
|
287
|
+
if (outcome.status === 'rejected') {
|
|
288
|
+
const reason = outcome.reason instanceof SelfRelayOnlyError ? 'reachable only by relaying through this node' : outcome.reason;
|
|
289
|
+
log('strand %s: redial of %s failed: %o', this.deps.label, members[i][0], reason);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
return outcomes.some((outcome) => outcome.status === 'fulfilled');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private cancelTimer(): void {
|
|
296
|
+
if (this.timer !== null) {
|
|
297
|
+
this.scheduler.clearTimeout(this.timer);
|
|
298
|
+
this.timer = null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** `primary` then the entries of `extra` it lacks, compared as strings. */
|
|
304
|
+
function unionAddrs(primary: readonly Multiaddr[], extra: readonly Multiaddr[]): Multiaddr[] {
|
|
305
|
+
const seen = new Set(primary.map((addr) => addr.toString()));
|
|
306
|
+
return [...primary, ...extra.filter((addr) => !seen.has(addr.toString()))];
|
|
307
|
+
}
|
|
@@ -30,7 +30,7 @@ import type { Multiaddr } from '@multiformats/multiaddr';
|
|
|
30
30
|
import type { StrandInstance, WakeRequest, WakeAck } from './types.js';
|
|
31
31
|
import { decodeLengthPrefixedFrame } from './seed-bootstrap.js';
|
|
32
32
|
import { type ControlStream, exchangeFrame, readStreamToEnd, replyAndClose } from './control-stream.js';
|
|
33
|
-
import { tryAddrsInTurn } from './peer-dial.js';
|
|
33
|
+
import { dialAddr, tryAddrsInTurn } from './peer-dial.js';
|
|
34
34
|
import { relayedRequestBudgetMs } from './link-budget.js';
|
|
35
35
|
|
|
36
36
|
const log = debug('sereus:cadre:strand-wake');
|
|
@@ -356,12 +356,14 @@ export async function dialWake(
|
|
|
356
356
|
}
|
|
357
357
|
|
|
358
358
|
/**
|
|
359
|
-
*
|
|
359
|
+
* Dial one address, open one stream, send the request, half-close, and read the ack.
|
|
360
360
|
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
361
|
+
* The address is dialed outside libp2p's dial queue (`dialAddr`), so another dial of
|
|
362
|
+
* the target in flight cannot capture this attempt; an open connection is still reused.
|
|
363
|
+
*
|
|
364
|
+
* `signal` is the per-attempt deadline from {@link dialWake}: it goes to the dial so a
|
|
365
|
+
* timeout during connect aborts it, and into {@link exchangeFrame} so a timeout after
|
|
366
|
+
* the stream is open resets it — releasing the otherwise unbounded ack-read.
|
|
365
367
|
*/
|
|
366
368
|
async function sendWake(
|
|
367
369
|
node: Libp2p,
|
|
@@ -371,12 +373,13 @@ async function sendWake(
|
|
|
371
373
|
timeoutMs: number,
|
|
372
374
|
signal: AbortSignal,
|
|
373
375
|
): Promise<WakeAck> {
|
|
376
|
+
const connection = await dialAddr(node, addr, signal, 'Wake dial');
|
|
374
377
|
// `runOnLimitedConnection: true`: the target may be reachable only over a
|
|
375
378
|
// circuit-relay connection (the signaling-first addr), which libp2p marks
|
|
376
379
|
// "limited". The wake exchange is a single tiny request→ack well within the
|
|
377
380
|
// relay's data/duration cap, so opening it on the limited connection is safe
|
|
378
381
|
// and is the whole point of dialing the relay address.
|
|
379
|
-
const rawStream = await
|
|
382
|
+
const rawStream = await connection.newStream(protocolId, { runOnLimitedConnection: true, signal });
|
|
380
383
|
|
|
381
384
|
const ack = await exchangeFrame(
|
|
382
385
|
rawStream as unknown as ControlStream,
|
package/src/types.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { StrandRevocationEnforcementConfig } from './strand-revocation-enfo
|
|
|
8
8
|
import type { StrandMembershipReconciliationConfig } from './strand-membership-reconciler.js';
|
|
9
9
|
import type { StrandFirstSyncConfig } from './strand-first-sync-gate.js';
|
|
10
10
|
import type { StrandDatabase } from './strand-database.js';
|
|
11
|
+
import type { StrandCommitDurability, StrandCommitHeld } from './strand-commit-durability.js';
|
|
11
12
|
import type { SeedTrustPolicy } from './seed-trust-policy.js';
|
|
12
13
|
import type { KeyStore, KeyId } from './key-store.js';
|
|
13
14
|
import type { TrustedOwnerStore, TrustSource } from './trusted-owner-store.js';
|
|
@@ -17,9 +18,10 @@ import type { EnrolledMachineStore } from './enrolled-machine-store.js';
|
|
|
17
18
|
import type { JoinedStrandStore } from './joined-strand-store.js';
|
|
18
19
|
import type { PushNotifier } from './push-notifier.js';
|
|
19
20
|
import type { CadreInviteSignedFields, RevocableTable } from './control-authorization.js';
|
|
20
|
-
import type { CadreInvitation } from './cadre-invite-protocol.js';
|
|
21
|
+
import type { CadreInvitation, CadreInviteAddressOutcome } from './cadre-invite-protocol.js';
|
|
21
22
|
import type { ControlRetryAbandonment } from './control-retry.js';
|
|
22
23
|
import type { FormationRejectionCode } from './strand-formation-rejection.js';
|
|
24
|
+
import type { ListenTransportOptions } from './relay-addrs.js';
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* Extended Libp2p node with the coordinatedRepo attached by db-p2p's
|
|
@@ -401,6 +403,20 @@ export interface NetworkConfig {
|
|
|
401
403
|
* ```
|
|
402
404
|
*/
|
|
403
405
|
transports?: Libp2pTransports;
|
|
406
|
+
/**
|
|
407
|
+
* Builds `@optimystic/db-p2p`'s default transport list, for the one case where cadre has to
|
|
408
|
+
* pass a list itself while {@link transports} is unset: a node with configured relays, which
|
|
409
|
+
* is built with libp2p's relay discovery switched off (`node-transports.ts`). Called with the
|
|
410
|
+
* switches the node's listen entries imply (`wsPort` when one names WebSocket). Ignored when
|
|
411
|
+
* {@link transports} is set.
|
|
412
|
+
*
|
|
413
|
+
* A Node host passes `nodeDefaultTransports` from the Node-only
|
|
414
|
+
* `@serfab/cadre-core/node-default-transports` subpath, as `cadre-cli` does. It is injected
|
|
415
|
+
* rather than built in, because that list names `@libp2p/tcp`, which must stay out of the
|
|
416
|
+
* graph React Native and browser bundles reach. Left unset, such a node is built from
|
|
417
|
+
* db-p2p's own list with relay discovery on.
|
|
418
|
+
*/
|
|
419
|
+
defaultTransports?: (listen: ListenTransportOptions) => Libp2pTransports;
|
|
404
420
|
/**
|
|
405
421
|
* Crypto primitives for the Noise handshake and the encrypted connection that
|
|
406
422
|
* follows, threaded to both the control node and every strand's cohort node — every
|
|
@@ -625,6 +641,16 @@ export interface NetworkConfig {
|
|
|
625
641
|
* kept it (the same pair aborted at a 200ms stall only when the deadline was 300ms). An
|
|
626
642
|
* interval strictly above the deadline is what makes the 30 seconds real.
|
|
627
643
|
*
|
|
644
|
+
* WHY THE OTHER END'S PING HANDLER CAPS IT TOO. The node being pinged echoes through
|
|
645
|
+
* `@libp2p/ping`'s inbound handler, which aborts the ping stream when its own timer fires; the
|
|
646
|
+
* pinging node's monitor sees the reset and aborts the connection exactly as it would on its
|
|
647
|
+
* own timeout. So this deadline is only real up to the handler timeout of the node being
|
|
648
|
+
* pinged, which has to stay above it. The deployed relay sets its handler to 60 s for that
|
|
649
|
+
* reason (`ops/docker/libp2p-infra/src/main.ts`). Every cadre node's handler is Optimystic's
|
|
650
|
+
* bare `ping()`, `@libp2p/ping`'s 10 s, and nothing here can move it
|
|
651
|
+
* (`tickets/blocked/optimystic-ping-handler-timeout-below-the-monitor-deadline.md`): between
|
|
652
|
+
* two cadre nodes, a ping whose echo takes longer than 10 s still drops the connection.
|
|
653
|
+
*
|
|
628
654
|
* WHAT THE LINK NEEDS OF IT. One ping opens a fresh stream and echoes over it: a protocol
|
|
629
655
|
* negotiation plus the echo, two link round trips by `link-budget.ts`'s counts (a `newStream`
|
|
630
656
|
* over a relayed circuit measured 3016 ms at 1500 ms one-way). At the slowest link sereus
|
|
@@ -1642,6 +1668,24 @@ export interface CadreNodeEvents {
|
|
|
1642
1668
|
* watcher auto-launched) hangs its "waiting for the other member" screen on the pair.
|
|
1643
1669
|
*/
|
|
1644
1670
|
'strand:writable': { strandId: string };
|
|
1671
|
+
/**
|
|
1672
|
+
* Emitted after every commit on a strand's database that wrote something, with whether
|
|
1673
|
+
* another member already holds it (`heldElsewhere`) and whether `strand:commit-held` will
|
|
1674
|
+
* say when it does (`followed`). Synchronous, inside the commit, before the app's statement
|
|
1675
|
+
* returns: keep handlers short. Covers every commit on the strand database, cadre-core's own
|
|
1676
|
+
* membership writes included, so filter by the ids you hold — the `StampId()` a
|
|
1677
|
+
* `returning StampId() as Tx` statement gave you. The same record is readable afterwards with
|
|
1678
|
+
* `StrandDatabase.commitDurability(tx)`. See docs/strands.md, "A commit's durability".
|
|
1679
|
+
*/
|
|
1680
|
+
'strand:commit-durability': StrandCommitDurability;
|
|
1681
|
+
/**
|
|
1682
|
+
* Emitted once when a commit reported `followed` below full replication becomes held by every
|
|
1683
|
+
* member of its cohort — the moment a write made while the others were away reaches them.
|
|
1684
|
+
* Synchronous, like `strand:commit-durability`, and can come right after it inside the same
|
|
1685
|
+
* commit. Never emitted for a commit made before your listener attached, or one forgotten
|
|
1686
|
+
* because its database closed, hibernated or the node restarted.
|
|
1687
|
+
*/
|
|
1688
|
+
'strand:commit-held': StrandCommitHeld;
|
|
1645
1689
|
/**
|
|
1646
1690
|
* Emitted when this node discovers it is no longer a member of a CLOSED
|
|
1647
1691
|
* strand — its party was removed by a manager, or it left. Detected by the
|
|
@@ -1776,6 +1820,18 @@ export interface CadreNodeEvents {
|
|
|
1776
1820
|
* matching predicate for a reader that subscribed too late to see the event.
|
|
1777
1821
|
*/
|
|
1778
1822
|
'claim:accepted': NodeClaimRecord;
|
|
1823
|
+
/**
|
|
1824
|
+
* Emitted once per node object when this node learns that an owner removed it from the
|
|
1825
|
+
* cadre: a `CadrePeer` tombstone names its peer id, it has no live `CadrePeer` row, and a
|
|
1826
|
+
* membership refresh of this process had seen its live row before. `CadreNode.removedFromCadre`
|
|
1827
|
+
* is the matching query for a reader that subscribed too late.
|
|
1828
|
+
*
|
|
1829
|
+
* BEST-EFFORT: it fires only if the tombstone reached this node before the other machines
|
|
1830
|
+
* cut it off, so a machine that was offline at removal time usually never sees it. Nothing
|
|
1831
|
+
* is stopped or deleted for you; the node only stops its owner re-issue drains, which can no
|
|
1832
|
+
* longer succeed. Stopping the node and deleting the cadre's data are the embedder's.
|
|
1833
|
+
*/
|
|
1834
|
+
'cadre:removed': CadreRemoval;
|
|
1779
1835
|
}
|
|
1780
1836
|
|
|
1781
1837
|
// ============================================================================
|
|
@@ -1791,8 +1847,8 @@ export interface CadreNodeEvents {
|
|
|
1791
1847
|
* PeerId by reading the record, re-verifying the self-signature against
|
|
1792
1848
|
* `publicKey`, checking freshness, and applying a trust gate.
|
|
1793
1849
|
*
|
|
1794
|
-
* The signature covers `peerId`, the comma-joined `addrs`, and `
|
|
1795
|
-
* `peerRecordSignedPayload`. `publicKey` is NOT inside the signed payload: it is
|
|
1850
|
+
* The signature covers `peerId`, the comma-joined `addrs`, `updatedAt` and `profile` —
|
|
1851
|
+
* see `peerRecordSignedPayload`. `publicKey` is NOT inside the signed payload: it is
|
|
1796
1852
|
* the key the signature is verified *with*, and its libp2p-identity binding to
|
|
1797
1853
|
* `peerId` is checked separately at resolve time, so re-signing it would be
|
|
1798
1854
|
* redundant.
|
|
@@ -1806,6 +1862,12 @@ export interface PeerAddressRecord {
|
|
|
1806
1862
|
addrs: string[];
|
|
1807
1863
|
/** epoch ms — freshness; strictly increasing per peer. */
|
|
1808
1864
|
updatedAt: number;
|
|
1865
|
+
/**
|
|
1866
|
+
* The publisher's own {@link CadreNodeConfig.profile}: `'transaction'` is an app (phone,
|
|
1867
|
+
* browser), `'storage'` an always-on node. Null on a row the machine has not yet
|
|
1868
|
+
* self-published (an owner or an invitation seated it).
|
|
1869
|
+
*/
|
|
1870
|
+
profile: NodeProfile | null;
|
|
1809
1871
|
/** ed25519 self-signature over the signed payload, base64url. */
|
|
1810
1872
|
sig: string;
|
|
1811
1873
|
}
|
|
@@ -1828,8 +1890,16 @@ export interface PeerAddressRecord {
|
|
|
1828
1890
|
export interface CadrePeerRow {
|
|
1829
1891
|
/** libp2p peer ID (base58btc) — the row key. */
|
|
1830
1892
|
peerId: string;
|
|
1893
|
+
/** ed25519 public key (base64url) stored for the peer, or null (not ed25519, or a row written before the column). */
|
|
1894
|
+
publicKey: string | null;
|
|
1831
1895
|
/** Comma-joined dialable multiaddrs as stored, or null when unpublished. */
|
|
1832
1896
|
multiaddr: string | null;
|
|
1897
|
+
/** The machine's freshness stamp (epoch ms); 0 when it has never self-published. */
|
|
1898
|
+
updatedAt: number;
|
|
1899
|
+
/** The machine's self-declared node profile; null until it self-publishes. */
|
|
1900
|
+
profile: NodeProfile | null;
|
|
1901
|
+
/** The machine's self-signature over its address record (`peerRecordSignedPayload`); null until it self-publishes. */
|
|
1902
|
+
sig: string | null;
|
|
1833
1903
|
/** Single-use anti-replay nonce the voucher signature, or the usage row, is bound to. */
|
|
1834
1904
|
stampId: string | null;
|
|
1835
1905
|
/** ed25519 public key (base64url) of the owner that vouched this row, or that issued the invitation that admitted it. */
|
|
@@ -1919,6 +1989,107 @@ export interface InvitationChain {
|
|
|
1919
1989
|
invites: Map<string, CadreInviteRow>;
|
|
1920
1990
|
}
|
|
1921
1991
|
|
|
1992
|
+
/**
|
|
1993
|
+
* One machine of the cadre as `CadreNode.listMachines` reports it: a live `CadrePeer` row,
|
|
1994
|
+
* or a live `OwnerKey` whose key derives a peer id with no `CadrePeer` row (an owner key
|
|
1995
|
+
* seated for a machine not yet admitted, or a founder that never self-published).
|
|
1996
|
+
*
|
|
1997
|
+
* `profile` and `lastPublishedAt` are the machine's own claim only when the row's
|
|
1998
|
+
* self-signature verifies; both are null otherwise, so an owner re-vouch that bumped
|
|
1999
|
+
* `UpdatedAt` without re-signing, or a row the machine never published, reports nothing
|
|
2000
|
+
* rather than someone else's stamp.
|
|
2001
|
+
*/
|
|
2002
|
+
export interface CadreMachine {
|
|
2003
|
+
peerId: string;
|
|
2004
|
+
/** ed25519 public key (base64url) behind `peerId`; null when the id is not ed25519. */
|
|
2005
|
+
publicKey: string | null;
|
|
2006
|
+
/** This node itself. */
|
|
2007
|
+
isSelf: boolean;
|
|
2008
|
+
/** `publicKey` is a live `OwnerKey` here. */
|
|
2009
|
+
isOwner: boolean;
|
|
2010
|
+
/** Has a live `CadrePeer` row; false for an owner key with no machine row. */
|
|
2011
|
+
isMember: boolean;
|
|
2012
|
+
/** Passes this node's authorized-membership predicate now (`CadreNode.listAuthorizedMembers`; always false for self, which that set excludes). */
|
|
2013
|
+
trusted: boolean;
|
|
2014
|
+
/** The machine's self-published node profile; null until it publishes, or when its signature does not verify. */
|
|
2015
|
+
profile: NodeProfile | null;
|
|
2016
|
+
/** `UpdatedAt` of its self-signed record, re-stamped while it runs; null on the same terms as `profile`. */
|
|
2017
|
+
lastPublishedAt: number | null;
|
|
2018
|
+
/** `CadrePeer.VouchOwner` (or `OwnerKey.VouchOwner` for an owner-only entry): the owner that vouched it, or the issuer of the invitation that admitted it. */
|
|
2019
|
+
vouchedBy: string | null;
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
/** Why `CadreNode.removeMachine` refuses by name (`MachineRemovalRefusedError.code`). */
|
|
2023
|
+
export type MachineRemovalRefusal =
|
|
2024
|
+
/** This node holds no enrolled owner key, so it cannot sign the removal. */
|
|
2025
|
+
| 'not-an-owner'
|
|
2026
|
+
/** The machine is this node: an owner cannot sign its own `OwnerKey` removal, so ask another owner. */
|
|
2027
|
+
| 'self'
|
|
2028
|
+
/** The machine holds the only live owner key here (`MinOneOwner`). */
|
|
2029
|
+
| 'last-owner';
|
|
2030
|
+
|
|
2031
|
+
/**
|
|
2032
|
+
* What `CadreNode.removeMachine` would do for one machine, read without writing
|
|
2033
|
+
* (`CadreNode.planMachineRemoval`), for an app's confirmation step.
|
|
2034
|
+
*/
|
|
2035
|
+
export interface MachineRemovalPlan {
|
|
2036
|
+
/** The machine as listed, or null when no live row names it here (already removed). */
|
|
2037
|
+
machine: CadreMachine | null;
|
|
2038
|
+
refusal: MachineRemovalRefusal | null;
|
|
2039
|
+
/** The machine's key is a live owner key, so the removal also retires it. */
|
|
2040
|
+
removesOwnerKey: boolean;
|
|
2041
|
+
/** Rows the machine's owner key vouched, which stop passing the membership predicate everywhere once it is removed unless re-vouched first. Empty unless `removesOwnerKey`. */
|
|
2042
|
+
dependents: {
|
|
2043
|
+
/** Live `OwnerKey` rows whose `VouchOwner` is the machine's key, this node's own excluded. */
|
|
2044
|
+
owners: string[];
|
|
2045
|
+
/** Live `CadrePeer` rows whose `VouchOwner` is the machine's key, the machine itself and this node excluded. */
|
|
2046
|
+
peers: string[];
|
|
2047
|
+
};
|
|
2048
|
+
/** Live `CadreInvite` keys the machine could redeem to come back: targeted at it, or already redeemed by it with uses left. */
|
|
2049
|
+
invitations: string[];
|
|
2050
|
+
/** This node's own `OwnerKey` or `CadrePeer` row was vouched by the machine. It cannot re-vouch its own rows; another owner has to re-add it afterwards. */
|
|
2051
|
+
ownDependency: boolean;
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
/** Options for `CadreNode.removeMachine`. */
|
|
2055
|
+
export interface RemoveMachineOptions {
|
|
2056
|
+
/** Dependents to re-vouch under this node's key before the removal; each must appear in the plan's `dependents`. */
|
|
2057
|
+
revouch?: {
|
|
2058
|
+
owners?: readonly string[];
|
|
2059
|
+
peers?: readonly string[];
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
/** What `CadreNode.removeMachine` wrote. All false and empty when no live row named the machine here. */
|
|
2064
|
+
export interface MachineRemovalResult {
|
|
2065
|
+
removedPeer: boolean;
|
|
2066
|
+
removedOwnerKey: boolean;
|
|
2067
|
+
removedDeviceToken: boolean;
|
|
2068
|
+
/** `CadreInvite` keys whose withdrawal tombstone this call filed. */
|
|
2069
|
+
withdrawnInvitations: string[];
|
|
2070
|
+
/** Dependents re-vouched under this node's key before the removal. */
|
|
2071
|
+
revouched: {
|
|
2072
|
+
owners: string[];
|
|
2073
|
+
peers: string[];
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
/**
|
|
2078
|
+
* This node's own removal from its cadre, as it learned it: the `CadrePeer` tombstone naming
|
|
2079
|
+
* its peer id, read after it had seen its own live row (`CadreNode.removedFromCadre`, the
|
|
2080
|
+
* `cadre:removed` event; "Removing a machine" in docs/architecture.md).
|
|
2081
|
+
*/
|
|
2082
|
+
export interface CadreRemoval {
|
|
2083
|
+
/** The cadre this node was removed from (`CadreNode.partyId`). */
|
|
2084
|
+
partyId: string;
|
|
2085
|
+
/** The `StampId` the tombstone retired. */
|
|
2086
|
+
stampId: string;
|
|
2087
|
+
/** The owner key that filed the tombstone, when its signature verifies and the key is in this node's trusted-owner anchor; null otherwise. */
|
|
2088
|
+
signerKey: string | null;
|
|
2089
|
+
/** When this node learned of the removal (epoch ms). */
|
|
2090
|
+
at: number;
|
|
2091
|
+
}
|
|
2092
|
+
|
|
1922
2093
|
/**
|
|
1923
2094
|
* One {@link CadreInviteRow} with its standing, as `ControlDatabase.listCadreInviteStatuses`
|
|
1924
2095
|
* (and `CadreNode.listCadreInvitations`) reports it. `live` is the redemption's own test:
|
|
@@ -2433,13 +2604,58 @@ export interface CreateCadreInvitationOptions {
|
|
|
2433
2604
|
uses?: number;
|
|
2434
2605
|
}
|
|
2435
2606
|
|
|
2607
|
+
/**
|
|
2608
|
+
* Where a phone could redeem a cadre invitation from, judged on the addresses of machines
|
|
2609
|
+
* other than the one that minted it — the answer to "can the new app join once this one is
|
|
2610
|
+
* closed?". A minting machine that is itself always on can therefore read `issuer-only`.
|
|
2611
|
+
*/
|
|
2612
|
+
export type CadreInvitationReach =
|
|
2613
|
+
/** Another machine has a relayed (`/p2p-circuit`), public-name or public-IP WebSocket address. */
|
|
2614
|
+
| 'anywhere'
|
|
2615
|
+
/** Other machines are named, but only at private or loopback addresses: works on their network only. */
|
|
2616
|
+
| 'local-network'
|
|
2617
|
+
/** The bundle names no other machine at an address a phone can dial. */
|
|
2618
|
+
| 'issuer-only';
|
|
2619
|
+
|
|
2436
2620
|
/** What `CadreNode.createCadreInvitation` returns: the bundle, and the same encoded for sharing. */
|
|
2437
2621
|
export interface CreateCadreInvitationResult {
|
|
2438
2622
|
invitation: CadreInvitation;
|
|
2439
|
-
/** `encodeCadreInvitation(invitation)`: base64url JSON for a QR code, a paste or a link. */
|
|
2623
|
+
/** `encodeCadreInvitation(invitation)`: `sereus-invite:1.` and base64url JSON, for a QR code, a paste or a link. */
|
|
2440
2624
|
encoded: string;
|
|
2625
|
+
/** Whether a device could redeem it once the minting machine is gone; see {@link CadreInvitationReach}. */
|
|
2626
|
+
reach: CadreInvitationReach;
|
|
2441
2627
|
}
|
|
2442
2628
|
|
|
2629
|
+
/** How `CadreNode.redeemCadreInvitation` runs. */
|
|
2630
|
+
export interface RedeemCadreInvitationOptions {
|
|
2631
|
+
/**
|
|
2632
|
+
* Keep making passes over the bundle's addresses while every answer is retryable, until a
|
|
2633
|
+
* member accepts, one refuses finally, the invitation expires, or `signal` aborts.
|
|
2634
|
+
* Default false: one pass.
|
|
2635
|
+
*/
|
|
2636
|
+
waitForMember?: boolean;
|
|
2637
|
+
/** Ends the pause and the pass in flight; the call then throws the signal's reason. */
|
|
2638
|
+
signal?: AbortSignal;
|
|
2639
|
+
/** Called at each pass and each pause; an exception it throws is logged and ignored. */
|
|
2640
|
+
onProgress?: (progress: CadreJoinProgress) => void;
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
/** A redemption's progress, as `RedeemCadreInvitationOptions.onProgress` reports it. */
|
|
2644
|
+
export type CadreJoinProgress =
|
|
2645
|
+
| { state: 'redeeming'; pass: number }
|
|
2646
|
+
| {
|
|
2647
|
+
state: 'waiting';
|
|
2648
|
+
pass: number;
|
|
2649
|
+
/** Epoch ms of the next pass. */
|
|
2650
|
+
retryAtMs: number;
|
|
2651
|
+
/**
|
|
2652
|
+
* `'no-member-answered'`: every address failed to dial or answer. `'member-not-ready'`: at
|
|
2653
|
+
* least one member answered with a retryable refusal (issuer-unknown, busy, conflict, internal).
|
|
2654
|
+
*/
|
|
2655
|
+
reason: 'no-member-answered' | 'member-not-ready';
|
|
2656
|
+
outcomes: readonly CadreInviteAddressOutcome[];
|
|
2657
|
+
};
|
|
2658
|
+
|
|
2443
2659
|
/** What `CadreNode.redeemCadreInvitation` returns once a member accepted. */
|
|
2444
2660
|
export interface RedeemCadreInvitationResult {
|
|
2445
2661
|
/** The member machine that admitted this device, when its address named one. */
|