@serfab/cadre-core 0.8.1 → 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 +6 -4
- 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 +1006 -101
- package/dist/cadre-node.js +1977 -283
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-authorization.d.ts +93 -0
- package/dist/control-authorization.js +57 -0
- package/dist/control-authorization.js.map +1 -0
- package/dist/control-cohort.d.ts +15 -15
- package/dist/control-cohort.js +14 -14
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +1033 -76
- package/dist/control-database.js +1520 -153
- package/dist/control-database.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +91 -12
- package/dist/control-formation-recorder.js +131 -16
- package/dist/control-formation-recorder.js.map +1 -1
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +774 -186
- 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 +12 -12
- package/dist/device-token.js +15 -14
- package/dist/device-token.js.map +1 -1
- package/dist/ed25519-key.d.ts +68 -0
- package/dist/ed25519-key.js +106 -0
- package/dist/ed25519-key.js.map +1 -0
- 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 +44 -10
- 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/key-store.d.ts +1 -1
- package/dist/key-store.js +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 +150 -11
- package/dist/peer-authorization.js +170 -13
- package/dist/peer-authorization.js.map +1 -1
- package/dist/peer-record.d.ts +24 -11
- package/dist/peer-record.js +29 -13
- package/dist/peer-record.js.map +1 -1
- package/dist/push-node.d.ts +38 -0
- package/dist/push-node.js +48 -0
- package/dist/push-node.js.map +1 -0
- package/dist/push-notifier-apns.d.ts +10 -2
- package/dist/push-notifier-apns.js +14 -10
- package/dist/push-notifier-apns.js.map +1 -1
- package/dist/push-notifier-fcm.d.ts +7 -1
- package/dist/push-notifier-fcm.js +9 -10
- package/dist/push-notifier-fcm.js.map +1 -1
- package/dist/push-notifier.d.ts +16 -35
- package/dist/push-notifier.js +16 -44
- package/dist/push-notifier.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 +263 -79
- package/dist/seed-bootstrap.js +413 -211
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/seed-trust-policy.d.ts +50 -24
- package/dist/seed-trust-policy.js +28 -19
- 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-database.d.ts +5 -5
- package/dist/strand-database.js +4 -4
- package/dist/strand-database.js.map +1 -1
- 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 +104 -5
- package/dist/strand-formation-protocol.js +233 -5
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +40 -2
- package/dist/strand-instance-manager.js +93 -15
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-member-key.d.ts +6 -6
- package/dist/strand-member-key.js +6 -6
- package/dist/strand-member-key.js.map +1 -1
- package/dist/strand-member-registry.d.ts +12 -12
- package/dist/strand-member-registry.js +22 -14
- package/dist/strand-member-registry.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +532 -138
- package/dist/strand-membership-writer.js +800 -179
- 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 +325 -50
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -1
- package/package.json +23 -8
- package/src/bootstrap-peer-store-file.ts +44 -0
- package/src/bootstrap-peer-store.ts +172 -0
- package/src/cadre-node.ts +2129 -295
- package/src/control-authorization.ts +114 -0
- package/src/control-cohort.ts +17 -17
- package/src/control-database.ts +1766 -191
- package/src/control-formation-recorder.ts +174 -21
- package/src/control-schema.ts +786 -198
- 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 +19 -14
- package/src/ed25519-key.ts +134 -0
- 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 +157 -24
- package/src/key-store-file.ts +10 -81
- package/src/key-store.ts +1 -1
- package/src/membership-connection-gater.ts +204 -0
- package/src/node-local-snapshot.ts +260 -0
- package/src/peer-authorization.ts +219 -13
- package/src/peer-record.ts +31 -13
- package/src/push-node.ts +64 -0
- package/src/push-notifier-apns.ts +16 -12
- package/src/push-notifier-fcm.ts +9 -10
- package/src/push-notifier.ts +16 -57
- package/src/relay-addrs.ts +111 -0
- package/src/relay-reservation.ts +823 -0
- package/src/seed-bootstrap.ts +490 -226
- package/src/seed-trust-policy.ts +65 -32
- package/src/strand-addr-protocol.ts +84 -53
- package/src/strand-backfill.ts +366 -0
- package/src/strand-database.ts +6 -6
- package/src/strand-formation-manager.ts +145 -19
- package/src/strand-formation-protocol.ts +310 -9
- package/src/strand-instance-manager.ts +113 -16
- package/src/strand-member-key.ts +7 -7
- package/src/strand-member-registry.ts +28 -23
- 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 +342 -51
- package/dist/authority-key.d.ts +0 -41
- package/dist/authority-key.js +0 -52
- package/dist/authority-key.js.map +0 -1
- package/src/authority-key.ts +0 -68
package/dist/seed-bootstrap.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { Libp2p } from '@libp2p/interface';
|
|
2
|
-
import type { ControlNetworkSeed, SeedAckMessage, AuthorizePeerOptions, ApplySeedResult, AddDroneOptions, AddPhoneOptions, DroneInitResult, InviteResult, CadreInvite, PeerAddressRecord, DeviceTokenRecord } from './types.js';
|
|
2
|
+
import type { ControlNetworkSeed, SeedAckMessage, AuthorizePeerOptions, ApplySeedResult, AddDroneOptions, AddPhoneOptions, DroneInitResult, InviteResult, CadreInvite, PeerAddressRecord, DeviceTokenRecord, RevocationRow } from './types.js';
|
|
3
3
|
import type { ControlDatabase } from './control-database.js';
|
|
4
4
|
import { type SeedTrustPolicy } from './seed-trust-policy.js';
|
|
5
|
+
import type { TrustedOwnerStore } from './trusted-owner-store.js';
|
|
5
6
|
/** Protocol ID for seed delivery */
|
|
6
7
|
export declare const SEED_PROTOCOL = "/sereus/seed/1.0.0";
|
|
7
8
|
/**
|
|
@@ -18,10 +19,10 @@ export declare function decodeLengthPrefixedFrame(data: Uint8Array, maxLength?:
|
|
|
18
19
|
*
|
|
19
20
|
* An Ed25519 PeerId is an identity multihash of the public key, so
|
|
20
21
|
* `peerIdFromString(id).publicKey.raw` is the 32-byte ed25519 key whose
|
|
21
|
-
* base64url form matches the `
|
|
22
|
-
* `
|
|
22
|
+
* base64url form matches the `OwnerKey.Key` representation (and
|
|
23
|
+
* `ed25519KeyPairFromLibp2p().publicKeyB64`). Returns null for a non-Ed25519
|
|
23
24
|
* id, a missing embedded key, or any parse failure — callers treat null as
|
|
24
|
-
* "not an
|
|
25
|
+
* "not an owner" rather than throwing.
|
|
25
26
|
*/
|
|
26
27
|
export declare function ed25519PublicKeyB64FromPeerId(peerId: string): string | null;
|
|
27
28
|
/**
|
|
@@ -40,10 +41,10 @@ export declare function canonicalSeedPayload(seed: Pick<ControlNetworkSeed, 'par
|
|
|
40
41
|
export interface SeedBootstrapConfig {
|
|
41
42
|
/** Party ID for this cadre */
|
|
42
43
|
partyId: string;
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
|
|
44
|
+
/** Owner private key for signing seeds and peer authorizations (base64url) */
|
|
45
|
+
ownerPrivateKey?: string;
|
|
46
|
+
/** Owner public key (base64url) - derived from private key if not provided */
|
|
47
|
+
ownerPublicKey?: string;
|
|
47
48
|
/**
|
|
48
49
|
* Optional async resolver returning the multiaddrs to embed in invites.
|
|
49
50
|
* When unset, `libp2pNode.getMultiaddrs()` is used. Hosts behind NAT supply
|
|
@@ -53,9 +54,9 @@ export interface SeedBootstrapConfig {
|
|
|
53
54
|
inviteAddressResolver?: () => Promise<string[]>;
|
|
54
55
|
/**
|
|
55
56
|
* Trust anchor for incoming seeds. Decides whether a signature-verified
|
|
56
|
-
* `signerKey` should be trusted, against the receiver's
|
|
57
|
-
* keys (NOT the seed body). Defaults to `
|
|
58
|
-
* rejects any signer not already in
|
|
57
|
+
* `signerKey` should be trusted, against the receiver's anchored owner
|
|
58
|
+
* keys (NOT the seed body). Defaults to `anchoredTrustPolicy()`, which
|
|
59
|
+
* rejects any signer not already in {@link trustedOwners}. An enrollment
|
|
59
60
|
* caller can pass a per-seed override to `applySeed` instead.
|
|
60
61
|
*
|
|
61
62
|
* A `CadreNode` forwards its node-wide `CadreNodeConfig.seedTrustPolicy` here
|
|
@@ -63,6 +64,19 @@ export interface SeedBootstrapConfig {
|
|
|
63
64
|
* since a network-delivered seed has no per-call override.
|
|
64
65
|
*/
|
|
65
66
|
trustPolicy?: SeedTrustPolicy;
|
|
67
|
+
/**
|
|
68
|
+
* The node-local, NON-replicated trusted-owner anchor. Supplies
|
|
69
|
+
* `SeedTrustContext.knownOwnerKeys` for every {@link applySeed}, and receives
|
|
70
|
+
* a key accepted via a pin/TOFU (see `SeedTrustDecision.anchorAs`).
|
|
71
|
+
*
|
|
72
|
+
* Deliberately NOT `ControlDatabase.getOwnerKeys()`: the replicated
|
|
73
|
+
* `OwnerKey` table is pollutable — any connecting node can genesis-insert its
|
|
74
|
+
* own key and let it replicate — so a seed signed by a stranger's self-issued
|
|
75
|
+
* owner key would pass a table-anchored check. A `CadreNode` passes its
|
|
76
|
+
* `getTrustedOwnerStore()` here. Unset (e.g. a directly-constructed service in
|
|
77
|
+
* a test) means an EMPTY anchor: only a pinned/TOFU policy can accept a seed.
|
|
78
|
+
*/
|
|
79
|
+
trustedOwners?: TrustedOwnerStore;
|
|
66
80
|
/**
|
|
67
81
|
* Time the inbound seed handler waits for the seed frame before aborting the
|
|
68
82
|
* read (ms). Defaults to {@link DEFAULT_SEED_READ_TIMEOUT_MS}. Bounds a
|
|
@@ -75,6 +89,15 @@ export interface SeedBootstrapConfig {
|
|
|
75
89
|
* returned without applying any seed.
|
|
76
90
|
*/
|
|
77
91
|
maxConcurrentSeeds?: number;
|
|
92
|
+
/**
|
|
93
|
+
* Time {@link SeedBootstrapService.deliverSeed} waits for the whole exchange —
|
|
94
|
+
* dial, write, ack read — before aborting (ms). Defaults to
|
|
95
|
+
* {@link DEFAULT_SEED_DELIVER_TIMEOUT_MS}. Bounds the SENDER against a seed
|
|
96
|
+
* target that accepts the stream and then never replies; the target is a
|
|
97
|
+
* not-yet-trusted node during onboarding, so this is the more exposed
|
|
98
|
+
* direction than the receiver knobs above.
|
|
99
|
+
*/
|
|
100
|
+
seedDeliverTimeoutMs?: number;
|
|
78
101
|
}
|
|
79
102
|
/**
|
|
80
103
|
* Event callbacks for seed-related events
|
|
@@ -82,8 +105,15 @@ export interface SeedBootstrapConfig {
|
|
|
82
105
|
export interface SeedEventCallbacks {
|
|
83
106
|
/** Called when a seed is received via the protocol */
|
|
84
107
|
onSeedReceived?: (partyId: string, peerId: string) => void;
|
|
85
|
-
/**
|
|
86
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Called when a seed is successfully applied.
|
|
110
|
+
*
|
|
111
|
+
* `seed` is the applied seed itself: the inbound protocol handler applies it
|
|
112
|
+
* INSIDE the service, so this callback is the only seam through which a
|
|
113
|
+
* `CadreNode` sees a wire-delivered seed's contents — which it needs to
|
|
114
|
+
* retain the owner-flagged peers as cold-start bootstrap dial targets.
|
|
115
|
+
*/
|
|
116
|
+
onSeedApplied?: (partyId: string, peersAdded: number, seed: ControlNetworkSeed) => void;
|
|
87
117
|
/** Called when seed application fails */
|
|
88
118
|
onSeedError?: (partyId: string, error: string) => void;
|
|
89
119
|
}
|
|
@@ -98,10 +128,11 @@ export declare class SeedBootstrapService {
|
|
|
98
128
|
private readonly config;
|
|
99
129
|
private libp2pNode;
|
|
100
130
|
private controlDatabase;
|
|
101
|
-
private readonly
|
|
131
|
+
private readonly ownerPublicKey;
|
|
102
132
|
private readonly trustPolicy;
|
|
103
133
|
private readonly seedReadTimeoutMs;
|
|
104
134
|
private readonly maxConcurrentSeeds;
|
|
135
|
+
private readonly seedDeliverTimeoutMs;
|
|
105
136
|
/** In-flight inbound seed streams, used to enforce {@link maxConcurrentSeeds}. */
|
|
106
137
|
private activeStreams;
|
|
107
138
|
private eventCallbacks;
|
|
@@ -112,12 +143,12 @@ export declare class SeedBootstrapService {
|
|
|
112
143
|
*/
|
|
113
144
|
setEventCallbacks(callbacks: SeedEventCallbacks): void;
|
|
114
145
|
/**
|
|
115
|
-
* Whether this service holds an
|
|
116
|
-
*
|
|
146
|
+
* Whether this service holds an owner private key, i.e. can produce the
|
|
147
|
+
* owner signatures that gate `CadrePeer` / `DeviceToken` inserts, deletes,
|
|
117
148
|
* and re-authorizations. A seed-listener-only service (`enableSeedListener`,
|
|
118
|
-
* no
|
|
119
|
-
* or re-issue
|
|
120
|
-
* drain to skip
|
|
149
|
+
* no owner key) returns false: it can receive/apply seeds but cannot author
|
|
150
|
+
* or re-issue owner writes. Used by the write-while-alone re-replication
|
|
151
|
+
* drain to skip owner work on a non-owner node.
|
|
121
152
|
*/
|
|
122
153
|
canAuthorize(): boolean;
|
|
123
154
|
/**
|
|
@@ -137,96 +168,187 @@ export declare class SeedBootstrapService {
|
|
|
137
168
|
}): void;
|
|
138
169
|
/**
|
|
139
170
|
* Authorize a new peer to join the cadre.
|
|
140
|
-
* Signs
|
|
171
|
+
* Signs a membership voucher with the owner key and inserts into CadrePeer table.
|
|
141
172
|
*
|
|
142
|
-
* The
|
|
173
|
+
* The owner vouches the `PublicKey <-> PeerId` binding: rather than trust a
|
|
143
174
|
* caller-supplied key, the binding is enforced by construction — `PublicKey` is
|
|
144
175
|
* DERIVED from the (Ed25519) `peerId`. A non-Ed25519 peer id yields a null
|
|
145
176
|
* `PublicKey`, and such a row can never be self-updated (it has no key to
|
|
146
177
|
* verify against), which is correct. The row is inserted with a fresh
|
|
147
|
-
* `UpdatedAt` but no self-signature (`Sig` null) — the
|
|
178
|
+
* `UpdatedAt` but no self-signature (`Sig` null) — the owner cannot produce
|
|
148
179
|
* the peer's self-signature, so the peer must self-publish (see
|
|
149
180
|
* {@link CadreNode.registerSelf}) before the row resolves.
|
|
150
181
|
*/
|
|
151
182
|
authorizePeer(options: AuthorizePeerOptions): Promise<void>;
|
|
152
183
|
/**
|
|
153
|
-
*
|
|
184
|
+
* Owner-signed INSERT of this node's OWN self-signed address record.
|
|
154
185
|
*
|
|
155
186
|
* Used by {@link CadreNode.registerSelf} when the node is not yet a member and
|
|
156
|
-
* is its own
|
|
187
|
+
* is its own owner (it holds the owner key): the row is owner-signed
|
|
157
188
|
* (satisfying `AuthorizedInsert`) AND carries a valid self-`Sig`, so it resolves
|
|
158
189
|
* immediately without a follow-up self-update.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
190
|
+
*
|
|
191
|
+
* @returns `true` when this call seated the row, `false` when a concurrent writer
|
|
192
|
+
* (e.g. an {@link authorizePeer} of this node's own id) had already seated it —
|
|
193
|
+
* in which case the row in the database is NOT this record and carries whatever
|
|
194
|
+
* `Sig` that writer had, so the caller must self-update to publish its signature.
|
|
195
|
+
*/
|
|
196
|
+
insertSelfPeerRecord(record: PeerAddressRecord): Promise<boolean>;
|
|
197
|
+
/**
|
|
198
|
+
* Shared owner-signed `CadrePeer` INSERT — the row fields and the owner-key
|
|
199
|
+
* precondition; the stamp mint, voucher signature, write lock, existence check and
|
|
200
|
+
* membership notify are all {@link ControlDatabase.insertCadrePeer}'s (see there for
|
|
201
|
+
* the anti-replay and insert-race rationale). No `CadrePeer` write may bypass that
|
|
202
|
+
* method: the write is what admits the peer's traffic.
|
|
203
|
+
*
|
|
204
|
+
* The DB check precedes the owner-key check here, matching the order this method has
|
|
205
|
+
* always surfaced them (the owner-key error used to come out of the signer, after the
|
|
206
|
+
* DB check).
|
|
207
|
+
*
|
|
208
|
+
* @returns `true` when this call performed the INSERT, `false` when the row was
|
|
209
|
+
* already seated by a concurrent writer. The loser needs to know: an authorize seats
|
|
210
|
+
* a row with a null `Sig`, so a self-publish that lost the race must fall through to
|
|
211
|
+
* a self-update or its record never lands.
|
|
167
212
|
*/
|
|
168
213
|
private insertCadrePeerRow;
|
|
169
214
|
/**
|
|
170
|
-
*
|
|
215
|
+
* Owner-signed INSERT of a peer's OWN self-signed `DeviceToken` row.
|
|
171
216
|
*
|
|
172
217
|
* Counterpart to {@link insertSelfPeerRecord} for the device-token registry: the
|
|
173
|
-
* row is
|
|
174
|
-
*
|
|
175
|
-
* own self-`Sig` over the token payload. The
|
|
176
|
-
*
|
|
218
|
+
* row is owner-signed (satisfying `DeviceToken.AuthorizedInsert` via the
|
|
219
|
+
* 'add'-tagged digest, {@link deviceTokenAddDigest}) AND carries the peer's
|
|
220
|
+
* own self-`Sig` over the token payload. The owner signature covers the WHOLE row
|
|
221
|
+
* (every column, ending in a freshly minted single-use `StampId`) — but covering the
|
|
222
|
+
* token contents is not the same as vouching them: the peer's `Sig` (verified at
|
|
177
223
|
* resolve time against the bound `CadrePeer.PublicKey`) is what makes the row
|
|
178
224
|
* resolvable. Used by {@link CadreNode.registerDeviceToken} for the first publish
|
|
179
|
-
* when the node is its own
|
|
225
|
+
* when the node is its own owner.
|
|
226
|
+
*
|
|
227
|
+
* The stamp is per-INSERT, so a re-register after a clear mints a fresh one and a
|
|
228
|
+
* fresh signature — unaffected by the cleared row's retired stamp
|
|
229
|
+
* (`DeviceToken.NotRevoked`).
|
|
180
230
|
*/
|
|
181
231
|
insertSelfDeviceToken(record: DeviceTokenRecord): Promise<void>;
|
|
182
232
|
/**
|
|
183
|
-
*
|
|
184
|
-
* invalidation). The `DeviceToken.
|
|
185
|
-
*
|
|
186
|
-
*
|
|
233
|
+
* Owner-signed DELETE of a peer's `DeviceToken` row (logout / token
|
|
234
|
+
* invalidation). The `DeviceToken.AuthorizedDelete` constraint validates an owner
|
|
235
|
+
* signature over the 'remove'-tagged digest bound to the STORED row's
|
|
236
|
+
* (PeerId, StampId) — deliberately distinct from the insert digest, so a captured
|
|
237
|
+
* insert approval can never be replayed to clear a token. Like {@link removePeer}
|
|
238
|
+
* for `CadrePeer`, clearing a token requires the owner key.
|
|
239
|
+
*
|
|
240
|
+
* The delete and the `Revocation` tombstone retiring the row's `StampId` commit in
|
|
241
|
+
* ONE transaction — `DeviceToken.RevocationRecorded` refuses a bare delete, and
|
|
242
|
+
* without the tombstone the stamp would free up and the never-expiring insert
|
|
243
|
+
* approval (which the cleared device holds a copy of) would re-seat the token. The
|
|
244
|
+
* tombstone carries its OWN owner signature: retiring a stamp permanently forecloses
|
|
245
|
+
* that row, so it is an owner action in its own right.
|
|
246
|
+
*
|
|
247
|
+
* Both digests, the stamp read, and the transaction come from
|
|
248
|
+
* {@link ControlDatabase.deleteDeviceToken} — one shared implementation across
|
|
249
|
+
* `CadrePeer` / `DeviceToken` / `Strand` / `ValidationKey`. What stays here is the
|
|
250
|
+
* owner-key precondition; a no-op on an already-absent row is the shared body's
|
|
251
|
+
* behavior, and unlike {@link removePeer} nothing here rides on it.
|
|
187
252
|
*/
|
|
188
253
|
deleteDeviceToken(peerId: string): Promise<void>;
|
|
189
254
|
/**
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
* {@link
|
|
193
|
-
*
|
|
255
|
+
* Return the configured owner private key, or throw if none is set. The single
|
|
256
|
+
* precondition gate for every owner-signed write. {@link removePeer} /
|
|
257
|
+
* {@link reauthorizePeer} read the row's `StampId` from the DB BEFORE they sign, so
|
|
258
|
+
* they call this up front — otherwise a keyless service would either surface the
|
|
259
|
+
* wrong "Control database not initialized" error or, worse, silently no-op when the
|
|
260
|
+
* target row is absent (the early `stampId === null` return) instead of rejecting.
|
|
194
261
|
*/
|
|
195
|
-
private
|
|
262
|
+
private requireOwnerPrivateKey;
|
|
196
263
|
/**
|
|
197
|
-
*
|
|
264
|
+
* The owner PUBLIC key that rides in every owner-signed write's context, non-null.
|
|
198
265
|
*
|
|
199
|
-
* The
|
|
200
|
-
*
|
|
201
|
-
*
|
|
266
|
+
* The field is nullable because a read-only service carries no owner key at all, but the
|
|
267
|
+
* constructor derives the public key whenever `ownerPrivateKey` is set — so gating on
|
|
268
|
+
* {@link requireOwnerPrivateKey} first makes the pair inseparable and the second throw
|
|
269
|
+
* unreachable. Callers that must reject a keyless service BEFORE any DB read use this as
|
|
270
|
+
* their first line and get the owner-key precondition for free.
|
|
271
|
+
*/
|
|
272
|
+
private requireOwnerPublicKey;
|
|
273
|
+
/**
|
|
274
|
+
* Sign a base64url digest with the owner key (ed25519). The single place the
|
|
275
|
+
* owner private key is applied; callers pass the canonical domain-tagged digest for
|
|
276
|
+
* the specific action ({@link deviceTokenAddDigest}), or the raw-bytes form via
|
|
277
|
+
* {@link signMessageBytes}. Throws if no owner key is set.
|
|
278
|
+
*/
|
|
279
|
+
private signDigest;
|
|
280
|
+
/**
|
|
281
|
+
* Adapt the control database's raw-bytes `signMessage` callback (every guarded delete
|
|
282
|
+
* takes one) to {@link signDigest}'s base64url-string form. Both encodings hash to the
|
|
283
|
+
* same signed bytes (`sign` decodes its base64url input), so a signature minted here
|
|
284
|
+
* satisfies the same schema CHECK as one from the callers that sign the bytes directly — see
|
|
285
|
+
* `control-revocation-replay.spec.ts`'s "raw-bytes and digest-string signers agree".
|
|
286
|
+
*/
|
|
287
|
+
private signMessageBytes;
|
|
288
|
+
/**
|
|
289
|
+
* Remove a peer from the cadre by owner signature.
|
|
290
|
+
*
|
|
291
|
+
* The `CadrePeer.AuthorizedDelete` (`check on delete`) constraint validates a
|
|
292
|
+
* signature over the DISTINCT 'remove'-tagged digest
|
|
293
|
+
* `digest('CadreControl.CadrePeer', 'remove', old.PeerId, old.StampId)` by an owner
|
|
294
|
+
* key — deliberately NOT the insert voucher digest, so the row's stored `VouchSig` can
|
|
295
|
+
* never be replayed to delete.
|
|
296
|
+
*
|
|
297
|
+
* The delete and the `Revocation` tombstone retiring the row's `StampId` commit in ONE
|
|
298
|
+
* transaction — `CadrePeer.RevocationRecorded` refuses a bare delete, and without the
|
|
299
|
+
* tombstone the stamp would free up and the original admission approval (which never
|
|
300
|
+
* expires, and which the removed peer holds a copy of) would re-seat the row.
|
|
301
|
+
*
|
|
302
|
+
* The tombstone is separately owner-signed (satisfying `Revocation.Authorized`):
|
|
303
|
+
* retiring a stamp evicts that peer party-wide and permanently forecloses re-admitting
|
|
304
|
+
* the row, so it is an owner action in its own right, not a side effect the delete's
|
|
305
|
+
* signature covers.
|
|
306
|
+
*
|
|
307
|
+
* Both digests, the stamp read, the transaction, and the post-commit membership notify
|
|
308
|
+
* come from {@link ControlDatabase.deleteCadrePeer} — one shared implementation across
|
|
309
|
+
* `CadrePeer` / `DeviceToken` / `Strand` / `ValidationKey`. What stays here is the
|
|
310
|
+
* owner-key precondition and the absent-row gate the notify depends on (below).
|
|
202
311
|
*/
|
|
203
312
|
removePeer(peerId: string): Promise<void>;
|
|
204
313
|
/**
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
* `Sig` are left intact — so it is safe over a row whose peer has not
|
|
217
|
-
* self-published (`Sig` null); the caller must skip a row that already carries a
|
|
218
|
-
* self-`Sig` (that row is the owning peer's to refresh, and bumping `UpdatedAt`
|
|
219
|
-
* without re-signing would invalidate its self-signature).
|
|
314
|
+
* Owner "re-touch" of an existing `CadrePeer` membership row: bump `UpdatedAt` and
|
|
315
|
+
* re-vouch the row so it is re-emitted as a fresh, broadcasting transaction. This is
|
|
316
|
+
* the write-while-alone re-replication primitive (`control-write-ensure-replicated`):
|
|
317
|
+
* a membership row that committed local-only (its block's cluster ≤1 at insert) is
|
|
318
|
+
* pushed to the cohort once it grows.
|
|
319
|
+
*
|
|
320
|
+
* The stamp read, the voucher signature, the write lock and the membership notify are
|
|
321
|
+
* {@link ControlDatabase.reauthorizeCadrePeer}'s — including the caveat that it rebinds
|
|
322
|
+
* `VouchOwner` to this node's owner key. What stays here is the owner-key precondition.
|
|
323
|
+
*
|
|
324
|
+
* A no-op (no throw, no notify) when the row does not exist.
|
|
220
325
|
*
|
|
221
326
|
* @param peerId - the membership row to re-touch.
|
|
222
327
|
* @param updatedAt - the strictly-increasing freshness stamp to write.
|
|
223
|
-
* @throws if no
|
|
328
|
+
* @throws if no owner private key is configured (a non-owner cannot
|
|
224
329
|
* re-sign another peer's row) or the control database is not initialized.
|
|
225
330
|
*/
|
|
226
331
|
reauthorizePeer(peerId: string, updatedAt: number): Promise<void>;
|
|
332
|
+
/**
|
|
333
|
+
* Owner re-issue of a batch of `Revocation` tombstones: bump each row's
|
|
334
|
+
* `ReissuedAt` so the storage layer re-broadcasts a tombstone that committed
|
|
335
|
+
* while the node was alone. The delete-while-alone counterpart of
|
|
336
|
+
* {@link reauthorizePeer} — a removed row cannot be re-touched (it is gone),
|
|
337
|
+
* but its tombstone can, and every membership read treats a retired stamp as
|
|
338
|
+
* absent.
|
|
339
|
+
*
|
|
340
|
+
* The signatures, the single transaction, and the strictly-monotonic
|
|
341
|
+
* `reissuedAt` contract are {@link ControlDatabase.reissueRevocations}'s. What
|
|
342
|
+
* stays here is the owner-key precondition.
|
|
343
|
+
*
|
|
344
|
+
* @returns how many tombstones were re-issued (`rows.length` on success).
|
|
345
|
+
* @throws if no owner private key is configured or the control database is not
|
|
346
|
+
* initialized.
|
|
347
|
+
*/
|
|
348
|
+
reissueRevocations(rows: readonly RevocationRow[], reissuedAt: number): Promise<number>;
|
|
227
349
|
/**
|
|
228
350
|
* Create a seed from the current control network state.
|
|
229
|
-
* The seed contains peer information and is signed by an
|
|
351
|
+
* The seed contains peer information and is signed by an owner.
|
|
230
352
|
*/
|
|
231
353
|
createSeed(): Promise<ControlNetworkSeed>;
|
|
232
354
|
/**
|
|
@@ -234,10 +356,15 @@ export declare class SeedBootstrapService {
|
|
|
234
356
|
*
|
|
235
357
|
* Validates the seed signature, then evaluates a trust anchor for the
|
|
236
358
|
* `signerKey` that does NOT come from the seed body: the receiver's
|
|
237
|
-
*
|
|
238
|
-
* TOFU via the configured/overriding
|
|
239
|
-
* self-asserting seed — one that merely lists its
|
|
240
|
-
* peer — no longer passes
|
|
359
|
+
* node-local {@link SeedBootstrapConfig.trustedOwners} anchor, optionally
|
|
360
|
+
* augmented by pinned keys or TOFU via the configured/overriding
|
|
361
|
+
* `SeedTrustPolicy`. A forged self-asserting seed — one that merely lists its
|
|
362
|
+
* own signer as an owner peer — no longer passes, and neither does one signed
|
|
363
|
+
* by a key a stranger genesis-inserted into the replicated `OwnerKey` table.
|
|
364
|
+
*
|
|
365
|
+
* A key accepted via a pin/TOFU is persisted into the anchor (the policy says
|
|
366
|
+
* so via `SeedTrustDecision.anchorAs`), so the next seed from that owner is
|
|
367
|
+
* anchored without re-supplying the invite.
|
|
241
368
|
*
|
|
242
369
|
* @param seed - The seed to apply (already transport-decoded).
|
|
243
370
|
* @param options.trustPolicy - Per-call policy override (e.g. a
|
|
@@ -247,6 +374,26 @@ export declare class SeedBootstrapService {
|
|
|
247
374
|
applySeed(seed: ControlNetworkSeed, options?: {
|
|
248
375
|
trustPolicy?: SeedTrustPolicy;
|
|
249
376
|
}): Promise<ApplySeedResult>;
|
|
377
|
+
/**
|
|
378
|
+
* Persist a signer that a pin/TOFU accepted into the node-local anchor, so a
|
|
379
|
+
* later seed from the same owner is anchored without re-supplying the invite
|
|
380
|
+
* or re-prompting. Only the policy decides this happens (`anchorAs` is unset
|
|
381
|
+
* when the key was already anchored, so a plain re-apply writes nothing) and
|
|
382
|
+
* `trust()` is idempotent, keeping the original provenance for a known key.
|
|
383
|
+
*
|
|
384
|
+
* Failure to PERSIST does not fail the seed: `trust()` reflects the key in the
|
|
385
|
+
* in-memory anchor synchronously, so this seed and the rest of the session are
|
|
386
|
+
* unaffected — only durability across a restart is lost, and that is logged.
|
|
387
|
+
*
|
|
388
|
+
* NOTE: anchoring a key can flip `CadrePeer` rows ALREADY present from
|
|
389
|
+
* unauthorized to authorized, which the write-driven membership-gate refresh
|
|
390
|
+
* (`ControlDatabase.mutateCadrePeer`) cannot see — no row was written. Every
|
|
391
|
+
* anchor mutation today rides seed application, and both seed paths refresh the
|
|
392
|
+
* gate explicitly afterwards (`CadreNode.applySeed`, `onSeedApplied`). If some
|
|
393
|
+
* future path anchors an owner OUTSIDE seed application, it owes the same
|
|
394
|
+
* `CadreNode.refreshMembershipGate()` — or the anchor needs its own hub.
|
|
395
|
+
*/
|
|
396
|
+
private anchorAcceptedSigner;
|
|
250
397
|
/**
|
|
251
398
|
* Encode a seed for out-of-band delivery (e.g., QR code, copy/paste).
|
|
252
399
|
*/
|
|
@@ -257,8 +404,37 @@ export declare class SeedBootstrapService {
|
|
|
257
404
|
decodeSeed(encoded: string): ControlNetworkSeed;
|
|
258
405
|
/**
|
|
259
406
|
* Deliver a seed directly to a peer via the /sereus/seed/1.0.0 protocol.
|
|
407
|
+
*
|
|
408
|
+
* Sender hardening: the whole exchange — dial, write, ack read — is bounded by
|
|
409
|
+
* {@link seedDeliverTimeoutMs}, and the ack is capped at {@link MAX_SEED_SIZE}.
|
|
410
|
+
* The target is a NOT-YET-TRUSTED node the instigator chose to dial during
|
|
411
|
+
* onboarding, so an unbounded read here is strictly more exposed than the
|
|
412
|
+
* membership-gated receiver paths: without the bound a target that accepts the
|
|
413
|
+
* stream and never replies parks this call forever, and one that streams
|
|
414
|
+
* arbitrary bytes as a fake ack exhausts memory.
|
|
260
415
|
*/
|
|
261
416
|
deliverSeed(targetMultiaddr: string, seed: ControlNetworkSeed): Promise<SeedAckMessage>;
|
|
417
|
+
/**
|
|
418
|
+
* Open one stream to the target, send the seed frame, half-close, and read the ack.
|
|
419
|
+
*
|
|
420
|
+
* `signal` is the deadline from {@link deliverSeed}: it goes to `dialProtocol` so
|
|
421
|
+
* a timeout during connect aborts the dial, and into {@link exchangeFrame} so a
|
|
422
|
+
* timeout after the stream is open resets it — releasing the otherwise unbounded
|
|
423
|
+
* ack-read.
|
|
424
|
+
*
|
|
425
|
+
* Deliberately NOT `runOnLimitedConnection`: a wake sets it because a wake is a
|
|
426
|
+
* tiny frame over a relay, whereas a seed is up to 1MB and this delivery path
|
|
427
|
+
* does not dial relay addresses today. Changing that is a separate decision.
|
|
428
|
+
*/
|
|
429
|
+
private sendSeed;
|
|
430
|
+
/**
|
|
431
|
+
* Read the ack frame a delivery target writes back, bounded by
|
|
432
|
+
* {@link seedDeliverTimeoutMs} and capped at {@link MAX_SEED_SIZE} — an
|
|
433
|
+
* untrusted target must not be able to stream unlimited bytes as a fake ack.
|
|
434
|
+
* Decoding runs inside {@link exchangeFrame}'s `try`, so a malformed or
|
|
435
|
+
* non-JSON ack resets the stream rather than leaking it.
|
|
436
|
+
*/
|
|
437
|
+
private readSeedAck;
|
|
262
438
|
/**
|
|
263
439
|
* Get this node's circuit relay address for inclusion in seeds.
|
|
264
440
|
* Returns null if no relay address is available.
|
|
@@ -271,12 +447,20 @@ export declare class SeedBootstrapService {
|
|
|
271
447
|
/**
|
|
272
448
|
* Query peers from the control database.
|
|
273
449
|
*
|
|
274
|
-
*
|
|
450
|
+
* Owner identity is sourced from the `OwnerKey` table, not from the
|
|
275
451
|
* transport peer ID. An Ed25519 libp2p PeerId embeds its public key (identity
|
|
276
452
|
* multihash), so each peer's ed25519 key is derivable from its `PeerId`; a
|
|
277
|
-
* peer is an
|
|
278
|
-
* This makes any
|
|
279
|
-
* `
|
|
453
|
+
* peer is an owner iff that derived key is in the `OwnerKey` set.
|
|
454
|
+
* This makes any owner node markable — not just the local one — and ties
|
|
455
|
+
* `isOwner` to the control table rather than to `peerId === self`.
|
|
456
|
+
*
|
|
457
|
+
* NOTE: this is the one owner lookup deliberately left on the REPLICATED
|
|
458
|
+
* table rather than the node-local anchor. `SeedPeer.isOwner` is a dial hint
|
|
459
|
+
* — the receiver dials owner-flagged peers first — not a trust decision, and
|
|
460
|
+
* the receiver re-derives real trust from its own anchor. So a polluted table
|
|
461
|
+
* costs at most a wasted dial, while anchoring here would silently drop
|
|
462
|
+
* legitimate co-owners this node never pinned. If `isOwner` ever gates
|
|
463
|
+
* anything the receiver TRUSTS, move it to the anchor.
|
|
280
464
|
*/
|
|
281
465
|
private queryPeers;
|
|
282
466
|
/**
|
|
@@ -357,11 +541,11 @@ export declare class SeedBootstrapService {
|
|
|
357
541
|
*/
|
|
358
542
|
decodeInvite(encoded: string): CadreInvite;
|
|
359
543
|
/**
|
|
360
|
-
* Dial an
|
|
361
|
-
* Use this on a phone after receiving an invite to connect to the
|
|
544
|
+
* Dial an owner from an invite.
|
|
545
|
+
* Use this on a phone after receiving an invite to connect to the owner.
|
|
362
546
|
*
|
|
363
547
|
* @param invite - The invite received out-of-band
|
|
364
|
-
* @returns Connection to the
|
|
548
|
+
* @returns Connection to the owner
|
|
365
549
|
*/
|
|
366
550
|
dialInvite(invite: CadreInvite): Promise<void>;
|
|
367
551
|
}
|