@cello-protocol/daemon 0.0.49 → 0.0.50

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.
@@ -1 +1 @@
1
- {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,OAAO,KAAK,EACV,YAAY,EACZ,oBAAoB,EAQrB,MAAM,YAAY,CAAC;AAKpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAU/D,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAsB7G,OAAO,KAAK,EAAE,kBAAkB,EAA+C,MAAM,yBAAyB,CAAC;AAQ/G,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,2BAA2B,CAAC;AA0InF,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,SAAS,IAAI,oBAAoB,CAAC;IAClC;;;;OAIG;IACH,qBAAqB,IAAI,kBAAkB,CAAC;IAC5C;;;;;;;;OAQG;IACH,gBAAgB,IAAI,SAAS,GAAG,IAAI,CAAC;IACrC;;;;OAIG;IACH,oBAAoB,IAAI,kBAAkB,CAAC;IAC3C;;;OAGG;IACH,iBAAiB,IAAI,eAAe,CAAC;CACtC;AA2CD,eAAO,MAAM,sBAAsB,QAAsB,CAAC;AAE1D,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAkhM7E"}
1
+ {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,OAAO,KAAK,EACV,YAAY,EACZ,oBAAoB,EAQrB,MAAM,YAAY,CAAC;AAKpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAU/D,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAuB7G,OAAO,KAAK,EAAE,kBAAkB,EAA+C,MAAM,yBAAyB,CAAC;AAQ/G,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,2BAA2B,CAAC;AA0InF,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,SAAS,IAAI,oBAAoB,CAAC;IAClC;;;;OAIG;IACH,qBAAqB,IAAI,kBAAkB,CAAC;IAC5C;;;;;;;;OAQG;IACH,gBAAgB,IAAI,SAAS,GAAG,IAAI,CAAC;IACrC;;;;OAIG;IACH,oBAAoB,IAAI,kBAAkB,CAAC;IAC3C;;;OAGG;IACH,iBAAiB,IAAI,eAAe,CAAC;CACtC;AA2CD,eAAO,MAAM,sBAAsB,QAAsB,CAAC;AAE1D,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CA6kM7E"}
package/dist/daemon.js CHANGED
@@ -48,12 +48,13 @@ import { RegistrationManager } from "./registration-manager.js";
48
48
  import { DaemonRegistrationContext } from "./registration-context.js";
49
49
  import { DbRegistrationPersistence, DbIdentityStore } from "./db-identity-store.js";
50
50
  import { DbManifestVersionStore } from "./manifest-version-store-db.js";
51
- import { verify as ed25519Verify, sealToRecipient, generateKLocalSeed, InMemoryKeyProvider, hash as cryptoHash } from "@cello-protocol/crypto";
51
+ import { verify as ed25519Verify, generateKLocalSeed, InMemoryKeyProvider, hash as cryptoHash } from "@cello-protocol/crypto";
52
52
  import { attemptSealUpgrade as attemptSealUpgradeImpl, verifyUpgradeConfirmedCert } from "./seal-upgrade.js";
53
53
  import { upgradeAbsentToRecovered, hasAbsentParticipant } from "./seal-receipt-upgrade.js";
54
54
  // CELLO-M7-MSG-001 (AC-013/AC-018): the single application content-size cap, enforced
55
55
  // at the send point here (the receive point lives in the transport content decode).
56
56
  import { MAX_CONTENT_BYTES, computeGenesisPrevRoot, buildAgentRevocationTbs, MONIKER_RE, validateMoniker } from "@cello-protocol/protocol-types";
57
+ import { sealParkEnvelope } from "./park-envelope.js";
57
58
  import { resolveCelloEnv, createTransportSelector, isProductionVariant, } from "./transport-composition.js";
58
59
  import { selectAdvertisedAddress } from "./transport-selector.js";
59
60
  import { parseSessionAssignment, sessionRequestErrorReason, parseDiscoveryLookupResult, discoveryLookupErrorReason, extractOfferedMoniker, resolveOutboundMoniker } from "./session-assignment-parser.js";
@@ -1508,7 +1509,7 @@ export async function startDaemon(config) {
1508
1509
  // the session's relay endpoint; we seal the content to the recipient (E2E — the relay never sees
1509
1510
  // plaintext, INV-3) and deposit it to that relay's store-and-forward mailbox via the standing
1510
1511
  // receiver node. The recipient pulls + recovers it at the witnessed sequence (R1) on next online.
1511
- sessionNodeManager.setContentParkHook(async ({ sessionId, recipientPubkeyHex, relayPeerId, relayAddrs, contentHashHex, content, structure1Cbor, structure2Cbor }) => {
1512
+ sessionNodeManager.setContentParkHook(async ({ agentName, sessionId, recipientPubkeyHex, relayPeerId, relayAddrs, contentHashHex, content, structure1Cbor, structure2Cbor }) => {
1512
1513
  const node = sessionNodeManager.getStandingReceiverNode();
1513
1514
  if (!node) {
1514
1515
  const reason = "standing_receiver_unavailable";
@@ -1519,10 +1520,32 @@ export async function startDaemon(config) {
1519
1520
  // parked when nothing was ever deposited.
1520
1521
  return { ok: false, reason };
1521
1522
  }
1523
+ // SEC-1: sign the entry as the SENDING agent. Without a key we cannot produce an envelope the
1524
+ // recipient will accept, so fail LOUD rather than depositing something that will be refused on
1525
+ // recovery (a deposit the recipient must reject is worse than no deposit — it looks delivered).
1526
+ const senderKp = keyProviders.get(agentName);
1527
+ if (!senderKp) {
1528
+ const reason = "signing_key_unavailable";
1529
+ logger.warn("content.park.deposit.failed", { agentName, sessionId, contentHash: contentHashHex, reason });
1530
+ return { ok: false, reason };
1531
+ }
1522
1532
  const recipientPubkey = Buffer.from(recipientPubkeyHex, "hex");
1533
+ const contentHashBytes = Buffer.from(contentHashHex, "hex");
1534
+ logger.info("content.park.signed", { agentName, sessionId, contentHash: contentHashHex });
1523
1535
  // DOD-MSG-4 (2b): seal the ORDERING ENVELOPE (content + the relay's signed Structure2), not bare
1524
1536
  // content, so the parked entry is self-ordering on recover. The relay still holds only ciphertext.
1525
- const ciphertext = sealToRecipient(recipientPubkey, sessionNodeManager.encodeParkEnvelope(content, structure1Cbor, structure2Cbor));
1537
+ // SEC-1: sealParkEnvelope is the SOLE producer — it signs (sender's K_local, over the
1538
+ // session/recipient/content binding) and seals in one place, so the two park sites cannot drift
1539
+ // apart on what gets signed.
1540
+ const ciphertext = await sealParkEnvelope({
1541
+ signer: senderKp,
1542
+ sessionIdHex: sessionId,
1543
+ recipientPubkey,
1544
+ contentHash: contentHashBytes,
1545
+ content,
1546
+ structure1Cbor,
1547
+ structure2Cbor,
1548
+ });
1526
1549
  const client = new ContentParkClient({ relayPeerId, relayAddrs: [...relayAddrs], logger });
1527
1550
  const res = await client.deposit(node, {
1528
1551
  recipientPubkey,
@@ -1573,11 +1596,29 @@ export async function startDaemon(config) {
1573
1596
  const node = sessionNodeManager.getStandingReceiverNode(record.agent_name);
1574
1597
  if (!node)
1575
1598
  return { parked: false, error: "standing_receiver_unavailable" };
1599
+ // SEC-1: the crash backstop must sign too — an unsigned re-park would be REFUSED on recovery,
1600
+ // which would turn the message-loss backstop into a message-loss cause. This is why the SEC-1
1601
+ // signature binds to the sender's own K_local and NOT to the relay's ordering record: the
1602
+ // ordering record is not persisted in retry_queue (see below) and so cannot be reproduced here,
1603
+ // but the KEY can — the owning agent still holds it after a crash. Hence: NO schema migration.
1604
+ const senderKp = keyProviders.get(ownerName);
1605
+ if (!senderKp)
1606
+ return { parked: false, error: "signing_key_unavailable" };
1576
1607
  const recipientPubkey = Buffer.from(record.counterparty_pubkey, "hex");
1608
+ const contentHashBytes = Buffer.from(entry.contentHashHex, "hex");
1609
+ logger.info("content.park.signed", { agentName: ownerName, sessionId: entry.sessionId, contentHash: entry.contentHashHex, source: "startup_flush" });
1577
1610
  // DOD-MSG-4 (2b): seal the envelope shape too (content only — the durable awaiting queue does not
1578
1611
  // persist the ordering record, so a crash-backstop re-park recovers in arrival order; the common
1579
1612
  // live-park path above carries the full Structure2). Keeps ONE envelope format on the recover side.
1580
- const ciphertext = sealToRecipient(recipientPubkey, sessionNodeManager.encodeParkEnvelope(entry.contentBlob));
1613
+ // SEC-1: same sole producer as the live hook — the backstop signs from the persisted
1614
+ // (sessionId, recipient, contentHash), which is why it survives a crash with NO schema migration.
1615
+ const ciphertext = await sealParkEnvelope({
1616
+ signer: senderKp,
1617
+ sessionIdHex: entry.sessionId,
1618
+ recipientPubkey,
1619
+ contentHash: contentHashBytes,
1620
+ content: entry.contentBlob,
1621
+ });
1581
1622
  const client = new ContentParkClient({ relayPeerId: ep.relayPeerId, relayAddrs: [...ep.relayAddrs], logger });
1582
1623
  const res = await client.deposit(node, {
1583
1624
  recipientPubkey,
@@ -3706,22 +3747,28 @@ export async function startDaemon(config) {
3706
3747
  const client = new ContentParkClient({ relayPeerId, relayAddrs, logger });
3707
3748
  const entries = await client.pull(node, Buffer.from(recipientPubkey, "hex"), kp);
3708
3749
  let recovered = 0;
3750
+ // SEC-1 / review M2: a REFUSED entry must not vanish behind `ok:true`. Report it — an operator
3751
+ // seeing {ok:true, pulled:3, recovered:0} would otherwise have three messages evaporate with the
3752
+ // only explanation buried in the daemon log. This is both the injection signal and, given the
3753
+ // deliberate no-tolerant-window rollout, the lagging-peer signal.
3754
+ const refusals = [];
3755
+ // AC7: one correlationId per recover flow, threaded through every event it emits.
3756
+ const correlationId = randomUUID();
3709
3757
  for (const e of entries) {
3710
3758
  const unsealed = await kp.openContentSeal(e.ciphertext);
3711
3759
  if (!unsealed) {
3712
3760
  logger.warn("content.recover.unseal_failed", { sessionId: e.sessionIdHex, contentHash: e.contentHashHex });
3713
3761
  continue;
3714
3762
  }
3715
- // DOD-MSG-4 (2b): the unsealed blob is the ordering envelope (content + the relay's signed
3716
- // Structure2). Extract the content; if the record is present, verify it and feed the strict-in-
3717
- // order gate the canonical sequence BEFORE ingest so recovered messages order the same way a
3718
- // direct frame does (closes review finding #3). Old/bare-content seals decode to content alone.
3719
- const env = sessionNodeManager.decodeParkEnvelope(unsealed);
3763
+ // SEC-1: authenticate THEN ingest fused in recoverParkedEntry, which is now the ONLY way
3764
+ // parked content can enter the transcript. It verifies the sender's signature over
3765
+ // (session_id, recipient_pubkey, content_hash) and that the signer is this session's
3766
+ // counterparty, and it still verifies + records the DOD-MSG-4 ordering record when present.
3767
+ // Relay deposit is unauthenticated and the seal is anonymous, so WITHOUT this gate anyone
3768
+ // holding the recipient's public key — the relay above all — could inject content that gets
3769
+ // attributed to the honest counterparty and then notarized by the bilateral seal.
3720
3770
  const contentHashBytes = Buffer.from(e.contentHashHex, "hex");
3721
- if (env.structure1Cbor && env.structure2Cbor) {
3722
- sessionNodeManager.recordOrderingRecord(recipientAgent.name, e.sessionIdHex, env.structure1Cbor, env.structure2Cbor, contentHashBytes);
3723
- }
3724
- const ingest = await sessionNodeManager.ingestReceivedContent(recipientAgent.name, e.sessionIdHex, env.content, contentHashBytes);
3771
+ const ingest = await sessionNodeManager.recoverParkedEntry(recipientAgent.name, e.sessionIdHex, Buffer.from(recipientPubkey, "hex"), unsealed, contentHashBytes, correlationId);
3725
3772
  if (ingest.ok && ingest.held) {
3726
3773
  // DOD-MSG-4 (review finding #4): a held entry is NOT yet an appended leaf — its sequence is
3727
3774
  // the FUTURE canonical index, not a completed recovery. Do not count it as recovered; log it
@@ -3758,10 +3805,14 @@ export async function startDaemon(config) {
3758
3805
  }
3759
3806
  }
3760
3807
  else {
3761
- logger.warn("content.recover.ingest_failed", { sessionId: e.sessionIdHex, contentHash: e.contentHashHex, reason: ingest.reason });
3808
+ // SEC-1 (M2): carry the refusal out to the caller, not just to the log. Deliberately still
3809
+ // NOT confirm-deleted — a forgery must not be able to evict itself from the mailbox, and a
3810
+ // genuine v1-peer message must not be destroyed by our refusing it.
3811
+ refusals.push({ contentHash: e.contentHashHex, sessionId: e.sessionIdHex, reason: ingest.reason });
3812
+ logger.warn("content.recover.ingest_failed", { sessionId: e.sessionIdHex, contentHash: e.contentHashHex, reason: ingest.reason, correlationId });
3762
3813
  }
3763
3814
  }
3764
- return { ok: true, recovered, pulled: entries.length };
3815
+ return { ok: true, recovered, pulled: entries.length, refused: refusals.length, refusals };
3765
3816
  }
3766
3817
  // DOD-MSG-4 (auto-recover-on-reconnect): when an agent comes online, drain its parked mailbox from
3767
3818
  // every relay it has sessions on — symmetric to the SENDER's flushAwaitingContent. Without this,