@cello-protocol/daemon 0.0.190 → 0.0.191
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.js +5 -1
- package/dist/daemon.js.map +1 -1
- package/dist/park-envelope.d.ts +58 -1
- package/dist/park-envelope.d.ts.map +1 -1
- package/dist/park-envelope.js +61 -1
- package/dist/park-envelope.js.map +1 -1
- package/dist/session-node-manager.d.ts +2 -0
- package/dist/session-node-manager.d.ts.map +1 -1
- package/dist/session-node-manager.js +188 -5
- package/dist/session-node-manager.js.map +1 -1
- package/dist/session-relay-client.d.ts +42 -1
- package/dist/session-relay-client.d.ts.map +1 -1
- package/dist/session-relay-client.js +142 -14
- package/dist/session-relay-client.js.map +1 -1
- package/package.json +5 -5
|
@@ -1887,7 +1887,7 @@ export class SessionNodeManager {
|
|
|
1887
1887
|
* when its value is `undefined` — forces each of the three callers to state what this message was
|
|
1888
1888
|
* hashed under, so a new fourth caller cannot omit it by accident.
|
|
1889
1889
|
*/
|
|
1890
|
-
async #parkContent(agentName, sessionId, contentHashHex, content, structure1Cbor, structure2Cbor, contentHashAlg) {
|
|
1890
|
+
async #parkContent(agentName, sessionId, contentHashHex, content, structure1Cbor, structure2Cbor, contentHashAlg, structure1Signature, parkLeafKind) {
|
|
1891
1891
|
// Fault injection FIRST, so it reproduces the real shape: the refusal happens at the same point
|
|
1892
1892
|
// the live hook refuses (before any deposit), with the same event and the same `cause`.
|
|
1893
1893
|
if (this.#parkFaultRemaining > 0) {
|
|
@@ -1923,6 +1923,11 @@ export class SessionNodeManager {
|
|
|
1923
1923
|
// on recover too (sealed INTO the ciphertext envelope — INV-3: the relay still sees only ciphertext).
|
|
1924
1924
|
structure1Cbor,
|
|
1925
1925
|
structure2Cbor,
|
|
1926
|
+
// 034-CARRYLEAF: the author's signature over `structure1Cbor`, so the RECIPIENT can witness
|
|
1927
|
+
// this leaf if its author never does. Without it the mailbox route stays truncatable.
|
|
1928
|
+
structure1Signature,
|
|
1929
|
+
// 034-CARRYLEAF: the leaf DOMAIN, so a recovered leaf is never witnessed under a guess.
|
|
1930
|
+
leafKind: parkLeafKind,
|
|
1926
1931
|
// B2b: the park route must name the same algorithm the direct frame did, or the recipient
|
|
1927
1932
|
// verifies the same message two different ways depending on which route it took.
|
|
1928
1933
|
contentHashAlg,
|
|
@@ -7935,6 +7940,16 @@ export class SessionNodeManager {
|
|
|
7935
7940
|
// `finally` retires — same defect, other end, other cap (64 outbound per protocol per
|
|
7936
7941
|
// connection). See the note on #handleContentStream's finally.
|
|
7937
7942
|
let sendStream;
|
|
7943
|
+
/**
|
|
7944
|
+
* 034-CARRYLEAF — HOISTED OUT OF THE TRY so the PARK path in the catch can carry them.
|
|
7945
|
+
*
|
|
7946
|
+
* The park copy is built when the direct send fails, which is inside the catch below; declaring
|
|
7947
|
+
* these in the try left the parked envelope with only the relay-WITNESSED claim, so a message
|
|
7948
|
+
* its author deliberately did not witness was parked with no ordering claim at all and could
|
|
7949
|
+
* never be witnessed by its recipient.
|
|
7950
|
+
*/
|
|
7951
|
+
let frameS1;
|
|
7952
|
+
let frameSig;
|
|
7938
7953
|
try {
|
|
7939
7954
|
/**
|
|
7940
7955
|
* ─── EVERY FRAME CARRIES ITS OWN PROOF — `DOD-M15-AUTHORSHIP-ABSENT-1` ────────────────────
|
|
@@ -7958,8 +7973,8 @@ export class SessionNodeManager {
|
|
|
7958
7973
|
* widened that window enough to break the live two-node round trip. Measured, not reasoned
|
|
7959
7974
|
* about: seam-3 went red and both daemons logged `session.key.agreed` before the refusal.
|
|
7960
7975
|
*/
|
|
7961
|
-
|
|
7962
|
-
|
|
7976
|
+
frameS1 = orderingS1;
|
|
7977
|
+
frameSig = orderingSig;
|
|
7963
7978
|
let frameS2 = orderingS2;
|
|
7964
7979
|
if (frameS1 === undefined || frameSig === undefined) {
|
|
7965
7980
|
const own = await this.#signOwnContentClaim(agentName, sessionId, entry, contentHash);
|
|
@@ -8097,6 +8112,21 @@ export class SessionNodeManager {
|
|
|
8097
8112
|
// unknown CBOR key, so emitting it is safe for every build in existence; a newer one reads
|
|
8098
8113
|
// it and verifies under the named algorithm instead of assuming.
|
|
8099
8114
|
content_hash_alg: contentHashAlg,
|
|
8115
|
+
/**
|
|
8116
|
+
* 034-CARRYLEAF review F5 — WHICH LEAF DOMAIN this content belongs to.
|
|
8117
|
+
*
|
|
8118
|
+
* Documents and rejection envelopes ride this same frame, and the receiver had no way to
|
|
8119
|
+
* recover their kind: it appended them locally as "doc" from its own inspection of the
|
|
8120
|
+
* body, while anything it witnessed on the sender's behalf went to the relay as a MESSAGE
|
|
8121
|
+
* leaf. The certified root is over content hashes, so no root diverges — but the relay's
|
|
8122
|
+
* canonical log and the carried `leaf_kind` would describe the leaf as something it is not,
|
|
8123
|
+
* and a leaf kind selects a HASH DOMAIN everywhere else in this protocol.
|
|
8124
|
+
*
|
|
8125
|
+
* Same argument as `content_hash_alg` beside it: an older peer ignores an unknown CBOR key,
|
|
8126
|
+
* so emitting it is safe for every build in existence, and a receiver that does not see it
|
|
8127
|
+
* declines to witness rather than guessing (see `#witnessReceivedLeaf`).
|
|
8128
|
+
*/
|
|
8129
|
+
leaf_kind: leafKind,
|
|
8100
8130
|
});
|
|
8101
8131
|
// Injected dial failure — thrown from inside the try so it lands in exactly the catch the
|
|
8102
8132
|
// real connection_lost lands in, and the whole downstream path (untrack → park → durable
|
|
@@ -8175,7 +8205,21 @@ export class SessionNodeManager {
|
|
|
8175
8205
|
...this.#streamCensus(entry.node, entry.counterpartySessionPeerId),
|
|
8176
8206
|
correlationId,
|
|
8177
8207
|
});
|
|
8178
|
-
|
|
8208
|
+
/**
|
|
8209
|
+
* ⚠️ **THE PARKED COPY CARRIES THE SIGNED CLAIM, NOT ONLY THE WITNESSED ONE — 034-CARRYLEAF
|
|
8210
|
+
* review F1, and this is what closes the withholding attack on the MAILBOX route.**
|
|
8211
|
+
*
|
|
8212
|
+
* It passed `orderingS1`, which exists only when the relay witnessed this leaf — so a message
|
|
8213
|
+
* the sender deliberately did not witness was parked with no ordering claim at all, and the
|
|
8214
|
+
* recipient recovered it holding nothing the relay would accept as proof of authorship. They
|
|
8215
|
+
* could read it and could never put it in a receipt.
|
|
8216
|
+
*
|
|
8217
|
+
* `frameS1`/`frameSig` are the pair the DIRECT frame carries for this same message: the
|
|
8218
|
+
* relay's witnessed claim when there is one, and otherwise a claim this agent signed itself.
|
|
8219
|
+
* Either way it is the author's signature over the author's own ordering claim, which is the
|
|
8220
|
+
* only form the relay accepts when the recipient witnesses on their behalf.
|
|
8221
|
+
*/
|
|
8222
|
+
const attempt = await this.#parkContent(agentName, sessionId, hashHex, content, frameS1, orderingS2, contentHashAlg, frameSig, leafKind);
|
|
8179
8223
|
if (attempt.outcome === "parked") {
|
|
8180
8224
|
this.#noteImpairmentRetention(agentName, sessionId, "parked");
|
|
8181
8225
|
return { ok: true, delivered: false, parked: true, ...(assignedSeq === undefined ? {} : { sequenceNumber: assignedSeq }), ...(sentAuthorship === undefined ? {} : { authorship: sentAuthorship }), ...(relayRefusal === undefined ? {} : { relayRefusal }) };
|
|
@@ -12235,6 +12279,32 @@ export class SessionNodeManager {
|
|
|
12235
12279
|
* wired, this line is the difference between an all-clear and a permanent silent discard — and
|
|
12236
12280
|
* finding that then costs more than the clause costs now.
|
|
12237
12281
|
*/
|
|
12282
|
+
/**
|
|
12283
|
+
* ─── WITNESS A RECOVERED MESSAGE ITS SENDER NEVER WITNESSED — 034-CARRYLEAF review F1 ────────
|
|
12284
|
+
*
|
|
12285
|
+
* The mailbox route's half of the withholding fix. `recoveredSeq` absent means no relay ordering
|
|
12286
|
+
* record came with it, which is the same shape the direct path treats as "their submit never
|
|
12287
|
+
* happened" — and it is reached the same two ways: their relay was briefly unreachable, or they
|
|
12288
|
+
* are withholding on purpose.
|
|
12289
|
+
*
|
|
12290
|
+
* ⚠️ THE SIGNATURE COMES FROM THE ENVELOPE, AND ONLY A v4 ENVELOPE HAS ONE. `parkSig`
|
|
12291
|
+
* authenticates the DEPOSIT — it signs `(session_id, recipient_pubkey, content_hash)` — and the
|
|
12292
|
+
* relay will not accept it, because a counter-submit is admissible only against the author's own
|
|
12293
|
+
* signature over their own ordering claim. A v2 or v3 envelope therefore cannot be witnessed on
|
|
12294
|
+
* its author's behalf, and is left alone rather than guessed at.
|
|
12295
|
+
*
|
|
12296
|
+
* **So this route is closed against a peer running a stock client, and open to one that
|
|
12297
|
+
* deliberately emits an older envelope.** Requiring v4 is the step that closes it completely,
|
|
12298
|
+
* and it waits on nothing in the field emitting v2 or v3 — the same tolerate-then-enforce
|
|
12299
|
+
* sequence every bilateral wire change in this milestone follows.
|
|
12300
|
+
*/
|
|
12301
|
+
if (result.ok && result.held !== true && result.screenedOut !== true &&
|
|
12302
|
+
recoveredSeq === null && env.structure1Cbor && env.structure1Signature) {
|
|
12303
|
+
const kind = env.leafKind;
|
|
12304
|
+
if (typeof kind === "number") {
|
|
12305
|
+
this.#witnessReceivedLeaf(agentName, sessionId, contentHash, env.structure1Cbor, env.structure1Signature, kind, correlationId);
|
|
12306
|
+
}
|
|
12307
|
+
}
|
|
12238
12308
|
if (priorDeclaredAlg !== undefined && result.ok && result.held !== true && result.screenedOut !== true) {
|
|
12239
12309
|
// Cleared ONLY on a real reconciliation. Clearing on the lookup (as this did) forgets the
|
|
12240
12310
|
// refusal even when the recovery fails, so the next genuine reconciliation says nothing.
|
|
@@ -12937,6 +13007,81 @@ export class SessionNodeManager {
|
|
|
12937
13007
|
this.#sessionContentKeys.set(this.#k(agentName, sessionId), Uint8Array.from(key));
|
|
12938
13008
|
this.#contentEncryptionReasons.delete(this.#k(agentName, sessionId));
|
|
12939
13009
|
}
|
|
13010
|
+
/**
|
|
13011
|
+
* Hand the relay a leaf this agent RECEIVED whose author never submitted it — 034-CARRYLEAF.
|
|
13012
|
+
*
|
|
13013
|
+
* **The attack this closes:** somebody sends you something, declines to have it witnessed, and
|
|
13014
|
+
* seals one message short. The relay's account really does end before their last message, so your
|
|
13015
|
+
* receipt does too — every leaf validly signed, nothing false, the last thing said simply absent.
|
|
13016
|
+
*
|
|
13017
|
+
* **Why this is admissible and not a forgery:** the bytes are theirs, the signature over them is
|
|
13018
|
+
* theirs, and `#verifyAuthorshipClaim` verified it against this session's counterparty before a
|
|
13019
|
+
* word of it was ingested. The relay verifies it again against the directory-signed assignment.
|
|
13020
|
+
* Nothing here is asserted by us except that we received it.
|
|
13021
|
+
*
|
|
13022
|
+
* ⚠️ BEST-EFFORT, AND ITS FAILURE IS NOT SILENT. If the relay cannot be reached, the message is
|
|
13023
|
+
* still delivered and read — refusing it would make the relay a precondition for reading mail,
|
|
13024
|
+
* which is the thing every unit on this path has been careful not to do. What is lost is only the
|
|
13025
|
+
* guarantee that it can enter a receipt, and that surfaces where the operator can act on it: the
|
|
13026
|
+
* seal's own pre-flight refuses a gapped chain by name (`seal_carry_noncontiguous`) with guidance,
|
|
13027
|
+
* so the consequence reaches them at the moment it matters rather than as a log line here.
|
|
13028
|
+
*/
|
|
13029
|
+
#witnessReceivedLeaf(agentName, sessionId, contentHash, structure1Cbor, senderSignature,
|
|
13030
|
+
/** The domain the AUTHOR assigned this leaf, read off their frame — never guessed (review F5). */
|
|
13031
|
+
leafKind, correlationId) {
|
|
13032
|
+
const entry = this.#activeNodes.get(this.#k(agentName, sessionId));
|
|
13033
|
+
if (!entry?.relayClient || !entry.relaySessionIdBytes) {
|
|
13034
|
+
this.#logger.warn("session.content.witness_received.unavailable", {
|
|
13035
|
+
agentName, sessionId, correlationId,
|
|
13036
|
+
impact: "a message arrived that its sender never had witnessed, and this side has no relay client " +
|
|
13037
|
+
"for the session, so it could not be witnessed here either. It is delivered and readable; " +
|
|
13038
|
+
"it cannot enter a notarized receipt until some party witnesses it.",
|
|
13039
|
+
});
|
|
13040
|
+
return;
|
|
13041
|
+
}
|
|
13042
|
+
void entry.relayClient
|
|
13043
|
+
.witnessReceivedLeaf(entry.node, entry.relaySessionIdBytes, contentHash, leafKind, {
|
|
13044
|
+
structure1Cbor,
|
|
13045
|
+
senderSignature,
|
|
13046
|
+
})
|
|
13047
|
+
.then((res) => {
|
|
13048
|
+
if (res.ok) {
|
|
13049
|
+
this.#logger.info("session.content.witness_received", {
|
|
13050
|
+
agentName, sessionId, correlationId, relaySequence: res.sequence_number,
|
|
13051
|
+
impact: "this side witnessed a message its SENDER did not. The leaf now holds a canonical " +
|
|
13052
|
+
"position, so it can appear in a receipt whatever the sender does next.",
|
|
13053
|
+
});
|
|
13054
|
+
// It has a position now, so it can be acknowledged like any other received message.
|
|
13055
|
+
this.#noteAcknowledgeable(agentName, sessionId, res.sequence_number - 1, contentHash);
|
|
13056
|
+
return;
|
|
13057
|
+
}
|
|
13058
|
+
/**
|
|
13059
|
+
* `counter_submit_duplicate` is NOT a failure and must not be logged as one: it means this
|
|
13060
|
+
* relay already holds the leaf, which is the outcome we wanted. It fires on the ordinary
|
|
13061
|
+
* race where the sender's own submit lands while ours is in flight.
|
|
13062
|
+
*/
|
|
13063
|
+
if (res.reason === "counter_submit_duplicate") {
|
|
13064
|
+
this.#logger.info("session.content.witness_received.already_held", {
|
|
13065
|
+
agentName, sessionId, correlationId,
|
|
13066
|
+
impact: "the relay already held this leaf — its sender witnessed it after all, or in parallel with us.",
|
|
13067
|
+
});
|
|
13068
|
+
return;
|
|
13069
|
+
}
|
|
13070
|
+
this.#logger.error("session.content.witness_received.failed", {
|
|
13071
|
+
agentName, sessionId, correlationId, reason: res.reason,
|
|
13072
|
+
...(res.detail === undefined ? {} : { detail: res.detail }),
|
|
13073
|
+
impact: "a message arrived that its sender never had witnessed, and this side could not witness " +
|
|
13074
|
+
"it either. It is delivered and readable. What is at risk is the RECEIPT: if this " +
|
|
13075
|
+
"message is still unwitnessed when the conversation is sealed, the seal will refuse a " +
|
|
13076
|
+
"gapped chain by name rather than quietly leaving it out.",
|
|
13077
|
+
});
|
|
13078
|
+
})
|
|
13079
|
+
.catch((err) => {
|
|
13080
|
+
this.#logger.error("session.content.witness_received.threw", {
|
|
13081
|
+
agentName, sessionId, correlationId, error: extractErrorMessage(err),
|
|
13082
|
+
});
|
|
13083
|
+
});
|
|
13084
|
+
}
|
|
12940
13085
|
/**
|
|
12941
13086
|
* Record that a message ARRIVED and was accepted at a known canonical position — 033-ACKEMIT
|
|
12942
13087
|
* review F1.
|
|
@@ -15645,8 +15790,46 @@ export class SessionNodeManager {
|
|
|
15645
15790
|
* whatever we hold of the content. That limit is structural to a (position, content) pair
|
|
15646
15791
|
* and it is what the carried-leaf follow-on closes.
|
|
15647
15792
|
*/
|
|
15648
|
-
if (framedSeq !== null)
|
|
15793
|
+
if (framedSeq !== null) {
|
|
15649
15794
|
this.#noteAcknowledgeable(agentName, sessionId, framedSeq, contentHash);
|
|
15795
|
+
}
|
|
15796
|
+
else {
|
|
15797
|
+
/**
|
|
15798
|
+
* ─── WITNESS WHAT THEY DID NOT — 034-CARRYLEAF, and this is the line that closes
|
|
15799
|
+
* `DOD-M15-WITHHOLD-SEAL-1` ────────────────────────────────────────────────────────────
|
|
15800
|
+
*
|
|
15801
|
+
* No ordering record means the sender never asked the relay to witness this message. Two
|
|
15802
|
+
* things look identical from here: their relay was briefly unreachable, or they are
|
|
15803
|
+
* withholding it on purpose so it cannot appear in the receipt. **We do not need to tell
|
|
15804
|
+
* those apart, and that is the point** — the same action repairs both, and it costs the
|
|
15805
|
+
* honest case nothing.
|
|
15806
|
+
*
|
|
15807
|
+
* We hold their signature over their own bytes. So we hand it to the relay ourselves.
|
|
15808
|
+
*/
|
|
15809
|
+
/**
|
|
15810
|
+
* ⚠️ **THE KIND COMES OFF THE FRAME, AND WITHOUT IT WE DECLINE TO WITNESS — review F5.**
|
|
15811
|
+
*
|
|
15812
|
+
* A leaf kind selects a HASH DOMAIN, and documents and rejection envelopes ride this same
|
|
15813
|
+
* frame. Hardcoding `msg` meant a document witnessed on its author's behalf entered the
|
|
15814
|
+
* relay's canonical log described as something it is not. A peer too old to send the field
|
|
15815
|
+
* is left alone rather than guessed at: witnessing their leaf under the wrong domain would
|
|
15816
|
+
* be a worse outcome than not witnessing it, because it puts a wrong statement in the
|
|
15817
|
+
* record instead of leaving a gap the seal can name.
|
|
15818
|
+
*/
|
|
15819
|
+
const framedKind = frame["leaf_kind"];
|
|
15820
|
+
if (typeof framedKind === "number") {
|
|
15821
|
+
void this.#witnessReceivedLeaf(agentName, sessionId, contentHash, s1Cbor, senderSig, framedKind, correlationId);
|
|
15822
|
+
}
|
|
15823
|
+
else {
|
|
15824
|
+
this.#logger.info("session.content.witness_received.kind_unknown", {
|
|
15825
|
+
agentName, sessionId, correlationId,
|
|
15826
|
+
impact: "a message arrived that its sender never had witnessed, and the frame did not say " +
|
|
15827
|
+
"which leaf domain it belongs to — their build predates the field. It was NOT " +
|
|
15828
|
+
"witnessed on their behalf, because witnessing it under a guessed domain would put " +
|
|
15829
|
+
"a wrong statement in the record rather than leave a gap the seal can name.",
|
|
15830
|
+
});
|
|
15831
|
+
}
|
|
15832
|
+
}
|
|
15650
15833
|
void this.#sendDeliveryAck(agentName, sessionId, contentHash, correlationId);
|
|
15651
15834
|
}
|
|
15652
15835
|
}
|