@cello-protocol/daemon 0.0.54 → 0.0.55
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/daemon.d.ts +5 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +23 -11
- package/dist/daemon.js.map +1 -1
- package/dist/session-connection-gater.d.ts +4 -2
- package/dist/session-connection-gater.d.ts.map +1 -1
- package/dist/session-connection-gater.js +16 -10
- package/dist/session-connection-gater.js.map +1 -1
- package/dist/session-node-manager.d.ts +21 -0
- package/dist/session-node-manager.d.ts.map +1 -1
- package/dist/session-node-manager.js +182 -5
- package/dist/session-node-manager.js.map +1 -1
- package/dist/signaling-connect.d.ts +11 -0
- package/dist/signaling-connect.d.ts.map +1 -1
- package/dist/signaling-connect.js +48 -1
- package/dist/signaling-connect.js.map +1 -1
- package/package.json +3 -3
|
@@ -35,7 +35,8 @@ import { CELLO_CONTENT_PROTOCOL_ID, NodeAutoNatService } from "@cello-protocol/t
|
|
|
35
35
|
import { verify } from "@cello-protocol/crypto";
|
|
36
36
|
import { encodeSealPayload, MONIKER_RE, validateMoniker } from "@cello-protocol/protocol-types";
|
|
37
37
|
import { decodeParkEnvelope, authenticateParkedEntry, pubkeyMatchesHex } from "./park-envelope.js";
|
|
38
|
-
import {
|
|
38
|
+
import { isValidMultiaddr } from "@cello-protocol/transport";
|
|
39
|
+
import { AgentRelayClient, LEAF_KIND_CTRL, extractErrorMessage } from "./session-relay-client.js";
|
|
39
40
|
import { RelayReceiptStore } from "./relay-receipt-store.js";
|
|
40
41
|
import { SessionSealLeafStore } from "./session-seal-leaf-store.js";
|
|
41
42
|
import { PassthroughGatewayClient, GATEWAY_UNAVAILABLE, GOVERNANCE_TIMEOUT, } from "@cello-protocol/gateway";
|
|
@@ -2455,6 +2456,15 @@ export class SessionNodeManager {
|
|
|
2455
2456
|
if (addrs.length === 0) {
|
|
2456
2457
|
return { ok: false, reason: "no_counterparty_addrs", error: "the assignment carried no counterparty session addrs to dial" };
|
|
2457
2458
|
}
|
|
2459
|
+
// DOD-NAT-REACHABILITY-1: a /p2p-circuit counterparty address is dialed
|
|
2460
|
+
// THROUGH its relay, so the gater must admit that relay peer OUTBOUND. The
|
|
2461
|
+
// relay id is embedded in the address, which arrived inside the FROST-signed
|
|
2462
|
+
// assignment — the same authorization rail as the assigned witness relay.
|
|
2463
|
+
for (const addr of addrs) {
|
|
2464
|
+
const viaRelay = addr.match(/\/p2p\/([^/]+)\/p2p-circuit/);
|
|
2465
|
+
if (viaRelay)
|
|
2466
|
+
entry.gater.setAllowedOutboundPeer(viaRelay[1]);
|
|
2467
|
+
}
|
|
2458
2468
|
let lastError = "";
|
|
2459
2469
|
for (const addr of addrs) {
|
|
2460
2470
|
try {
|
|
@@ -2467,8 +2477,11 @@ export class SessionNodeManager {
|
|
|
2467
2477
|
return { ok: true };
|
|
2468
2478
|
}
|
|
2469
2479
|
catch (err) {
|
|
2470
|
-
//
|
|
2471
|
-
|
|
2480
|
+
// extractErrorMessage handles the transport's structured plain-object
|
|
2481
|
+
// throws (dial() never throws Error instances) — the old
|
|
2482
|
+
// `instanceof Error` idiom logged "[object Object]" on every dial
|
|
2483
|
+
// failure; try the next addr.
|
|
2484
|
+
lastError = extractErrorMessage(err);
|
|
2472
2485
|
}
|
|
2473
2486
|
}
|
|
2474
2487
|
this.#logger.warn("session.transport.connect.failed", {
|
|
@@ -3871,6 +3884,131 @@ export class SessionNodeManager {
|
|
|
3871
3884
|
this.#standingReceiverCreating.delete(agentName);
|
|
3872
3885
|
}
|
|
3873
3886
|
}
|
|
3887
|
+
/**
|
|
3888
|
+
* DOD-NAT-REACHABILITY-1 (Phase 2): relay endpoints the DIRECTORY handed this
|
|
3889
|
+
* agent at signaling-auth time — the freshest, health-filtered view of the
|
|
3890
|
+
* relay pool, and the only source a FRESH agent (no session history) has.
|
|
3891
|
+
*/
|
|
3892
|
+
#directoryRelayEndpoints = new Map();
|
|
3893
|
+
/**
|
|
3894
|
+
* DOD-NAT-REACHABILITY-1 (Phase 2): accept the directory's relay-pool endpoints
|
|
3895
|
+
* for an agent (arrives with signaling_auth_ok, i.e. on every connect AND every
|
|
3896
|
+
* reconnect). If the agent's standing receiver is up but holds NO reservation —
|
|
3897
|
+
* the agent-online ensure raced ahead of auth_ok, or every relay was down at
|
|
3898
|
+
* create time — rebuild it now so the agent becomes dialable without waiting
|
|
3899
|
+
* for a session handoff that (being unreachable) would never come.
|
|
3900
|
+
*/
|
|
3901
|
+
setDirectoryRelayEndpoints(agentName, endpoints) {
|
|
3902
|
+
this.#directoryRelayEndpoints.set(agentName, endpoints);
|
|
3903
|
+
if (endpoints.length === 0 || this.#shuttingDown)
|
|
3904
|
+
return;
|
|
3905
|
+
const sr = this.#standingReceivers.get(agentName);
|
|
3906
|
+
if (!sr)
|
|
3907
|
+
return; // not ensured yet — the coming ensure reads the map
|
|
3908
|
+
if (sr.node.listenAddresses().some((a) => a.includes("/p2p-circuit")))
|
|
3909
|
+
return; // already reserved
|
|
3910
|
+
this.#logger.info("session.standing_receiver.reservation.rebuild", {
|
|
3911
|
+
agentName,
|
|
3912
|
+
relayPeerIds: endpoints.map((e) => e.relayPeerId),
|
|
3913
|
+
});
|
|
3914
|
+
void this.#rebuildStandingReceiver(agentName);
|
|
3915
|
+
}
|
|
3916
|
+
/**
|
|
3917
|
+
* Replace an agent's reservation-less standing receiver with one that reserves.
|
|
3918
|
+
*
|
|
3919
|
+
* Deliberately NOT removeStandingReceiverForAgent()+ensureStandingReceiverForAgent():
|
|
3920
|
+
* the public remove CLEARS #agentsWantingReceiver, so a cello_stop_agent landing in
|
|
3921
|
+
* the window while node.stop() is awaited would find no map entry and no creating
|
|
3922
|
+
* marker, leave no tombstone, and the re-ensure would then RESURRECT a receiver for
|
|
3923
|
+
* an agent that asked to go dark — accepting inbound sessions for an offline agent.
|
|
3924
|
+
* Here the want-flag is left intact and re-checked after the stop: a concurrent stop
|
|
3925
|
+
* clears it, and the rebuild correctly no-ops.
|
|
3926
|
+
*/
|
|
3927
|
+
async #rebuildStandingReceiver(agentName) {
|
|
3928
|
+
try {
|
|
3929
|
+
const sr = this.#standingReceivers.get(agentName);
|
|
3930
|
+
if (sr) {
|
|
3931
|
+
this.#standingReceivers.delete(agentName);
|
|
3932
|
+
try {
|
|
3933
|
+
sr.autoNat.stop();
|
|
3934
|
+
await sr.node.stop();
|
|
3935
|
+
}
|
|
3936
|
+
catch (err) {
|
|
3937
|
+
this.#logger.warn("session.standing_receiver.teardown.failed", {
|
|
3938
|
+
agentName,
|
|
3939
|
+
error: extractErrorMessage(err),
|
|
3940
|
+
});
|
|
3941
|
+
}
|
|
3942
|
+
}
|
|
3943
|
+
// The agent may have gone offline while we were stopping the old node. Its
|
|
3944
|
+
// want-flag is the authority — never resurrect a receiver it disowned.
|
|
3945
|
+
if (!this.#agentsWantingReceiver.has(agentName) || this.#shuttingDown)
|
|
3946
|
+
return;
|
|
3947
|
+
await this.#ensureStandingReceiver(agentName);
|
|
3948
|
+
}
|
|
3949
|
+
catch (err) {
|
|
3950
|
+
this.#logger.warn("session.standing_receiver.reservation.rebuild.failed", {
|
|
3951
|
+
agentName,
|
|
3952
|
+
error: extractErrorMessage(err),
|
|
3953
|
+
});
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
3956
|
+
/**
|
|
3957
|
+
* DOD-NAT-REACHABILITY-1: circuit-relay listen addresses for an agent's known
|
|
3958
|
+
* relays, so its standing receiver takes reservations and becomes dialable
|
|
3959
|
+
* behind NAT. Sources, merged and deduped by relay peer id: the directory's
|
|
3960
|
+
* auth-time relay pool (freshest — first), then the persisted relay endpoints
|
|
3961
|
+
* of past sessions (getAgentRelayEndpoints — covers a directory that predates
|
|
3962
|
+
* the auth_ok extension).
|
|
3963
|
+
*/
|
|
3964
|
+
#reservationCircuitAddrs(agentName) {
|
|
3965
|
+
let persisted;
|
|
3966
|
+
try {
|
|
3967
|
+
persisted = this.getAgentRelayEndpoints(agentName);
|
|
3968
|
+
}
|
|
3969
|
+
catch (err) {
|
|
3970
|
+
// No DB / unknown agent — persisted source unavailable. The directory
|
|
3971
|
+
// source may still serve; reachability degrades only if both are empty.
|
|
3972
|
+
// Logged (not swallowed): a genuine DB failure must be distinguishable
|
|
3973
|
+
// from "fresh agent, no history" in the reachability trail.
|
|
3974
|
+
this.#logger.debug("session.standing_receiver.persisted_relays.unavailable", {
|
|
3975
|
+
agentName,
|
|
3976
|
+
error: extractErrorMessage(err),
|
|
3977
|
+
});
|
|
3978
|
+
persisted = [];
|
|
3979
|
+
}
|
|
3980
|
+
const merged = new Map();
|
|
3981
|
+
for (const ep of [...(this.#directoryRelayEndpoints.get(agentName) ?? []), ...persisted]) {
|
|
3982
|
+
if (!merged.has(ep.relayPeerId))
|
|
3983
|
+
merged.set(ep.relayPeerId, ep);
|
|
3984
|
+
}
|
|
3985
|
+
const addrs = [];
|
|
3986
|
+
const relayPeerIds = [];
|
|
3987
|
+
for (const ep of merged.values()) {
|
|
3988
|
+
const base = ep.relayAddrs[0];
|
|
3989
|
+
if (!base)
|
|
3990
|
+
continue;
|
|
3991
|
+
const candidate = base.includes("/p2p/")
|
|
3992
|
+
? `${base}/p2p-circuit`
|
|
3993
|
+
: `${base}/p2p/${ep.relayPeerId}/p2p-circuit`;
|
|
3994
|
+
// These addresses are built from DIRECTORY-supplied endpoints — data from off
|
|
3995
|
+
// this machine. A malformed one throws inside libp2p node construction, which
|
|
3996
|
+
// would take the standing receiver down entirely and leave the agent deaf to
|
|
3997
|
+
// ALL inbound (worse than the defect this fixes). A bad endpoint must cost one
|
|
3998
|
+
// relay, never the receiver.
|
|
3999
|
+
if (!isValidMultiaddr(candidate)) {
|
|
4000
|
+
this.#logger.warn("session.standing_receiver.relay_endpoint.invalid", {
|
|
4001
|
+
agentName,
|
|
4002
|
+
relayPeerId: ep.relayPeerId,
|
|
4003
|
+
addr: candidate,
|
|
4004
|
+
});
|
|
4005
|
+
continue;
|
|
4006
|
+
}
|
|
4007
|
+
addrs.push(candidate);
|
|
4008
|
+
relayPeerIds.push(ep.relayPeerId);
|
|
4009
|
+
}
|
|
4010
|
+
return { addrs, relayPeerIds };
|
|
4011
|
+
}
|
|
3874
4012
|
/** One standing-receiver create attempt (extracted for the M8B F14 retry loop). */
|
|
3875
4013
|
async #tryCreateStandingReceiver(agentName, correlationId) {
|
|
3876
4014
|
const sessionId = `standing_receiver_${randomUUID()}`;
|
|
@@ -3879,13 +4017,29 @@ export class SessionNodeManager {
|
|
|
3879
4017
|
allowedPeerId: null, // open — counterparty unknown at creation time
|
|
3880
4018
|
logger: this.#logger,
|
|
3881
4019
|
});
|
|
4020
|
+
// DOD-NAT-REACHABILITY-1: reserve with the agent's known relays. The relay
|
|
4021
|
+
// peers are allowed OUTBOUND on the gater up front, so reservation refreshes
|
|
4022
|
+
// keep working after the receiver is claimed and setAllowedPeer() narrows
|
|
4023
|
+
// the inbound gate to the session counterparty.
|
|
4024
|
+
const reservations = this.#reservationCircuitAddrs(agentName);
|
|
4025
|
+
for (const relayPeerId of reservations.relayPeerIds) {
|
|
4026
|
+
gater.setAllowedOutboundPeer(relayPeerId);
|
|
4027
|
+
}
|
|
3882
4028
|
let node;
|
|
3883
4029
|
try {
|
|
3884
|
-
node = await this.#factory.createNode({
|
|
4030
|
+
node = await this.#factory.createNode({
|
|
4031
|
+
sessionId,
|
|
4032
|
+
connectionGater: gater,
|
|
4033
|
+
nodeType: "standing_receiver",
|
|
4034
|
+
...(reservations.addrs.length > 0 ? { circuitRelayListenAddrs: reservations.addrs } : {}),
|
|
4035
|
+
});
|
|
3885
4036
|
await node.start();
|
|
3886
4037
|
}
|
|
3887
4038
|
catch (err) {
|
|
3888
|
-
|
|
4039
|
+
// extractErrorMessage, NOT String(err): the transport throws structured
|
|
4040
|
+
// plain objects ({ reason, message }), and String() destroys both into
|
|
4041
|
+
// "[object Object]" — the loud failure must carry its cause.
|
|
4042
|
+
const error = extractErrorMessage(err);
|
|
3889
4043
|
this.#logger.error("session.node.create.failed", {
|
|
3890
4044
|
sessionId,
|
|
3891
4045
|
agentName: `${STANDING_RECEIVER_AGENT_NAME}:${agentName}`,
|
|
@@ -3931,6 +4085,25 @@ export class SessionNodeManager {
|
|
|
3931
4085
|
sessionPeerId: node.getPeerId(),
|
|
3932
4086
|
correlationId,
|
|
3933
4087
|
});
|
|
4088
|
+
// DOD-NAT-REACHABILITY-1 observability: how reachable did this receiver come
|
|
4089
|
+
// up? circuitAddrs === 0 with reservations requested means every relay
|
|
4090
|
+
// refused/was unreachable — the agent is deaf to NAT'd initiators (public
|
|
4091
|
+
// ones can still connect directly). That must be LOUD, not a quiet shrug.
|
|
4092
|
+
const circuitAddrs = node.listenAddresses().filter((a) => a.includes("/p2p-circuit")).length;
|
|
4093
|
+
this.#logger.info("session.standing_receiver.reachability", {
|
|
4094
|
+
agentName,
|
|
4095
|
+
circuitAddrs,
|
|
4096
|
+
reservationsRequested: reservations.addrs.length,
|
|
4097
|
+
correlationId,
|
|
4098
|
+
});
|
|
4099
|
+
if (reservations.addrs.length > 0 && circuitAddrs === 0) {
|
|
4100
|
+
this.#logger.warn("session.standing_receiver.reservation.none", {
|
|
4101
|
+
agentName,
|
|
4102
|
+
reservationsRequested: reservations.addrs.length,
|
|
4103
|
+
relayPeerIds: reservations.relayPeerIds,
|
|
4104
|
+
correlationId,
|
|
4105
|
+
});
|
|
4106
|
+
}
|
|
3934
4107
|
return { outcome: "installed" };
|
|
3935
4108
|
}
|
|
3936
4109
|
/**
|
|
@@ -3946,6 +4119,10 @@ export class SessionNodeManager {
|
|
|
3946
4119
|
async removeStandingReceiverForAgent(agentName) {
|
|
3947
4120
|
// M8B F14: the agent no longer wants a receiver — disarm the teardown re-arm.
|
|
3948
4121
|
this.#agentsWantingReceiver.delete(agentName);
|
|
4122
|
+
// The directory hands these out at signaling-auth time, so a re-started agent
|
|
4123
|
+
// gets a fresh set on its next connect — holding the old ones would keep a
|
|
4124
|
+
// retired agent's relay list alive for the daemon's lifetime.
|
|
4125
|
+
this.#directoryRelayEndpoints.delete(agentName);
|
|
3949
4126
|
const sr = this.#standingReceivers.get(agentName);
|
|
3950
4127
|
if (!sr) {
|
|
3951
4128
|
// L1: an #ensureStandingReceiver for this agent may be in flight (parked on start(), so no
|