@serfab/cadre-core 1.13.0 → 1.14.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/dist/cadre-invite-protocol.d.ts +348 -0
- package/dist/cadre-invite-protocol.js +605 -0
- package/dist/cadre-invite-protocol.js.map +1 -0
- package/dist/cadre-node.d.ts +523 -180
- package/dist/cadre-node.js +921 -293
- package/dist/cadre-node.js.map +1 -1
- package/dist/claim-proof.d.ts +45 -0
- package/dist/claim-proof.js +132 -0
- package/dist/claim-proof.js.map +1 -0
- package/dist/control-authorization.d.ts +80 -19
- package/dist/control-authorization.js +55 -13
- package/dist/control-authorization.js.map +1 -1
- package/dist/control-cohort.js +1 -1
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +514 -97
- package/dist/control-database.js +1033 -195
- package/dist/control-database.js.map +1 -1
- package/dist/control-protocol-guard.d.ts +142 -0
- package/dist/control-protocol-guard.js +293 -0
- package/dist/control-protocol-guard.js.map +1 -0
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +801 -1067
- package/dist/control-schema.js.map +1 -1
- package/dist/control-stream.d.ts +4 -0
- package/dist/control-stream.js +22 -7
- package/dist/control-stream.js.map +1 -1
- package/dist/delegate-admission.d.ts +1 -1
- package/dist/delegate-admission.js +1 -1
- package/dist/device-token.d.ts +11 -9
- package/dist/device-token.js +14 -12
- package/dist/device-token.js.map +1 -1
- package/dist/ed25519-key.d.ts +15 -0
- package/dist/ed25519-key.js +28 -1
- package/dist/ed25519-key.js.map +1 -1
- package/dist/index.d.ts +14 -9
- package/dist/index.js +27 -9
- package/dist/index.js.map +1 -1
- package/dist/link-budget.d.ts +16 -29
- package/dist/link-budget.js +16 -31
- package/dist/link-budget.js.map +1 -1
- package/dist/membership-connection-gater.d.ts +190 -151
- package/dist/membership-connection-gater.js +235 -173
- package/dist/membership-connection-gater.js.map +1 -1
- package/dist/node-claim-payload.d.ts +19 -0
- package/dist/node-claim-payload.js +120 -0
- package/dist/node-claim-payload.js.map +1 -0
- package/dist/owner-anchor-sync.d.ts +22 -0
- package/dist/owner-anchor-sync.js +143 -0
- package/dist/owner-anchor-sync.js.map +1 -0
- package/dist/peer-authorization.d.ts +113 -18
- package/dist/peer-authorization.js +225 -29
- package/dist/peer-authorization.js.map +1 -1
- package/dist/peer-dial.d.ts +19 -0
- package/dist/peer-dial.js +22 -1
- package/dist/peer-dial.js.map +1 -1
- package/dist/peer-join-backfill.d.ts +26 -2
- package/dist/peer-join-backfill.js +54 -12
- package/dist/peer-join-backfill.js.map +1 -1
- package/dist/peer-record.d.ts +13 -9
- package/dist/peer-record.js +16 -12
- package/dist/peer-record.js.map +1 -1
- package/dist/pending-join-runner.d.ts +66 -58
- package/dist/pending-join-runner.js +136 -145
- package/dist/pending-join-runner.js.map +1 -1
- package/dist/push-fanout.d.ts +8 -2
- package/dist/push-fanout.js +8 -2
- package/dist/push-fanout.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +147 -122
- package/dist/seed-bootstrap.js +284 -269
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/seed-trust-policy.d.ts +98 -37
- package/dist/seed-trust-policy.js +188 -2
- package/dist/seed-trust-policy.js.map +1 -1
- package/dist/strand-addr-protocol.d.ts +24 -26
- package/dist/strand-addr-protocol.js +19 -26
- package/dist/strand-addr-protocol.js.map +1 -1
- package/dist/strand-formation-protocol.js +1 -1
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-revocation-enforcer.d.ts +11 -11
- package/dist/strand-revocation-enforcer.js +11 -11
- package/dist/strand-wake-protocol.d.ts +13 -16
- package/dist/strand-wake-protocol.js +13 -20
- package/dist/strand-wake-protocol.js.map +1 -1
- package/dist/trusted-owner-store.d.ts +35 -5
- package/dist/trusted-owner-store.js +50 -8
- package/dist/trusted-owner-store.js.map +1 -1
- package/dist/types.d.ts +302 -87
- package/dist/types.js.map +1 -1
- package/package.json +8 -7
- package/src/cadre-invite-protocol.ts +864 -0
- package/src/cadre-node.ts +9381 -8686
- package/src/claim-proof.ts +147 -0
- package/src/control-authorization.ts +203 -117
- package/src/control-cohort.ts +1 -1
- package/src/control-database.ts +1216 -218
- package/src/control-protocol-guard.ts +348 -0
- package/src/control-schema.ts +812 -1078
- package/src/control-stream.ts +26 -7
- package/src/delegate-admission.ts +1 -1
- package/src/device-token.ts +14 -10
- package/src/ed25519-key.ts +28 -1
- package/src/index.ts +84 -10
- package/src/link-budget.ts +16 -32
- package/src/membership-connection-gater.ts +637 -570
- package/src/node-claim-payload.ts +125 -0
- package/src/owner-anchor-sync.ts +171 -0
- package/src/peer-authorization.ts +258 -29
- package/src/peer-dial.ts +22 -1
- package/src/peer-join-backfill.ts +56 -11
- package/src/peer-record.ts +16 -11
- package/src/pending-join-runner.ts +729 -735
- package/src/push-fanout.ts +8 -2
- package/src/seed-bootstrap.ts +344 -321
- package/src/seed-trust-policy.ts +353 -140
- package/src/strand-addr-protocol.ts +26 -35
- package/src/strand-formation-protocol.ts +1 -1
- package/src/strand-revocation-enforcer.ts +11 -11
- package/src/strand-wake-protocol.ts +13 -23
- package/src/trusted-owner-store.ts +242 -171
- package/src/types.ts +2471 -2242
package/src/control-stream.ts
CHANGED
|
@@ -35,6 +35,10 @@ export interface ControlStream extends AsyncIterable<Uint8Array> {
|
|
|
35
35
|
send(data: Uint8Array): boolean;
|
|
36
36
|
close(): Promise<void>;
|
|
37
37
|
abort(err: Error): void;
|
|
38
|
+
/** Whether the remote has half-closed its write end (`'closed'`); absent on test doubles. */
|
|
39
|
+
readonly remoteWriteStatus?: string;
|
|
40
|
+
/** Bytes received and not yet read; absent on test doubles. */
|
|
41
|
+
readonly readBufferLength?: number;
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
/** Write a JSON object as a single 4-byte big-endian length-prefixed frame. */
|
|
@@ -206,16 +210,31 @@ export function readStreamToEnd(
|
|
|
206
210
|
return Promise.race([readLoop, timeout]).finally(() => clearTimeout(timer));
|
|
207
211
|
}
|
|
208
212
|
|
|
209
|
-
/**
|
|
213
|
+
/**
|
|
214
|
+
* Accumulate every chunk to EOF, rejecting once the running total exceeds `maxBytes`.
|
|
215
|
+
*
|
|
216
|
+
* A remote that half-closed before this read began has already sent everything it will,
|
|
217
|
+
* so the read ends once those buffered bytes are in. Its own end-of-stream cannot be
|
|
218
|
+
* waited for: libp2p 3.3.11's stream iterator ends on the one-shot `remoteCloseWrite`
|
|
219
|
+
* event, and an iterator attached after it fired never ends. That happens to any handler
|
|
220
|
+
* that awaits before reading, as the control-protocol guard's live membership check makes
|
|
221
|
+
* the wake and strand-addr handlers do.
|
|
222
|
+
*/
|
|
210
223
|
async function collect(stream: ControlStream, maxBytes: number, label: string): Promise<Uint8Array> {
|
|
211
224
|
const chunks: Uint8Array[] = [];
|
|
212
225
|
let total = 0;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
226
|
+
const alreadySent = stream.remoteWriteStatus === 'closed' ? (stream.readBufferLength ?? 0) : undefined;
|
|
227
|
+
if (alreadySent !== 0) {
|
|
228
|
+
for await (const chunk of stream) {
|
|
229
|
+
const bytes = toBytes(chunk);
|
|
230
|
+
chunks.push(bytes);
|
|
231
|
+
total += bytes.length;
|
|
232
|
+
if (total > maxBytes) {
|
|
233
|
+
throw new Error(`${label} message too large: ${total} bytes exceeds max ${maxBytes}`);
|
|
234
|
+
}
|
|
235
|
+
if (alreadySent !== undefined && total >= alreadySent) {
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
219
238
|
}
|
|
220
239
|
}
|
|
221
240
|
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* granted delegate is admitted outright, never drawing on the small
|
|
22
22
|
* unauthorized-reservation budget that unplaced peers share
|
|
23
23
|
* (`membership-connection-gater.ts` → "The relay-reservation seam") and never
|
|
24
|
-
*
|
|
24
|
+
* put on the provisional connection deadline. The fail-closed per-stream gate
|
|
25
25
|
* (`authorizeInboundControlStream`) never honors a grant, so a delegate gets
|
|
26
26
|
* the connection and its reservation (all a circuit-relay `hop` needs) and
|
|
27
27
|
* nothing more.
|
package/src/device-token.ts
CHANGED
|
@@ -9,15 +9,16 @@
|
|
|
9
9
|
* encoding, so no field split is ambiguous even with an opaque platform token. It
|
|
10
10
|
* is intentionally NOT JSON-canonicalized (deterministic across node/browser/RN,
|
|
11
11
|
* no key ordering) and is reconstructable inside the `DeviceToken.AuthorizedUpdate`
|
|
12
|
-
* SQL constraint from the row's own columns
|
|
12
|
+
* SQL constraint from the row's own columns and the node-local `party_id()`:
|
|
13
13
|
*
|
|
14
|
-
* digest('CadreControl.DeviceToken', 'publish',
|
|
14
|
+
* digest('CadreControl.DeviceToken', 'publish', party_id(),
|
|
15
15
|
* new.PeerId, new.Platform, new.Token, cast(new.UpdatedAt as text))
|
|
16
16
|
*
|
|
17
17
|
* Keep {@link deviceTokenSignedPayload} and that constraint byte-for-byte in sync.
|
|
18
18
|
* The `'publish'` action tag marks this as a peer's SELF-signed record (signed with
|
|
19
19
|
* the peer's own key, not an owner key) and keeps it disjoint from every
|
|
20
|
-
* owner-signed digest
|
|
20
|
+
* owner-signed digest; the party id keeps a record published into one party's table
|
|
21
|
+
* from verifying in another's (peer-record.ts says why a node uses its own party).
|
|
21
22
|
*
|
|
22
23
|
* Unlike a peer-address record, a device-token record carries NO public key: the
|
|
23
24
|
* signature is verified against the `CadrePeer.PublicKey` bound to the same PeerId
|
|
@@ -34,10 +35,11 @@ import type { DeviceTokenRecord, PushPlatform } from './types.js';
|
|
|
34
35
|
* `DeviceToken.AuthorizedUpdate` constraint exactly; both sides take the default
|
|
35
36
|
* base64url output of a single `digest(...)`, which round-trips cleanly.
|
|
36
37
|
*/
|
|
37
|
-
export function deviceTokenSignedPayload(record: Omit<DeviceTokenRecord, 'sig'>): string {
|
|
38
|
+
export function deviceTokenSignedPayload(partyId: string, record: Omit<DeviceTokenRecord, 'sig'>): string {
|
|
38
39
|
const fields = controlAuthorizationFields(
|
|
39
40
|
'CadreControl.DeviceToken',
|
|
40
41
|
'publish',
|
|
42
|
+
partyId,
|
|
41
43
|
[record.peerId, record.platform, record.token, String(record.updatedAt)]
|
|
42
44
|
);
|
|
43
45
|
return digest(fields, 'sha256', 'base64url') as string;
|
|
@@ -47,14 +49,16 @@ export function deviceTokenSignedPayload(record: Omit<DeviceTokenRecord, 'sig'>)
|
|
|
47
49
|
* Sign a device-token record with the ed25519 private key behind its `peerId`.
|
|
48
50
|
* Returns a fully-populated {@link DeviceTokenRecord}.
|
|
49
51
|
*
|
|
52
|
+
* @param partyId - the party whose `DeviceToken` table the record is published into
|
|
50
53
|
* @param fields - the record fields to sign
|
|
51
54
|
* @param privateKeyB64 - base64url ed25519 seed (see `ed25519KeyPairFromLibp2p`)
|
|
52
55
|
*/
|
|
53
56
|
export function signDeviceTokenRecord(
|
|
57
|
+
partyId: string,
|
|
54
58
|
fields: Omit<DeviceTokenRecord, 'sig'>,
|
|
55
59
|
privateKeyB64: string
|
|
56
60
|
): DeviceTokenRecord {
|
|
57
|
-
const payloadDigest = deviceTokenSignedPayload(fields);
|
|
61
|
+
const payloadDigest = deviceTokenSignedPayload(partyId, fields);
|
|
58
62
|
const sig = sign(
|
|
59
63
|
payloadDigest,
|
|
60
64
|
privateKeyB64,
|
|
@@ -68,15 +72,15 @@ export function signDeviceTokenRecord(
|
|
|
68
72
|
|
|
69
73
|
/**
|
|
70
74
|
* Verify a record's self-signature against the `CadrePeer.PublicKey` (base64url)
|
|
71
|
-
* bound to its `peerId`. Reconstructs the signed bytes from the record
|
|
72
|
-
* {@link signDeviceTokenRecord} produced them. Returns false on a missing
|
|
73
|
-
* any verification failure.
|
|
75
|
+
* bound to its `peerId`, for `partyId`. Reconstructs the signed bytes from the record
|
|
76
|
+
* exactly as {@link signDeviceTokenRecord} produced them. Returns false on a missing
|
|
77
|
+
* key/sig or any verification failure.
|
|
74
78
|
*/
|
|
75
|
-
export function verifyDeviceTokenSignature(record: DeviceTokenRecord, publicKeyB64: string): boolean {
|
|
79
|
+
export function verifyDeviceTokenSignature(partyId: string, record: DeviceTokenRecord, publicKeyB64: string): boolean {
|
|
76
80
|
if (!publicKeyB64 || !record.sig) {
|
|
77
81
|
return false;
|
|
78
82
|
}
|
|
79
|
-
const payloadDigest = deviceTokenSignedPayload(record);
|
|
83
|
+
const payloadDigest = deviceTokenSignedPayload(partyId, record);
|
|
80
84
|
return verify(
|
|
81
85
|
payloadDigest,
|
|
82
86
|
record.sig,
|
package/src/ed25519-key.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { toString as uint8ArrayToString, fromString as uint8ArrayFromString } from 'uint8arrays';
|
|
2
2
|
import { getPublicKey } from '@optimystic/quereus-plugin-crypto';
|
|
3
|
+
import { peerIdFromString } from '@libp2p/peer-id';
|
|
3
4
|
import type { PrivateKey } from '@libp2p/interface';
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -68,12 +69,38 @@ export function ed25519PublicKeyFromPrivate(privateKeyB64: string): string {
|
|
|
68
69
|
return getPublicKey(privateKeyB64, 'ed25519', 'base64url', 'base64url') as string;
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Derive the base64url ed25519 public key embedded in an Ed25519 libp2p PeerId.
|
|
74
|
+
*
|
|
75
|
+
* An Ed25519 PeerId is an identity multihash of the public key, so
|
|
76
|
+
* `peerIdFromString(id).publicKey.raw` is the 32-byte ed25519 key whose
|
|
77
|
+
* base64url form matches the `OwnerKey.Key` representation (and
|
|
78
|
+
* `ed25519KeyPairFromLibp2p().publicKeyB64`). Returns null for a non-Ed25519
|
|
79
|
+
* id, a missing embedded key, or any parse failure — callers treat null as
|
|
80
|
+
* "not an owner" rather than throwing.
|
|
81
|
+
*
|
|
82
|
+
* Lives here, beside the other key bridges, so the lightweight verifiers in
|
|
83
|
+
* `peer-authorization.ts` can check a stored `PeerKey` against a row's `PeerId`
|
|
84
|
+
* without importing the seed-bootstrap runtime.
|
|
85
|
+
*/
|
|
86
|
+
export function ed25519PublicKeyB64FromPeerId(peerId: string): string | null {
|
|
87
|
+
try {
|
|
88
|
+
const parsed = peerIdFromString(peerId);
|
|
89
|
+
if (parsed.type !== 'Ed25519' || !parsed.publicKey) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return uint8ArrayToString(parsed.publicKey.raw, 'base64url');
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
71
98
|
/** Longest rejected key value echoed back in an error — a real key is 43 base64url chars. */
|
|
72
99
|
const REJECTED_VALUE_ECHO_LIMIT = 64;
|
|
73
100
|
|
|
74
101
|
/**
|
|
75
102
|
* Render a rejected value for an error message, capped: owner keys now reach this check
|
|
76
|
-
* from remote-supplied fields (a
|
|
103
|
+
* from remote-supplied fields (a cadre invitation's `ownerKeys`, a donation request's
|
|
77
104
|
* `ownerKeys`), so an unbounded echo would let a peer turn one junk string into a
|
|
78
105
|
* megabyte of log line.
|
|
79
106
|
*/
|
package/src/index.ts
CHANGED
|
@@ -5,10 +5,10 @@ export * from './types.js';
|
|
|
5
5
|
export { canonicalJson } from './canonical-json.js';
|
|
6
6
|
|
|
7
7
|
// Main CadreNode class
|
|
8
|
-
export { CadreNode } from './cadre-node.js';
|
|
8
|
+
export { CadreNode, OWNER_INVITATION_DEFAULT_TTL_MS, CADRE_INVITATION_DEFAULT_TTL_MS } from './cadre-node.js';
|
|
9
9
|
|
|
10
10
|
// Control database
|
|
11
|
-
export { ControlDatabase, MissingHostStrandError, buildAuthorizationMessage, formationVouchMessage, formationConsentMessage, REAPABLE_TABLES, type ReapableTable, type ControlDatabaseConfig, type ControlTable, type RevocableTable, type FormationUsageResult, type MembershipChangeListener, type GuardedDeleteListener, type ControlWriteAbandonedListener, type RevokedRowRef } from './control-database.js';
|
|
11
|
+
export { ControlDatabase, MissingHostStrandError, InvitationExhaustedError, CadreInviteIssuerUnknownError, buildAuthorizationMessage, registerPartyIdFunction, formationVouchMessage, formationConsentMessage, cadreInviteAddMessage, cadreInviteRedeemMessage, cadreInviteConsentMessage, projectSeedPeers, REAPABLE_TABLES, type ReapableTable, type ControlDatabaseConfig, type ControlTable, type RevocableTable, type FormationUsageResult, type MembershipChangeListener, type GuardedDeleteListener, type ControlWriteAbandonedListener, type RevokedRowRef } from './control-database.js';
|
|
12
12
|
|
|
13
13
|
// Bounded retry for transient control-write failures (classifier + loop behind
|
|
14
14
|
// ControlDatabase.lockedWithRetry; exported so the integration package can drive the
|
|
@@ -45,10 +45,10 @@ export type {
|
|
|
45
45
|
} from './control-retry.js';
|
|
46
46
|
|
|
47
47
|
// Control-plane authorization field vector (the domain/action tagging every signer shares)
|
|
48
|
-
export { controlAuthorizationFields, type ControlDomain, type ControlAction } from './control-authorization.js';
|
|
48
|
+
export { controlAuthorizationFields, unboundAuthorizationFields, cadreInviteRowFields, type ControlDomain, type BoundControlDomain, type UnboundControlDomain, type ControlAction, type CadreInviteSignedFields } from './control-authorization.js';
|
|
49
49
|
|
|
50
50
|
// Ed25519 key bridge (libp2p Ed25519 -> base64url keypair)
|
|
51
|
-
export { ed25519KeyPairFromLibp2p, ed25519PublicKeyFromPrivate, requireEd25519PublicKeyB64, type Ed25519KeyPair } from './ed25519-key.js';
|
|
51
|
+
export { ed25519KeyPairFromLibp2p, ed25519PublicKeyFromPrivate, ed25519PublicKeyB64FromPeerId, requireEd25519PublicKeyB64, type Ed25519KeyPair } from './ed25519-key.js';
|
|
52
52
|
|
|
53
53
|
// Pluggable key store (backend-agnostic identity/owner key material seam).
|
|
54
54
|
// Dependency-free: the interface, error, default slot id, and in-memory backend
|
|
@@ -362,7 +362,8 @@ export {
|
|
|
362
362
|
export {
|
|
363
363
|
SeedBootstrapService,
|
|
364
364
|
SEED_PROTOCOL,
|
|
365
|
-
|
|
365
|
+
mergeSeedPeers,
|
|
366
|
+
seedDigest,
|
|
366
367
|
type SeedBootstrapConfig,
|
|
367
368
|
type SeedEventCallbacks
|
|
368
369
|
} from './seed-bootstrap.js';
|
|
@@ -375,12 +376,27 @@ export {
|
|
|
375
376
|
deviceTokenAddDigest,
|
|
376
377
|
deviceTokenRemoveDigest,
|
|
377
378
|
formationConsentDigest,
|
|
379
|
+
cadreInviteAddDigest,
|
|
380
|
+
cadreInviteRedeemDigest,
|
|
381
|
+
cadreInviteConsentDigest,
|
|
382
|
+
ownerKeyAddDigest,
|
|
378
383
|
verifyPeerAuthorization,
|
|
379
384
|
verifyCadrePeerVoucher,
|
|
385
|
+
verifyOwnerKeyVoucher,
|
|
386
|
+
verifyRevocationSigner,
|
|
380
387
|
verifyFormationConsent,
|
|
388
|
+
verifyInvitationAdmission,
|
|
389
|
+
verifyInvitationOwnerAdmission,
|
|
390
|
+
verifyCadreInviteRow,
|
|
391
|
+
verifyCadreInviteRedemption,
|
|
381
392
|
type DeviceTokenAuthorizedRow
|
|
382
393
|
} from './peer-authorization.js';
|
|
383
394
|
|
|
395
|
+
// How the trusted-owner anchor follows the replicated OwnerKey table along verifiable
|
|
396
|
+
// chains: the pure derivation rule `CadreNode.syncOwnerAnchor` applies on every membership
|
|
397
|
+
// refresh (owner-anchor-sync.ts).
|
|
398
|
+
export { deriveOwnerAnchor, type OwnerAnchorInputs, type OwnerAnchorDerivation } from './owner-anchor-sync.js';
|
|
399
|
+
|
|
384
400
|
// Strand Wake (control-network push-wake protocol)
|
|
385
401
|
export {
|
|
386
402
|
StrandWakeService,
|
|
@@ -469,6 +485,7 @@ export {
|
|
|
469
485
|
export {
|
|
470
486
|
dialPeerAddrs,
|
|
471
487
|
SelfRelayOnlyError,
|
|
488
|
+
PeerUnreachableError,
|
|
472
489
|
tryAddrsInTurn,
|
|
473
490
|
directBeforeRelayed,
|
|
474
491
|
DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS,
|
|
@@ -508,11 +525,35 @@ export {
|
|
|
508
525
|
anchoredTrustPolicy,
|
|
509
526
|
pinnedKeyTrustPolicy,
|
|
510
527
|
tofuTrustPolicy,
|
|
528
|
+
claimSecretTrustPolicy,
|
|
511
529
|
type SeedTrustPolicy,
|
|
512
530
|
type SeedTrustContext,
|
|
513
|
-
type SeedTrustDecision
|
|
531
|
+
type SeedTrustDecision,
|
|
532
|
+
type ClaimSecretTrustPolicyOptions
|
|
514
533
|
} from './seed-trust-policy.js';
|
|
515
534
|
|
|
535
|
+
// Claim proof: how a brand-new node's first seed proves its sender holds the node's
|
|
536
|
+
// one-time claim secret. The owner side builds the proof over `seedDigest` (exported
|
|
537
|
+
// with the seed bootstrap above); `claimSecretTrustPolicy` verifies it.
|
|
538
|
+
// `ClaimRefusedError` is what `CadreNode.claimNode` throws when the node said no;
|
|
539
|
+
// `PeerUnreachableError` (exported with the peer dial above) when it was never reached.
|
|
540
|
+
export {
|
|
541
|
+
parseClaimSecret,
|
|
542
|
+
claimProof,
|
|
543
|
+
verifyClaimProof,
|
|
544
|
+
ClaimRefusedError,
|
|
545
|
+
CLAIM_SECRET_BYTES
|
|
546
|
+
} from './claim-proof.js';
|
|
547
|
+
|
|
548
|
+
// The claim payload: the node's peer id, addresses and claim secret as the text a host
|
|
549
|
+
// shows as a QR code and a phone scans (`CadreNode.claimNode` takes the decoded form).
|
|
550
|
+
export {
|
|
551
|
+
encodeNodeClaimPayload,
|
|
552
|
+
decodeNodeClaimPayload,
|
|
553
|
+
NODE_CLAIM_PAYLOAD_PREFIX,
|
|
554
|
+
type NodeClaimPayload
|
|
555
|
+
} from './node-claim-payload.js';
|
|
556
|
+
|
|
516
557
|
// Schema Verification
|
|
517
558
|
export {
|
|
518
559
|
signSchema,
|
|
@@ -553,6 +594,35 @@ export {
|
|
|
553
594
|
type FormationRejectionCode
|
|
554
595
|
} from './strand-formation-rejection.js';
|
|
555
596
|
export { formationDeadlines, type FormationDeadlines } from './strand-formation-deadlines.js';
|
|
597
|
+
// Cadre invitation redemption transport: the bundle an owner hands out, the wire messages, the
|
|
598
|
+
// member-side handler and the device-side dialer, and how a redemption fails
|
|
599
|
+
export {
|
|
600
|
+
CadreInviteHandler,
|
|
601
|
+
redeemAtMembers,
|
|
602
|
+
signRedeemRequest,
|
|
603
|
+
verifyRedeemReply,
|
|
604
|
+
encodeCadreInvitation,
|
|
605
|
+
decodeCadreInvitation,
|
|
606
|
+
isWellFormedCadreInviteRow,
|
|
607
|
+
sanitizeAddrs,
|
|
608
|
+
CadreInviteRejectedError,
|
|
609
|
+
CadreInviteUnreachableError,
|
|
610
|
+
CadreInviteReplyInvalidError,
|
|
611
|
+
CADRE_INVITE_PROTOCOL,
|
|
612
|
+
CADRE_INVITE_REJECTION_RETRYABLE,
|
|
613
|
+
isCadreInviteRejectionCode,
|
|
614
|
+
type CadreInvitation,
|
|
615
|
+
type CadreInviteRedeemRequest,
|
|
616
|
+
type CadreInviteRedeemReply,
|
|
617
|
+
type CadreInviteAccepted,
|
|
618
|
+
type CadreInviteRejectionCode,
|
|
619
|
+
type CadreInviteAddressOutcome,
|
|
620
|
+
type CadreInviteHandlerOptions,
|
|
621
|
+
type CadreInviteStore,
|
|
622
|
+
type RedeemAtMembersOptions,
|
|
623
|
+
type RedeemSigner,
|
|
624
|
+
type CadreInviteRedemption
|
|
625
|
+
} from './cadre-invite-protocol.js';
|
|
556
626
|
// The pending-join retry loop behind CadreNode.requestJoin (PendingJoinStatus is in types.ts)
|
|
557
627
|
export { PENDING_JOIN_POLL_MS, PENDING_JOIN_MAX_BACKOFF_MS, MAX_PENDING_JOIN_MS } from './pending-join-runner.js';
|
|
558
628
|
|
|
@@ -569,13 +639,17 @@ export {
|
|
|
569
639
|
type StrandFormationManagerOptions
|
|
570
640
|
} from './strand-formation-manager.js';
|
|
571
641
|
|
|
642
|
+
// Control-network per-stream protocol guard (which peers may open which protocol)
|
|
643
|
+
export {
|
|
644
|
+
controlProtocolClasses,
|
|
645
|
+
type ControlProtocolClass
|
|
646
|
+
} from './control-protocol-guard.js';
|
|
647
|
+
|
|
572
648
|
// Control-network inbound connection gate (membership defense-in-depth)
|
|
573
649
|
export {
|
|
574
650
|
createMembershipConnectionGater,
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
RELAY_ADMISSION_RESERVE_DEADLINE_MS,
|
|
578
|
-
RELAY_ADMISSION_CLOSE_TIMEOUT_MS,
|
|
651
|
+
PROVISIONAL_ADMISSION_DEADLINE_MS,
|
|
652
|
+
PROVISIONAL_ADMISSION_CLOSE_TIMEOUT_MS,
|
|
579
653
|
MAX_UNAUTHORIZED_RELAY_RESERVATIONS,
|
|
580
654
|
UnauthorizedReservationBudget,
|
|
581
655
|
type InboundAdmissionPolicy,
|
package/src/link-budget.ts
CHANGED
|
@@ -73,9 +73,7 @@
|
|
|
73
73
|
* Every row that opens a relayed connection counts opening the relay connection too
|
|
74
74
|
* ({@link RELAYED_DIAL_ROUND_TRIPS}), because a dialer cannot know beforehand whether it holds
|
|
75
75
|
* one, and a budget that is too short fails as silently as an absent peer. The
|
|
76
|
-
* reservation-request row is {@link RELAY_RESERVE_REQUEST_ROUND_TRIPS},
|
|
77
|
-
* a party-run relay waits for a stranger's reservation ({@link relayAdmissionReserveDeadlineMs}).
|
|
78
|
-
* The commit row is a measurement of a whole write, not a count of exchanges; its doc comment
|
|
76
|
+
* reservation-request row is {@link RELAY_RESERVE_REQUEST_ROUND_TRIPS}. The commit row is a measurement of a whole write, not a count of exchanges; its doc comment
|
|
79
77
|
* ({@link COMMIT_ROUND_TRIPS}) holds the figures and says when to re-measure it.
|
|
80
78
|
*
|
|
81
79
|
* **Measured** 2026-09-26, one Windows machine, loopback dedicated relay: a relayed dial over a
|
|
@@ -198,10 +196,10 @@
|
|
|
198
196
|
* the host derives its provisioning budget from its own, so a joiner declaring a faster
|
|
199
197
|
* link than the host gives up on a reply the host is still entitled to send. Declare the
|
|
200
198
|
* same link on every machine of a party.
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
199
|
+
* Provisional admission too: a member derives how long it keeps a connection from a peer it
|
|
200
|
+
* cannot place from ITS declaration ({@link relayedRequestBudgetMs}), so a member declaring a
|
|
201
|
+
* faster link than a redeeming device can close that device's connection while the device is
|
|
202
|
+
* still waiting on its redemption.
|
|
205
203
|
*/
|
|
206
204
|
|
|
207
205
|
import { resolveLinkDeadlines, type Libp2pConnectionTimeouts, type RpcDeadlineDefaults } from '@optimystic/db-p2p';
|
|
@@ -261,20 +259,19 @@ export const RELAY_DIAL_ROUND_TRIPS = 1;
|
|
|
261
259
|
export const RELAYED_DIAL_ROUND_TRIPS = RELAY_DIAL_ROUND_TRIPS + CIRCUIT_DIAL_ROUND_TRIPS;
|
|
262
260
|
|
|
263
261
|
/**
|
|
264
|
-
* Link round trips from the moment a
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
262
|
+
* Link round trips from the moment a relay accepts a client's connection to the moment that
|
|
263
|
+
* client's RESERVE request reaches it. libp2p's reservation store (`@libp2p/circuit-relay-v2`'s
|
|
264
|
+
* `addRelay`) opens the hop stream the instant its dial resolves, without waiting for identify,
|
|
265
|
+
* so:
|
|
268
266
|
*
|
|
269
267
|
* 1. The client's last Noise handshake message and its hop-stream open arrive at the relay
|
|
270
|
-
* together.
|
|
271
|
-
*
|
|
268
|
+
* together. A party-run relay's gate runs on the handshake message, then the relay answers
|
|
269
|
+
* the protocol negotiation.
|
|
272
270
|
* 2. The answer reaches the client and the client's RESERVE request reaches the relay: one round
|
|
273
271
|
* trip on the client-to-relay hop, which is the whole link round trip when all of the link's
|
|
274
272
|
* delay is on the client's own hop (a phone on a congested mobile link).
|
|
275
273
|
*
|
|
276
|
-
* The relay's decision on that request is local time, not link time
|
|
277
|
-
* {@link relayAdmissionReserveDeadlineMs} adds it separately.
|
|
274
|
+
* The relay's decision on that request is local time, not link time.
|
|
278
275
|
*/
|
|
279
276
|
export const RELAY_RESERVE_REQUEST_ROUND_TRIPS = 1;
|
|
280
277
|
|
|
@@ -511,23 +508,6 @@ export function relayReservationBudgetMs(linkRoundTripMs?: number): number {
|
|
|
511
508
|
return RELAY_RESERVATION_ROUND_TRIPS * resolveLinkRoundTripMs(linkRoundTripMs) + 2 * ADMISSION_DECISION_TIMEOUT_MS;
|
|
512
509
|
}
|
|
513
510
|
|
|
514
|
-
/**
|
|
515
|
-
* How long a party-run relay keeps a connection it admitted only for relay use
|
|
516
|
-
* (`membership-connection-gater.ts`'s `'admit-for-relay'`) before closing it, unless a
|
|
517
|
-
* reservation is admitted on it first: {@link RELAY_RESERVE_REQUEST_ROUND_TRIPS} at the declared
|
|
518
|
-
* link round trip, plus one {@link ADMISSION_DECISION_TIMEOUT_MS} for the relay's own decision on
|
|
519
|
-
* the reservation (`denyInboundRelayReservation`), which disarms the timer only once it settles.
|
|
520
|
-
* 3 500 + 2 000 = 5 500 ms at the default declaration.
|
|
521
|
-
*
|
|
522
|
-
* The declaration is the RELAY's own, because the relay is the machine that decides.
|
|
523
|
-
*
|
|
524
|
-
* What it costs, against the fixed 5 000 ms it replaced: a stranger that connects and never
|
|
525
|
-
* reserves holds a mute connection 0.5 s longer at the default declaration.
|
|
526
|
-
*/
|
|
527
|
-
export function relayAdmissionReserveDeadlineMs(linkRoundTripMs?: number): number {
|
|
528
|
-
return RELAY_RESERVE_REQUEST_ROUND_TRIPS * resolveLinkRoundTripMs(linkRoundTripMs) + ADMISSION_DECISION_TIMEOUT_MS;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
511
|
/**
|
|
532
512
|
* Deadline for one request and its answer over a circuit that is already open —
|
|
533
513
|
* {@link CIRCUIT_REQUEST_ROUND_TRIPS} at the declared link round trip, plus `transferAllowanceMs`
|
|
@@ -549,6 +529,10 @@ export function circuitRequestBudgetMs(
|
|
|
549
529
|
*
|
|
550
530
|
* No transfer allowance, unlike {@link circuitRequestBudgetMs}: the requests this bounds are a few
|
|
551
531
|
* hundred bytes to a few KB, so their bytes cost nothing a round trip does not already cover.
|
|
532
|
+
*
|
|
533
|
+
* A control node also keeps a connection it admitted provisionally this long, at its own
|
|
534
|
+
* declaration, because a cadre invitation redemption is bounded by this budget on the device's
|
|
535
|
+
* side (`membership-connection-gater.ts`, `PROVISIONAL_ADMISSION_DEADLINE_MS`).
|
|
552
536
|
*/
|
|
553
537
|
export function relayedRequestBudgetMs(linkRoundTripMs?: number): number {
|
|
554
538
|
return relayedDialBudgetMs(linkRoundTripMs) + CIRCUIT_REQUEST_ROUND_TRIPS * resolveLinkRoundTripMs(linkRoundTripMs);
|