@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/src/relay-addrs.ts
CHANGED
|
@@ -1,32 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Translate `network.relayAddrs` — the operator-facing "use these relay servers"
|
|
3
|
-
* setting — into the
|
|
4
|
-
* slot
|
|
3
|
+
* setting — into the listen entry a node needs before it can hold a circuit-relay
|
|
4
|
+
* slot, and validate the operator's entries while doing it.
|
|
5
5
|
*
|
|
6
|
-
* `@libp2p/circuit-relay-v2`'s
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* that relay and hold a reservation. Everything downstream of the listen list —
|
|
10
|
-
* the reservation itself, `CadreNode.circuitRelayTargets()` picking delegate-announce
|
|
11
|
-
* targets, strand nodes inheriting the listen addrs, the `/p2p-circuit`-first address
|
|
12
|
-
* publishing in `registerSelf` — then works unchanged.
|
|
6
|
+
* `@libp2p/circuit-relay-v2`'s listener branches on the SHAPE of the listen addr,
|
|
7
|
+
* and the same `relayAddrs` list resolves to either shape depending on WHO is
|
|
8
|
+
* listening ({@link RelayListenRoute}):
|
|
13
9
|
*
|
|
14
|
-
*
|
|
10
|
+
* | route | listen entry | libp2p behaviour |
|
|
11
|
+
* | -------------- | ------------------------------------------- | --------------------------------------------------------- |
|
|
12
|
+
* | `'configured'` | `<relayAddr>/p2p-circuit`, one per relay | dial that relay from inside `listen()` and reserve, or throw |
|
|
13
|
+
* | `'search'` | a single bare `/p2p-circuit` | register a pending reservation; open no connection |
|
|
15
14
|
*
|
|
16
|
-
*
|
|
15
|
+
* The CONTROL node takes the `'search'` route, and `CadreNode.start()` drives the
|
|
16
|
+
* reservation explicitly (`relay-reservation.ts`) once the control database is up.
|
|
17
|
+
* That ordering is the point: a configured circuit listener dials the relay from
|
|
18
|
+
* inside `libp2p.start()`, so the control database was being built while a sibling
|
|
19
|
+
* relay was already in this node's cohort — and a sibling that has not yet
|
|
20
|
+
* replicated this node's `CadrePeer` row correctly refuses its control-DB streams,
|
|
21
|
+
* which killed `start()` outright. See `cadre-node.ts` → `start()`.
|
|
17
22
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* STRAND nodes keep the `'configured'` route (`strand-instance-manager.ts`): they
|
|
24
|
+
* inherit the control node's `NetworkConfig`, nothing drives an explicit
|
|
25
|
+
* reservation for them, and their relay connection cannot disturb a control-DB
|
|
26
|
+
* bring-up (a strand node's protocol ids are namespaced `/optimystic/strand-<id>/…`,
|
|
27
|
+
* so the relay is never in its cohort).
|
|
28
|
+
*
|
|
29
|
+
* `network.relayAddrs` is FAIL-FAST for the operator on both routes, just from
|
|
30
|
+
* different places: a malformed entry throws here at config resolution, and on the
|
|
31
|
+
* control node a first reservation attempt that does not land throws out of
|
|
32
|
+
* `start()` (`RelayReservationFailedError`). Naming a relay that is down still
|
|
33
|
+
* means the node does not come up.
|
|
34
|
+
*
|
|
35
|
+
* The bare `/p2p-circuit` search listener cannot open a connection on its own:
|
|
36
|
+
* libp2p fills a pending reservation from `RelayDiscovery`, which nominates a peer
|
|
37
|
+
* only once the relay-hop protocol id is in that peer's peer-store protocol list,
|
|
38
|
+
* and that list is written exclusively by IDENTIFY — which `@optimystic/db-p2p`
|
|
39
|
+
* namespaces per network, so a cadre node and a stock relay never identify each
|
|
40
|
+
* other. See `relay-reservation.ts`'s module doc and `docs/architecture.md`.
|
|
25
41
|
*/
|
|
26
42
|
|
|
27
43
|
import { multiaddr, type Component } from '@multiformats/multiaddr';
|
|
28
44
|
import { circuitRelayTargetOrThrow } from './delegate-admission.js';
|
|
29
45
|
import type { NetworkConfig } from './types.js';
|
|
46
|
+
import type { RelayReservationState } from './relay-reservation.js';
|
|
30
47
|
|
|
31
48
|
/**
|
|
32
49
|
* The direct listener a node keeps when it configures `relayAddrs` but no
|
|
@@ -40,6 +57,21 @@ import type { NetworkConfig } from './types.js';
|
|
|
40
57
|
*/
|
|
41
58
|
const DEFAULT_DIRECT_LISTEN_ADDR = '/ip4/0.0.0.0/tcp/0';
|
|
42
59
|
|
|
60
|
+
/**
|
|
61
|
+
* libp2p's relay SEARCH listener: registers one pending reservation and dials
|
|
62
|
+
* nothing. One entry covers every configured relay — the pending reservation is
|
|
63
|
+
* per-listener, not per-relay, and `relay-reservation.ts` fills it with whichever
|
|
64
|
+
* relay answers first.
|
|
65
|
+
*/
|
|
66
|
+
export const RELAY_SEARCH_LISTEN_ADDR = '/p2p-circuit';
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Which listener shape `network.relayAddrs` resolves to for this caller — see the
|
|
70
|
+
* table in the module doc. Defaults to `'configured'` everywhere except the
|
|
71
|
+
* control node's own libp2p options.
|
|
72
|
+
*/
|
|
73
|
+
export type RelayListenRoute = 'configured' | 'search';
|
|
74
|
+
|
|
43
75
|
/**
|
|
44
76
|
* The circuit-listen multiaddr for each configured relay: `<relayAddr>/p2p-circuit`.
|
|
45
77
|
* An entry that already carries `/p2p-circuit` is passed through unchanged.
|
|
@@ -57,20 +89,95 @@ export function relayCircuitAddrs(relayAddrs: readonly string[]): string[] {
|
|
|
57
89
|
|
|
58
90
|
/**
|
|
59
91
|
* The listen multiaddrs a node actually binds: configured `listenAddrs` plus the
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
92
|
+
* relay listen entry `route` calls for, deduplicated, order-stable (configured
|
|
93
|
+
* entries first). Returns `undefined` when neither field is set, so callers keep
|
|
94
|
+
* omitting `listenAddrs` and inherit db-p2p's default.
|
|
95
|
+
*
|
|
96
|
+
* Every `relayAddrs` entry is validated on BOTH routes — the `'search'` route
|
|
97
|
+
* discards the resolved circuit addrs, but an operator typo must fail at config
|
|
98
|
+
* resolution either way.
|
|
63
99
|
*/
|
|
64
|
-
export function resolveListenAddrs(
|
|
65
|
-
|
|
100
|
+
export function resolveListenAddrs(
|
|
101
|
+
network: NetworkConfig | undefined,
|
|
102
|
+
route: RelayListenRoute = 'configured'
|
|
103
|
+
): string[] | undefined {
|
|
104
|
+
const configured = relayCircuitAddrs(network?.relayAddrs ?? []);
|
|
105
|
+
const relayEntries = route === 'search'
|
|
106
|
+
? (configured.length > 0 ? [RELAY_SEARCH_LISTEN_ADDR] : [])
|
|
107
|
+
: configured;
|
|
66
108
|
const listenAddrs = network?.listenAddrs;
|
|
67
|
-
if (
|
|
109
|
+
if (route === 'search') {
|
|
110
|
+
rejectConfiguredCircuitListenAddrs(listenAddrs ?? []);
|
|
111
|
+
}
|
|
112
|
+
if (!listenAddrs && relayEntries.length === 0) {
|
|
68
113
|
return undefined;
|
|
69
114
|
}
|
|
70
115
|
// An explicitly empty `listenAddrs` (the React Native "cannot listen" case) stays
|
|
71
|
-
// empty — but a relay named alongside it still gets its
|
|
72
|
-
// acquiring
|
|
73
|
-
return dedupe([...(listenAddrs ?? [DEFAULT_DIRECT_LISTEN_ADDR]), ...
|
|
116
|
+
// empty — but a relay named alongside it still gets its listen entry, since
|
|
117
|
+
// acquiring a slot is the whole point of configuring a relay.
|
|
118
|
+
return dedupe([...(listenAddrs ?? [DEFAULT_DIRECT_LISTEN_ADDR]), ...relayEntries]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Thrown out of `CadreNode.start()` when the boot-path reservation drive for
|
|
123
|
+
* `network.relayAddrs` produces no `/p2p-circuit` address on its FIRST attempt.
|
|
124
|
+
*
|
|
125
|
+
* This is what keeps `network.relayAddrs` fail-fast now that the control node
|
|
126
|
+
* takes the `'search'` route: libp2p's own `UnsupportedListenAddressesError` used
|
|
127
|
+
* to abort start from inside `listen()`, and an operator who names a relay is
|
|
128
|
+
* telling the node it has no other reachability — coming up undialable is worse
|
|
129
|
+
* than not coming up. Names the relays and the reservation's own reason, both of
|
|
130
|
+
* which the libp2p error omitted.
|
|
131
|
+
*/
|
|
132
|
+
export class RelayReservationFailedError extends Error {
|
|
133
|
+
constructor(
|
|
134
|
+
readonly relayAddrs: readonly string[],
|
|
135
|
+
readonly state: RelayReservationState
|
|
136
|
+
) {
|
|
137
|
+
super(
|
|
138
|
+
`network.relayAddrs reservation failed (status: ${state.status}): ` +
|
|
139
|
+
`${state.error ?? 'no /p2p-circuit address appeared'} — relays: ${relayAddrs.join(', ')}`
|
|
140
|
+
);
|
|
141
|
+
this.name = 'RelayReservationFailedError';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* A hand-written `<relay>/p2p-circuit` entry in `network.listenAddrs` is the
|
|
147
|
+
* CONFIGURED listener shape, and it is unusable on the `'search'` route: libp2p
|
|
148
|
+
* dials that relay from inside `listen()`, the bring-up quiet period
|
|
149
|
+
* (`membership-connection-gater.ts`) denies exactly that dial, `listen()` fails, and
|
|
150
|
+
* the transport manager's default `FATAL_ALL` aborts `libp2p.start()`. The operator
|
|
151
|
+
* would see `UnsupportedListenAddressesError` from deep inside bring-up with nothing
|
|
152
|
+
* naming the cause, so name it here instead.
|
|
153
|
+
*
|
|
154
|
+
* `network.relayAddrs` is the exact replacement — same relay, and the reservation is
|
|
155
|
+
* driven after bring-up, which is the whole point of the route.
|
|
156
|
+
*/
|
|
157
|
+
function rejectConfiguredCircuitListenAddrs(listenAddrs: readonly string[]): void {
|
|
158
|
+
const configured = listenAddrs.filter(isConfiguredCircuitListenAddr);
|
|
159
|
+
if (configured.length > 0) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
'network.listenAddrs names a relay directly ' +
|
|
162
|
+
`(${configured.join(', ')}), which a control node cannot listen on: libp2p would ` +
|
|
163
|
+
'dial that relay during control-database bring-up, when the node accepts no ' +
|
|
164
|
+
'connections. Move the relay to network.relayAddrs, which reserves after bring-up.'
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** `<something>/p2p-circuit` — the configured shape, as opposed to the bare search addr. */
|
|
170
|
+
function isConfiguredCircuitListenAddr(listenAddr: string): boolean {
|
|
171
|
+
if (listenAddr === RELAY_SEARCH_LISTEN_ADDR) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
return multiaddr(listenAddr).getComponents().some((c) => c.name === 'p2p-circuit');
|
|
176
|
+
} catch {
|
|
177
|
+
// Not our error to raise: libp2p reports an unparsable listen addr itself, and
|
|
178
|
+
// this helper only ever ADDS a denial.
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
74
181
|
}
|
|
75
182
|
|
|
76
183
|
/**
|
package/src/relay-reservation.ts
CHANGED
|
@@ -4,28 +4,32 @@
|
|
|
4
4
|
* (live) status derivation.
|
|
5
5
|
*
|
|
6
6
|
* `@libp2p/circuit-relay-v2`'s listener branches on the SHAPE of the listen
|
|
7
|
-
* address
|
|
7
|
+
* address:
|
|
8
8
|
*
|
|
9
9
|
* | listen addr | libp2p behaviour | unreachable relay |
|
|
10
10
|
* | ------------------------------------------------- | ------------------------------------------------------------------------------------ | -------------------------- |
|
|
11
11
|
* | `<dial addr>/p2p/<relayPeerId>/p2p-circuit` | CONFIGURED reservation: dial that exact relay and reserve, or fail | `listen()` throws |
|
|
12
12
|
* | bare `/p2p-circuit` | SEARCH mode: register a pending reservation, to be filled by relay discovery | nothing throws, no reserve |
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
14
|
+
* A search listener needs someone to (a) dial the relay and (b) fill the pending
|
|
15
|
+
* reservation it created, then report whether one actually landed. That is this
|
|
16
|
+
* module. It is **fail-soft by construction**: nothing here throws.
|
|
17
|
+
*
|
|
18
|
+
* EVERY cadre CONTROL node now takes that route — `network.relayAddrs` resolves
|
|
19
|
+
* to the bare search entry (`relay-addrs.ts` → `RelayListenRoute`) and
|
|
20
|
+
* `CadreNode.start()` drives this module once the control database is up. The
|
|
21
|
+
* configured shape would dial the relay from inside `libp2p.start()`, putting a
|
|
22
|
+
* sibling in this node's cohort before its own control database existed, and a
|
|
23
|
+
* sibling that has not yet replicated this node's `CadrePeer` row correctly
|
|
24
|
+
* refuses its control-DB streams — which killed `start()` outright.
|
|
25
|
+
*
|
|
26
|
+
* So the two shapes are no longer alternative ROUTES, only alternative failure
|
|
27
|
+
* postures on top of the same drive: a node that names `network.relayAddrs` gets
|
|
28
|
+
* a `RelayReservationFailedError` out of `start()` when the first attempt lands
|
|
29
|
+
* nothing (fail-fast, the operator asked for this relay), while a node that calls
|
|
30
|
+
* `CadreNode.reserveRelays()` itself gets a non-`reserved` status and stays up
|
|
31
|
+
* (fail-soft, the browser-tab posture). Configuring both is now redundant, not
|
|
32
|
+
* fatal. STRAND nodes still take the configured shape — see `relay-addrs.ts`.
|
|
29
33
|
*
|
|
30
34
|
* ⚠️ WHY THE RESERVATION IS REQUESTED EXPLICITLY RATHER THAN LEFT TO DISCOVERY.
|
|
31
35
|
* libp2p fills a search listener's pending reservation from `RelayDiscovery`,
|
|
@@ -68,7 +72,8 @@ const log = debug('sereus:cadre:relay-reservation');
|
|
|
68
72
|
|
|
69
73
|
/**
|
|
70
74
|
* Relay-reservation posture for a node that reserves through the SEARCH listen
|
|
71
|
-
* addr (bare `/p2p-circuit`)
|
|
75
|
+
* addr (bare `/p2p-circuit`) — every cadre control node, whether its relays came
|
|
76
|
+
* from `network.relayAddrs` or from an explicit `CadreNode.reserveRelays()` call.
|
|
72
77
|
* - `none` — no relay addrs supplied; the node is undialable by design.
|
|
73
78
|
* - `dialing` — a drive is in flight.
|
|
74
79
|
* - `reserved` — the node currently holds at least one `/p2p-circuit` addr.
|
package/src/seed-bootstrap.ts
CHANGED
|
@@ -404,7 +404,7 @@ export class SeedBootstrapService {
|
|
|
404
404
|
insert into CadreControl.DeviceToken (PeerId, Platform, Token, UpdatedAt, Sig, StampId)
|
|
405
405
|
with context OwnerKey = ?, Signature = ?
|
|
406
406
|
values (?, ?, ?, ?, ?, ?)
|
|
407
|
-
`, [this.ownerPublicKey, signature, record.peerId, record.platform, record.token, record.updatedAt, record.sig, stampId]);
|
|
407
|
+
`, [this.ownerPublicKey, signature, record.peerId, record.platform, record.token, record.updatedAt, record.sig, stampId], 'device-token-insert');
|
|
408
408
|
log('Device token inserted (owner-signed): %s', record.peerId);
|
|
409
409
|
}
|
|
410
410
|
|
|
@@ -751,8 +751,8 @@ export class SeedBootstrapService {
|
|
|
751
751
|
// `ApplySeedResult.ownerDialsFailed`). Recovery is not this loop's job —
|
|
752
752
|
// `CadreNode.dialColdStartBootstrap` retries these same addresses on every
|
|
753
753
|
// control-cohort reconcile pass until the control database has siblings.
|
|
754
|
-
// `createSeed` projects
|
|
755
|
-
// minted after it joined finds ITSELF in the owner list. Dialing self always
|
|
754
|
+
// `createSeed` projects every non-revoked CadrePeer row, so an owner applying
|
|
755
|
+
// a seed minted after it joined finds ITSELF in the owner list. Dialing self always
|
|
756
756
|
// throws, which would report a healthy owner as "seeded but stranded".
|
|
757
757
|
// Optional-chained: partial libp2p handles (unit-test doubles) omit `peerId`,
|
|
758
758
|
// and an undefined self simply matches nothing.
|
|
@@ -966,6 +966,13 @@ export class SeedBootstrapService {
|
|
|
966
966
|
* This makes any owner node markable — not just the local one — and ties
|
|
967
967
|
* `isOwner` to the control table rather than to `peerId === self`.
|
|
968
968
|
*
|
|
969
|
+
* Read through {@link ControlDatabase.queryCadrePeers}, not a raw `CadrePeer`
|
|
970
|
+
* select: that reader drops any row whose `StampId` is retired in
|
|
971
|
+
* `CadreControl.Revocation`, so a removed member's addresses are never packed
|
|
972
|
+
* into a seed and pushed into a joiner's peerstore (`applySeed` adds every
|
|
973
|
+
* seed peer's addrs and dials the owner-flagged ones). A revoked peer is off
|
|
974
|
+
* the addressable surface everywhere, and this is one of its exits.
|
|
975
|
+
*
|
|
969
976
|
* NOTE: this is the one owner lookup deliberately left on the REPLICATED
|
|
970
977
|
* table rather than the node-local anchor. `SeedPeer.isOwner` is a dial hint
|
|
971
978
|
* — the receiver dials owner-flagged peers first — not a trust decision, and
|
|
@@ -980,29 +987,22 @@ export class SeedBootstrapService {
|
|
|
980
987
|
}
|
|
981
988
|
|
|
982
989
|
const ownerKeys = await this.controlDatabase.getOwnerKeys();
|
|
983
|
-
const
|
|
984
|
-
const peers: SeedPeer[] = [];
|
|
985
|
-
|
|
986
|
-
// Query CadrePeer table
|
|
987
|
-
for await (const row of db.eval('select PeerId, Multiaddr from CadreControl.CadrePeer')) {
|
|
988
|
-
const peerId = row.PeerId as string;
|
|
989
|
-
const multiaddr = row.Multiaddr as string | null;
|
|
990
|
+
const rows = await this.controlDatabase.queryCadrePeers();
|
|
990
991
|
|
|
992
|
+
return rows.map(({ peerId, multiaddr }) => {
|
|
991
993
|
// Derive the peer's ed25519 key from its PeerId; a non-Ed25519 peer or an
|
|
992
994
|
// unparsable id yields null and is treated as a non-owner rather than
|
|
993
995
|
// failing the whole seed creation.
|
|
994
996
|
const pubKeyB64 = ed25519PublicKeyB64FromPeerId(peerId);
|
|
995
997
|
const isOwner = pubKeyB64 !== null && ownerKeys.has(pubKeyB64);
|
|
996
998
|
|
|
997
|
-
|
|
999
|
+
return {
|
|
998
1000
|
peerId,
|
|
999
1001
|
multiaddrs: multiaddr ? multiaddr.split(',') : [],
|
|
1000
1002
|
isOwner,
|
|
1001
1003
|
...(isOwner ? { publicKey: pubKeyB64 } : {}),
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
return peers;
|
|
1004
|
+
};
|
|
1005
|
+
});
|
|
1006
1006
|
}
|
|
1007
1007
|
|
|
1008
1008
|
/**
|
package/src/seed-trust-policy.ts
CHANGED
|
@@ -1,140 +1,140 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Trust-anchor policy for control-network seeds.
|
|
3
|
-
*
|
|
4
|
-
* A seed carries an ed25519 signature over its body and the `signerKey` that
|
|
5
|
-
* produced it. Verifying that signature only proves the seed is internally
|
|
6
|
-
* consistent — it does NOT prove the signer is an owner, because both the
|
|
7
|
-
* signer key and the seed's own `isOwner` peer flags are attacker-supplied.
|
|
8
|
-
* The trust decision must therefore rest on an anchor that does not come from
|
|
9
|
-
* the seed body:
|
|
10
|
-
*
|
|
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.
|
|
20
|
-
* 3. TOFU (opt-in) — an explicit confirmation callback invoked on first sight
|
|
21
|
-
* of an unknown signer key. Interactive hosts only; off by default.
|
|
22
|
-
*
|
|
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.
|
|
31
|
-
*/
|
|
32
|
-
import type { TrustSource } from './trusted-owner-store.js';
|
|
33
|
-
|
|
34
|
-
export interface SeedTrustContext {
|
|
35
|
-
/** Party the seed claims to belong to. */
|
|
36
|
-
partyId: string;
|
|
37
|
-
/** ed25519 base64url signer key — already signature-verified by `applySeed`. */
|
|
38
|
-
signerKey: string;
|
|
39
|
-
/**
|
|
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.
|
|
43
|
-
*/
|
|
44
|
-
knownOwnerKeys: ReadonlySet<string>;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface SeedTrustDecision {
|
|
48
|
-
/** Whether the signer key is trusted. */
|
|
49
|
-
trusted: boolean;
|
|
50
|
-
/** Human-readable reason, surfaced as the seed-apply error when not trusted. */
|
|
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'>;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Decides whether a signature-verified seed's signer key should be trusted.
|
|
64
|
-
* May be synchronous (anchored/pinned) or asynchronous (TOFU confirmation).
|
|
65
|
-
*/
|
|
66
|
-
export interface SeedTrustPolicy {
|
|
67
|
-
evaluate(ctx: SeedTrustContext): Promise<SeedTrustDecision> | SeedTrustDecision;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
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.
|
|
74
|
-
*/
|
|
75
|
-
export function anchoredTrustPolicy(): SeedTrustPolicy {
|
|
76
|
-
return {
|
|
77
|
-
evaluate({ signerKey, knownOwnerKeys }) {
|
|
78
|
-
if (knownOwnerKeys.has(signerKey)) {
|
|
79
|
-
return { trusted: true };
|
|
80
|
-
}
|
|
81
|
-
return {
|
|
82
|
-
trusted: false,
|
|
83
|
-
reason: 'Signer key is not an anchored owner (anchored trust policy)',
|
|
84
|
-
};
|
|
85
|
-
},
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
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).
|
|
97
|
-
*/
|
|
98
|
-
export function pinnedKeyTrustPolicy(
|
|
99
|
-
pinned: Iterable<string>,
|
|
100
|
-
anchorAs: Exclude<TrustSource, 'genesis'> = 'invite'
|
|
101
|
-
): SeedTrustPolicy {
|
|
102
|
-
const pinnedSet = new Set(pinned);
|
|
103
|
-
return {
|
|
104
|
-
evaluate({ signerKey, knownOwnerKeys }) {
|
|
105
|
-
if (knownOwnerKeys.has(signerKey)) {
|
|
106
|
-
return { trusted: true };
|
|
107
|
-
}
|
|
108
|
-
if (pinnedSet.has(signerKey)) {
|
|
109
|
-
return { trusted: true, anchorAs };
|
|
110
|
-
}
|
|
111
|
-
return {
|
|
112
|
-
trusted: false,
|
|
113
|
-
reason: 'Signer key is neither an anchored nor a pinned owner (pinned-key trust policy)',
|
|
114
|
-
};
|
|
115
|
-
},
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
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.
|
|
125
|
-
*/
|
|
126
|
-
export function tofuTrustPolicy(
|
|
127
|
-
confirm: (ctx: SeedTrustContext) => Promise<boolean>
|
|
128
|
-
): SeedTrustPolicy {
|
|
129
|
-
return {
|
|
130
|
-
async evaluate(ctx) {
|
|
131
|
-
if (ctx.knownOwnerKeys.has(ctx.signerKey)) {
|
|
132
|
-
return { trusted: true };
|
|
133
|
-
}
|
|
134
|
-
const accepted = await confirm(ctx);
|
|
135
|
-
return accepted
|
|
136
|
-
? { trusted: true, anchorAs: 'operator' }
|
|
137
|
-
: { trusted: false, reason: 'TOFU confirmation declined for unknown signer key' };
|
|
138
|
-
},
|
|
139
|
-
};
|
|
140
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Trust-anchor policy for control-network seeds.
|
|
3
|
+
*
|
|
4
|
+
* A seed carries an ed25519 signature over its body and the `signerKey` that
|
|
5
|
+
* produced it. Verifying that signature only proves the seed is internally
|
|
6
|
+
* consistent — it does NOT prove the signer is an owner, because both the
|
|
7
|
+
* signer key and the seed's own `isOwner` peer flags are attacker-supplied.
|
|
8
|
+
* The trust decision must therefore rest on an anchor that does not come from
|
|
9
|
+
* the seed body:
|
|
10
|
+
*
|
|
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.
|
|
20
|
+
* 3. TOFU (opt-in) — an explicit confirmation callback invoked on first sight
|
|
21
|
+
* of an unknown signer key. Interactive hosts only; off by default.
|
|
22
|
+
*
|
|
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.
|
|
31
|
+
*/
|
|
32
|
+
import type { TrustSource } from './trusted-owner-store.js';
|
|
33
|
+
|
|
34
|
+
export interface SeedTrustContext {
|
|
35
|
+
/** Party the seed claims to belong to. */
|
|
36
|
+
partyId: string;
|
|
37
|
+
/** ed25519 base64url signer key — already signature-verified by `applySeed`. */
|
|
38
|
+
signerKey: string;
|
|
39
|
+
/**
|
|
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.
|
|
43
|
+
*/
|
|
44
|
+
knownOwnerKeys: ReadonlySet<string>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface SeedTrustDecision {
|
|
48
|
+
/** Whether the signer key is trusted. */
|
|
49
|
+
trusted: boolean;
|
|
50
|
+
/** Human-readable reason, surfaced as the seed-apply error when not trusted. */
|
|
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'>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Decides whether a signature-verified seed's signer key should be trusted.
|
|
64
|
+
* May be synchronous (anchored/pinned) or asynchronous (TOFU confirmation).
|
|
65
|
+
*/
|
|
66
|
+
export interface SeedTrustPolicy {
|
|
67
|
+
evaluate(ctx: SeedTrustContext): Promise<SeedTrustDecision> | SeedTrustDecision;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
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.
|
|
74
|
+
*/
|
|
75
|
+
export function anchoredTrustPolicy(): SeedTrustPolicy {
|
|
76
|
+
return {
|
|
77
|
+
evaluate({ signerKey, knownOwnerKeys }) {
|
|
78
|
+
if (knownOwnerKeys.has(signerKey)) {
|
|
79
|
+
return { trusted: true };
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
trusted: false,
|
|
83
|
+
reason: 'Signer key is not an anchored owner (anchored trust policy)',
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
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).
|
|
97
|
+
*/
|
|
98
|
+
export function pinnedKeyTrustPolicy(
|
|
99
|
+
pinned: Iterable<string>,
|
|
100
|
+
anchorAs: Exclude<TrustSource, 'genesis'> = 'invite'
|
|
101
|
+
): SeedTrustPolicy {
|
|
102
|
+
const pinnedSet = new Set(pinned);
|
|
103
|
+
return {
|
|
104
|
+
evaluate({ signerKey, knownOwnerKeys }) {
|
|
105
|
+
if (knownOwnerKeys.has(signerKey)) {
|
|
106
|
+
return { trusted: true };
|
|
107
|
+
}
|
|
108
|
+
if (pinnedSet.has(signerKey)) {
|
|
109
|
+
return { trusted: true, anchorAs };
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
trusted: false,
|
|
113
|
+
reason: 'Signer key is neither an anchored nor a pinned owner (pinned-key trust policy)',
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
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.
|
|
125
|
+
*/
|
|
126
|
+
export function tofuTrustPolicy(
|
|
127
|
+
confirm: (ctx: SeedTrustContext) => Promise<boolean>
|
|
128
|
+
): SeedTrustPolicy {
|
|
129
|
+
return {
|
|
130
|
+
async evaluate(ctx) {
|
|
131
|
+
if (ctx.knownOwnerKeys.has(ctx.signerKey)) {
|
|
132
|
+
return { trusted: true };
|
|
133
|
+
}
|
|
134
|
+
const accepted = await confirm(ctx);
|
|
135
|
+
return accepted
|
|
136
|
+
? { trusted: true, anchorAs: 'operator' }
|
|
137
|
+
: { trusted: false, reason: 'TOFU confirmation declined for unknown signer key' };
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|