@serfab/cadre-core 0.9.0 → 0.10.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 +4 -2
- 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 +918 -53
- package/dist/cadre-node.js +1843 -190
- 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-database.d.ts +990 -50
- package/dist/control-database.js +1459 -108
- package/dist/control-database.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +89 -10
- package/dist/control-formation-recorder.js +129 -14
- package/dist/control-formation-recorder.js.map +1 -1
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +774 -210
- 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 +151 -0
- package/dist/control-write-retry.js +411 -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 +16 -6
- package/dist/index.js +39 -5
- 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-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 +221 -56
- package/dist/seed-bootstrap.js +328 -188
- 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-formation-manager.d.ts +38 -9
- package/dist/strand-formation-manager.js +120 -17
- 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 +39 -1
- package/dist/strand-instance-manager.js +92 -14
- 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 +482 -88
- package/dist/strand-membership-writer.js +757 -136
- 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 +275 -20
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -1
- package/package.json +19 -8
- package/src/bootstrap-peer-store-file.ts +44 -0
- package/src/bootstrap-peer-store.ts +172 -0
- package/src/cadre-node.ts +1992 -202
- package/src/control-authorization.ts +114 -0
- package/src/control-database.ts +1704 -146
- package/src/control-formation-recorder.ts +172 -19
- package/src/control-schema.ts +786 -222
- package/src/control-stream.ts +81 -4
- package/src/control-write-retry.ts +473 -0
- package/src/delegate-admission.ts +302 -0
- package/src/device-token.ts +18 -13
- package/src/ed25519-key.ts +134 -69
- package/src/file-durable-slot.ts +61 -0
- package/src/formation-approval.ts +604 -0
- package/src/fs-atomic.ts +107 -0
- package/src/identity-key.ts +109 -0
- package/src/index.ts +136 -5
- package/src/key-store-file.ts +10 -81
- package/src/membership-connection-gater.ts +204 -0
- package/src/node-local-snapshot.ts +260 -0
- package/src/peer-authorization.ts +199 -21
- package/src/peer-record.ts +28 -10
- package/src/relay-addrs.ts +111 -0
- package/src/relay-reservation.ts +823 -0
- package/src/seed-bootstrap.ts +397 -197
- package/src/seed-trust-policy.ts +58 -25
- package/src/strand-addr-protocol.ts +84 -53
- package/src/strand-backfill.ts +366 -0
- package/src/strand-formation-manager.ts +145 -19
- package/src/strand-formation-protocol.ts +309 -8
- package/src/strand-instance-manager.ts +111 -14
- package/src/strand-member-registry.ts +18 -13
- package/src/strand-membership-writer.ts +1430 -640
- package/src/strand-solicitation.ts +155 -90
- package/src/strand-transport-key.ts +58 -0
- package/src/strand-wake-protocol.ts +29 -46
- package/src/strand-watcher.ts +66 -3
- package/src/trusted-owner-store-file.ts +41 -0
- package/src/trusted-owner-store.ts +171 -0
- package/src/types.ts +292 -21
package/src/seed-trust-policy.ts
CHANGED
|
@@ -8,25 +8,38 @@
|
|
|
8
8
|
* The trust decision must therefore rest on an anchor that does not come from
|
|
9
9
|
* the seed body:
|
|
10
10
|
*
|
|
11
|
-
* 1.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* 1. Anchored — keys in the receiver's NODE-LOCAL `TrustedOwnerStore`: the
|
|
12
|
+
* non-replicated, per-party record of owner keys established out of band
|
|
13
|
+
* (founding the party, the `CadreInvite` that enrolled this node, an
|
|
14
|
+
* operator pin). Deliberately NOT the replicated `CadreControl.OwnerKey`
|
|
15
|
+
* table — any connecting node can genesis-insert its own key there and let
|
|
16
|
+
* it replicate, so that table can be made to say "yes" by a stranger.
|
|
17
|
+
* 2. Pinned out-of-band — keys handed to the node for this seed only (carried
|
|
18
|
+
* by a `CadreInvite.ownerKeys`, or pinned by an operator) without having
|
|
19
|
+
* been anchored yet.
|
|
15
20
|
* 3. TOFU (opt-in) — an explicit confirmation callback invoked on first sight
|
|
16
21
|
* of an unknown signer key. Interactive hosts only; off by default.
|
|
17
22
|
*
|
|
18
|
-
* Secure default (`
|
|
19
|
-
*
|
|
20
|
-
*
|
|
23
|
+
* Secure default (`anchoredTrustPolicy`): a node with an empty anchor and no
|
|
24
|
+
* pinned keys rejects the seed — a seed can no longer vouch for its own signer,
|
|
25
|
+
* and neither can a polluted replicated table.
|
|
26
|
+
*
|
|
27
|
+
* A key accepted via anchor 2 or 3 is reported back through
|
|
28
|
+
* {@link SeedTrustDecision.anchorAs} so `applySeed` can persist it into the
|
|
29
|
+
* node-local anchor, and later seeds from the same owner are accepted without
|
|
30
|
+
* re-supplying the invite/confirmation.
|
|
21
31
|
*/
|
|
32
|
+
import type { TrustSource } from './trusted-owner-store.js';
|
|
33
|
+
|
|
22
34
|
export interface SeedTrustContext {
|
|
23
35
|
/** Party the seed claims to belong to. */
|
|
24
36
|
partyId: string;
|
|
25
37
|
/** ed25519 base64url signer key — already signature-verified by `applySeed`. */
|
|
26
38
|
signerKey: string;
|
|
27
39
|
/**
|
|
28
|
-
* The receiver's
|
|
29
|
-
*
|
|
40
|
+
* The receiver's anchored owner keys, sourced from its node-local
|
|
41
|
+
* `TrustedOwnerStore` — NOT from the seed, and NOT from the replicated
|
|
42
|
+
* `OwnerKey` table. Empty for a node whose anchor was never seeded.
|
|
30
43
|
*/
|
|
31
44
|
knownOwnerKeys: ReadonlySet<string>;
|
|
32
45
|
}
|
|
@@ -36,21 +49,30 @@ export interface SeedTrustDecision {
|
|
|
36
49
|
trusted: boolean;
|
|
37
50
|
/** Human-readable reason, surfaced as the seed-apply error when not trusted. */
|
|
38
51
|
reason?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Set when the key was trusted via an anchor OUTSIDE the node-local store (a
|
|
54
|
+
* pinned key, a TOFU confirmation): the provenance to record it under, so
|
|
55
|
+
* `applySeed` persists it and the next seed from that owner is anchored
|
|
56
|
+
* without re-supplying the pin. Omitted when the key was already anchored
|
|
57
|
+
* (nothing to persist) or not trusted at all.
|
|
58
|
+
*/
|
|
59
|
+
anchorAs?: Exclude<TrustSource, 'genesis'>;
|
|
39
60
|
}
|
|
40
61
|
|
|
41
62
|
/**
|
|
42
63
|
* Decides whether a signature-verified seed's signer key should be trusted.
|
|
43
|
-
* May be synchronous (
|
|
64
|
+
* May be synchronous (anchored/pinned) or asynchronous (TOFU confirmation).
|
|
44
65
|
*/
|
|
45
66
|
export interface SeedTrustPolicy {
|
|
46
67
|
evaluate(ctx: SeedTrustContext): Promise<SeedTrustDecision> | SeedTrustDecision;
|
|
47
68
|
}
|
|
48
69
|
|
|
49
70
|
/**
|
|
50
|
-
* Default policy: trust only keys already
|
|
51
|
-
*
|
|
71
|
+
* Default policy: trust only keys already in the receiver's node-local
|
|
72
|
+
* trusted-owner anchor. A node whose anchor was never seeded (no genesis, no
|
|
73
|
+
* invite pin, no operator pin) rejects every seed.
|
|
52
74
|
*/
|
|
53
|
-
export function
|
|
75
|
+
export function anchoredTrustPolicy(): SeedTrustPolicy {
|
|
54
76
|
return {
|
|
55
77
|
evaluate({ signerKey, knownOwnerKeys }) {
|
|
56
78
|
if (knownOwnerKeys.has(signerKey)) {
|
|
@@ -58,37 +80,48 @@ export function dbAnchoredTrustPolicy(): SeedTrustPolicy {
|
|
|
58
80
|
}
|
|
59
81
|
return {
|
|
60
82
|
trusted: false,
|
|
61
|
-
reason: 'Signer key is not
|
|
83
|
+
reason: 'Signer key is not an anchored owner (anchored trust policy)',
|
|
62
84
|
};
|
|
63
85
|
},
|
|
64
86
|
};
|
|
65
87
|
}
|
|
66
88
|
|
|
67
89
|
/**
|
|
68
|
-
* Cold-start policy: trust keys
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
90
|
+
* Cold-start policy: trust anchored keys plus a set pinned out-of-band
|
|
91
|
+
* (typically `CadreInvite.ownerKeys` or operator config). Lets an unenrolled
|
|
92
|
+
* invitee accept its first seed without the seed vouching for itself.
|
|
93
|
+
*
|
|
94
|
+
* @param anchorAs - provenance under which a pin-only acceptance is persisted
|
|
95
|
+
* into the node-local anchor ('invite' by default — the invite-redemption
|
|
96
|
+
* case; pass 'operator' for an operator-supplied pin).
|
|
72
97
|
*/
|
|
73
|
-
export function pinnedKeyTrustPolicy(
|
|
98
|
+
export function pinnedKeyTrustPolicy(
|
|
99
|
+
pinned: Iterable<string>,
|
|
100
|
+
anchorAs: Exclude<TrustSource, 'genesis'> = 'invite'
|
|
101
|
+
): SeedTrustPolicy {
|
|
74
102
|
const pinnedSet = new Set(pinned);
|
|
75
103
|
return {
|
|
76
104
|
evaluate({ signerKey, knownOwnerKeys }) {
|
|
77
|
-
if (knownOwnerKeys.has(signerKey)
|
|
105
|
+
if (knownOwnerKeys.has(signerKey)) {
|
|
78
106
|
return { trusted: true };
|
|
79
107
|
}
|
|
108
|
+
if (pinnedSet.has(signerKey)) {
|
|
109
|
+
return { trusted: true, anchorAs };
|
|
110
|
+
}
|
|
80
111
|
return {
|
|
81
112
|
trusted: false,
|
|
82
|
-
reason: 'Signer key is neither
|
|
113
|
+
reason: 'Signer key is neither an anchored nor a pinned owner (pinned-key trust policy)',
|
|
83
114
|
};
|
|
84
115
|
},
|
|
85
116
|
};
|
|
86
117
|
}
|
|
87
118
|
|
|
88
119
|
/**
|
|
89
|
-
* Opt-in interactive policy: trust keys already in the
|
|
90
|
-
*
|
|
91
|
-
*
|
|
120
|
+
* Opt-in interactive policy: trust keys already in the node-local anchor, and
|
|
121
|
+
* on an unknown key invoke `confirm` (e.g. a trust-circle UI prompt). The key is
|
|
122
|
+
* trusted iff `confirm` resolves true, and a confirmed key is persisted into the
|
|
123
|
+
* anchor as an 'operator' pin (a human at the console is the same provenance as
|
|
124
|
+
* an explicit operator pin) so the prompt is not repeated. Not enabled by default.
|
|
92
125
|
*/
|
|
93
126
|
export function tofuTrustPolicy(
|
|
94
127
|
confirm: (ctx: SeedTrustContext) => Promise<boolean>
|
|
@@ -100,7 +133,7 @@ export function tofuTrustPolicy(
|
|
|
100
133
|
}
|
|
101
134
|
const accepted = await confirm(ctx);
|
|
102
135
|
return accepted
|
|
103
|
-
? { trusted: true }
|
|
136
|
+
? { trusted: true, anchorAs: 'operator' }
|
|
104
137
|
: { trusted: false, reason: 'TOFU confirmation declined for unknown signer key' };
|
|
105
138
|
},
|
|
106
139
|
};
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
* Control-network strand-address RPC.
|
|
3
3
|
*
|
|
4
4
|
* A strand runs as its own libp2p node (`strand-<id>`, random port), separate
|
|
5
|
-
* from the control node (`control-<partyId>`)
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* from the control node (`control-<partyId>`), with its own transport peerId
|
|
6
|
+
* derived from the cadre identity key (`strand-transport-key.ts`). To seed a
|
|
7
|
+
* strand's mesh, a node needs a sibling's **strand-network** address — but
|
|
8
|
+
* `CadrePeer.Multiaddr` only stores the **control** node's addresses, and a
|
|
9
|
+
* control address now names a different peer entirely, not merely a different
|
|
10
|
+
* port. This protocol resolves the strand address on demand: a node asks its
|
|
11
|
+
* connected co-cadre siblings "what are your live strand-`X` multiaddrs?" and
|
|
10
12
|
* uses the union of their answers as the seed.
|
|
11
13
|
*
|
|
12
14
|
* Modeled directly on `strand-wake-protocol.ts`: a dedicated libp2p protocol id,
|
|
@@ -16,13 +18,14 @@
|
|
|
16
18
|
* one stream, so each side reads to EOF (under a read timeout) and decodes one
|
|
17
19
|
* frame via the shared {@link decodeLengthPrefixedFrame} guard.
|
|
18
20
|
*
|
|
19
|
-
* **Authorization (v1):**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
21
|
+
* **Authorization (v1):** like wake, the receiver defers entirely to the injected
|
|
22
|
+
* `isMember` predicate and requires no further signature; a peer it rejects gets
|
|
23
|
+
* an empty address list. `CadreNode` injects its AUTHORIZED-membership predicate
|
|
24
|
+
* (`isAuthorizedMember`: voucher on the requester's `CadrePeer` row verified
|
|
25
|
+
* against the node-local trusted-owner anchor), so an outsider that published its
|
|
26
|
+
* own rows into the replicated control DB cannot harvest live strand addresses.
|
|
27
|
+
* Cross-party strand bootstrap is a different mechanism (strand formation /
|
|
28
|
+
* `MemberPeer`) and is out of scope here.
|
|
26
29
|
*/
|
|
27
30
|
|
|
28
31
|
import debug from 'debug';
|
|
@@ -32,7 +35,7 @@ import { peerIdFromString } from '@libp2p/peer-id';
|
|
|
32
35
|
import type { StrandAddrRequest, StrandAddrResponse } from './types.js';
|
|
33
36
|
import { decodeLengthPrefixedFrame } from './seed-bootstrap.js';
|
|
34
37
|
import { orderSignalingFirst } from './peer-record.js';
|
|
35
|
-
import { type ControlStream, writeFrame,
|
|
38
|
+
import { type ControlStream, writeFrame, withDeadline, exchangeFrame, readStreamToEnd } from './control-stream.js';
|
|
36
39
|
|
|
37
40
|
const log = debug('sereus:cadre:strand-addr');
|
|
38
41
|
|
|
@@ -86,6 +89,16 @@ export interface StrandAddrServiceOptions {
|
|
|
86
89
|
* decision logic is unit-testable without a full node.
|
|
87
90
|
*/
|
|
88
91
|
getStrandMultiaddrs(strandId: string): string[];
|
|
92
|
+
/**
|
|
93
|
+
* Called when a member's request carries a `delegatePeerId` — the derived
|
|
94
|
+
* transport peerId its strand-`strandId` node runs as. Invoked only AFTER the
|
|
95
|
+
* `isMember` gate passed, with a validated (parseable, non-self) peerId.
|
|
96
|
+
* `CadreNode` injects its delegate-admission grant recorder so this node's
|
|
97
|
+
* connection gate — and thus its circuit-relay server, when it runs one —
|
|
98
|
+
* admits that peerId (see `delegate-admission.ts`). Optional: absent, an
|
|
99
|
+
* announce is ignored and the address lookup proceeds unchanged.
|
|
100
|
+
*/
|
|
101
|
+
onDelegateAnnounce?(announcerPeerId: string, strandId: string, delegatePeerId: string): void;
|
|
89
102
|
/**
|
|
90
103
|
* Time to wait for the inbound request frame before aborting the read (ms).
|
|
91
104
|
* Defaults to {@link DEFAULT_ADDR_READ_TIMEOUT_MS}. Bounds a buggy/compromised
|
|
@@ -209,22 +222,50 @@ export class StrandAddrService {
|
|
|
209
222
|
* decision matrix can be unit-tested directly (mirrors wake's
|
|
210
223
|
* `processWakeRequest`).
|
|
211
224
|
*
|
|
212
|
-
* - Non-member sender → empty `multiaddrs` (refused).
|
|
225
|
+
* - Non-member sender → empty `multiaddrs` (refused), no delegate grant.
|
|
226
|
+
* - Member request carrying a `delegatePeerId` → recorded via
|
|
227
|
+
* {@link StrandAddrServiceOptions.onDelegateAnnounce} before the lookup.
|
|
213
228
|
* - Strand not running locally → empty `multiaddrs` (`getStrandMultiaddrs` → `[]`).
|
|
214
229
|
* - Member + running strand → the strand's live, signaling-first multiaddrs.
|
|
215
230
|
*/
|
|
216
231
|
async processAddrRequest(request: StrandAddrRequest, remotePeerId: string): Promise<StrandAddrResponse> {
|
|
217
232
|
// Control-network membership is the v1 authorization: only this party's
|
|
218
|
-
// cadre peers may ask us for a strand address.
|
|
233
|
+
// cadre peers may ask us for a strand address (or announce a delegate).
|
|
219
234
|
if (!(await this.options.isMember(remotePeerId))) {
|
|
220
235
|
log('Refusing strand-addr from non-member %s', remotePeerId);
|
|
221
236
|
return emptyResponse(request.strandId);
|
|
222
237
|
}
|
|
223
238
|
|
|
239
|
+
this.recordDelegateAnnounce(request, remotePeerId);
|
|
240
|
+
|
|
224
241
|
const multiaddrs = this.options.getStrandMultiaddrs(request.strandId);
|
|
225
242
|
log('Strand-addr for %s → %d addr(s)', request.strandId, multiaddrs.length);
|
|
226
243
|
return { strandId: request.strandId, multiaddrs };
|
|
227
244
|
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Validate and forward a member's delegate announcement. A malformed field
|
|
248
|
+
* (unparsable peerId) and a self-announcement (`delegatePeerId` equal to the
|
|
249
|
+
* announcer's own peerId — a member is admitted directly, never by grant)
|
|
250
|
+
* are logged and dropped; the address lookup proceeds either way.
|
|
251
|
+
*/
|
|
252
|
+
private recordDelegateAnnounce(request: StrandAddrRequest, remotePeerId: string): void {
|
|
253
|
+
const { strandId, delegatePeerId } = request;
|
|
254
|
+
if (delegatePeerId === undefined || !this.options.onDelegateAnnounce) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (delegatePeerId === remotePeerId) {
|
|
258
|
+
log('Ignoring self-announcement from %s (strand %s)', remotePeerId, strandId);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
peerIdFromString(delegatePeerId);
|
|
263
|
+
} catch (err) {
|
|
264
|
+
log('Ignoring malformed delegatePeerId %o from %s: %o', delegatePeerId, remotePeerId, err);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
this.options.onDelegateAnnounce(remotePeerId, strandId, delegatePeerId);
|
|
268
|
+
}
|
|
228
269
|
}
|
|
229
270
|
|
|
230
271
|
/** A sibling to ask for its strand address. */
|
|
@@ -245,6 +286,13 @@ export interface CollectStrandAddrsOptions {
|
|
|
245
286
|
timeoutMs?: number;
|
|
246
287
|
/** Override the protocol id (defaults to {@link STRAND_ADDR_PROTOCOL}). */
|
|
247
288
|
protocolId?: string;
|
|
289
|
+
/**
|
|
290
|
+
* The derived transport peerId the caller's own strand-`strandId` node runs
|
|
291
|
+
* (or is about to run) as. Set on every request this collection sends, so
|
|
292
|
+
* each receiver records a delegate admission grant for it — see
|
|
293
|
+
* {@link StrandAddrRequest.delegatePeerId}. Omit for a pure address lookup.
|
|
294
|
+
*/
|
|
295
|
+
delegatePeerId?: string;
|
|
248
296
|
}
|
|
249
297
|
|
|
250
298
|
/**
|
|
@@ -268,7 +316,9 @@ export async function collectStrandAddrs(
|
|
|
268
316
|
options: CollectStrandAddrsOptions = {}
|
|
269
317
|
): Promise<string[]> {
|
|
270
318
|
const selfId = node.peerId.toString();
|
|
271
|
-
const request: StrandAddrRequest =
|
|
319
|
+
const request: StrandAddrRequest = options.delegatePeerId !== undefined
|
|
320
|
+
? { strandId, delegatePeerId: options.delegatePeerId }
|
|
321
|
+
: { strandId };
|
|
272
322
|
const candidates = peers.filter(p => p.peerId !== selfId);
|
|
273
323
|
|
|
274
324
|
// Concurrent dials; `dialOneSibling` swallows per-peer failure into `[]`, so
|
|
@@ -314,16 +364,13 @@ async function dialOneSibling(
|
|
|
314
364
|
}
|
|
315
365
|
|
|
316
366
|
for (const target of targets) {
|
|
317
|
-
// One controller per attempt: on timeout, `withTimeout`'s `onTimeout` aborts
|
|
318
|
-
// it, which aborts the in-flight dialProtocol (via the dial `signal`) and the
|
|
319
|
-
// live stream — so neither the connect nor the unbounded response-read leaks.
|
|
320
|
-
const controller = new AbortController();
|
|
321
367
|
try {
|
|
322
|
-
|
|
368
|
+
// One deadline per attempt: its signal aborts the in-flight dialProtocol and
|
|
369
|
+
// resets the live stream, so neither the connect nor the response-read leaks.
|
|
370
|
+
const response = await withDeadline(
|
|
323
371
|
timeoutMs,
|
|
324
372
|
`Strand-addr dial ${peer.peerId}`,
|
|
325
|
-
() => sendStrandAddr(node, target, protocolId, request, timeoutMs,
|
|
326
|
-
() => controller.abort(),
|
|
373
|
+
(signal) => sendStrandAddr(node, target, protocolId, request, timeoutMs, signal),
|
|
327
374
|
);
|
|
328
375
|
return response.multiaddrs;
|
|
329
376
|
} catch (err) {
|
|
@@ -361,12 +408,10 @@ function describeTarget(target: PeerId | Multiaddr): string {
|
|
|
361
408
|
* Open one stream to a target, send the request, half-close, and read the
|
|
362
409
|
* response.
|
|
363
410
|
*
|
|
364
|
-
* `signal` is the per-attempt
|
|
365
|
-
*
|
|
366
|
-
* the stream is open
|
|
367
|
-
* otherwise unbounded response-read.
|
|
368
|
-
* backstop for the case where an abort does not propagate (e.g. a stream double
|
|
369
|
-
* whose `abort()` is a no-op).
|
|
411
|
+
* `signal` is the per-attempt deadline from {@link dialOneSibling}: it goes to
|
|
412
|
+
* `dialProtocol` so a timeout during connect aborts the dial, and into
|
|
413
|
+
* {@link exchangeFrame} so a timeout after the stream is open resets it —
|
|
414
|
+
* releasing the otherwise unbounded response-read.
|
|
370
415
|
*/
|
|
371
416
|
async function sendStrandAddr(
|
|
372
417
|
node: Libp2p,
|
|
@@ -382,29 +427,15 @@ async function sendStrandAddr(
|
|
|
382
427
|
// relay's data/duration cap, so opening it on the limited connection is safe
|
|
383
428
|
// and is the whole point of dialing over the relay.
|
|
384
429
|
const rawStream = await node.dialProtocol(target, protocolId, { runOnLimitedConnection: true, signal });
|
|
385
|
-
const stream = rawStream as unknown as ControlStream;
|
|
386
|
-
|
|
387
|
-
const abortErr = new Error('Strand-addr dial aborted by timeout');
|
|
388
|
-
const onAbort = (): void => stream.abort(abortErr);
|
|
389
|
-
// If the timeout already fired during connect, release the freshly-opened stream now.
|
|
390
|
-
if (signal.aborted) {
|
|
391
|
-
onAbort();
|
|
392
|
-
throw abortErr;
|
|
393
|
-
}
|
|
394
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
395
430
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
throw err;
|
|
407
|
-
} finally {
|
|
408
|
-
signal.removeEventListener('abort', onAbort);
|
|
409
|
-
}
|
|
431
|
+
const response = await exchangeFrame(
|
|
432
|
+
rawStream as unknown as ControlStream,
|
|
433
|
+
signal,
|
|
434
|
+
request,
|
|
435
|
+
(stream) => readFrame<StrandAddrResponse>(stream, timeoutMs),
|
|
436
|
+
'Strand-addr dial aborted by timeout',
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
log('Strand-addr response: %d addr(s) for %s', response.multiaddrs.length, response.strandId);
|
|
440
|
+
return response;
|
|
410
441
|
}
|