release(v4.8.3): cross-channel msgId dedup + Shade.aliasSession
Some checks failed
Test / test (push) Has been cancelled
Cross-platform vectors / TypeScript vectors (bun) (push) Has been cancelled
Cross-platform vectors / Kotlin vectors (gradle) (push) Has been cancelled

Two follow-ups to the V4.8.2 duplicate-fan-out fixes Prism filed.

1. `Inbox.acceptBridgeFrame(blob)` + shared 4096-entry msgId LRU.
   The relay durably stores blobs and pushes them to every active
   delivery channel; without a cross-channel ack the bridge frame
   ran first and the next inbox-poll re-dispatched the same blob
   ~30 s later, tripping on consumed prekeys. Bridge consumers now
   plumb pushed frames through `acceptBridgeFrame`, which shares
   the dedup gate + ack path with `pollOnce`. Whichever channel
   delivers first wins; the other acks-and-skips. Inbox records
   the msgId before the ack so a parallel poll can't observe an
   in-flight ack window.

2. `Shade.aliasSession(oldLabel, newLabel)`. First-contact forces
   the receiver to label the new session by the relay's sender
   fingerprint hint (`fp:<senderfp>`); the post-decrypt plaintext
   typically announces the peer's real address. Aliasing moves
   session, trusted identity, peer-verification, and identity-
   version under the canonical label. Holds the per-peer mutex on
   both labels (lexicographic order) so concurrent crypto ops can't
   observe a half-moved state. Refuses to overwrite an existing
   session at the new label.

Wire change: `IncomingMessage.expiresAt?` now surfaces the relay's
expiry so receivers can pass bridge frames straight to
`acceptBridgeFrame` without inventing a TTL.

Tests cover bridge-then-poll, poll-then-bridge, aliasSession happy
path, refuse-to-overwrite, and same-label no-op.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-08 15:49:36 +02:00
parent 8c606ad498
commit d47774ef1c
33 changed files with 519 additions and 32 deletions

View File

@@ -655,6 +655,41 @@ export class Shade {
await this.gates.revoke(address);
}
/**
* Move every per-peer storage row for `oldLabel` (session, trusted
* identity, peer-verification, identity-version counter) to
* `newLabel`. Use this when first-contact forced you to label a
* session by the relay's sender-fingerprint hint
* (`fp:<hex>` — see `IncomingMessage.from` / `FetchedBlob.from`) and
* the just-decrypted plaintext announces the peer's canonical
* address: alias once and every subsequent
* `send`/`receive`/broadcast cross-check operates under the
* announced label, no app-side fp ↔ address mapping needed for the
* receive path.
*
* The rename is atomic from a per-peer-mutex perspective — both
* labels are locked for the duration so concurrent encrypt/decrypt
* can't observe a half-moved state. Throws if `oldLabel` has no
* session, or if `newLabel` already does (refuses to overwrite —
* call `resetSession` first if that's intentional).
*
* After alias, the SDK's internal serialization queues
* (`encryptChains`, `decryptChains`) for `oldLabel` are dropped so
* future operations don't queue behind a stale chain.
*
* V4.8.3 — Prism FR `session-label-asymmetry-v4.8.2.md`.
*/
async aliasSession(oldLabel: string, newLabel: string): Promise<void> {
if (!this.initialized) throw new Error('Not initialized');
await this.manager.aliasSession(oldLabel, newLabel);
// The SDK's per-`from` chains are keyed by label; drop the old
// entries so future `send`/`receive` to either label start with a
// fresh queue rather than chaining off whatever was last in flight
// for `oldLabel`.
this.encryptChains.delete(oldLabel);
this.decryptChains.delete(oldLabel);
}
/**
* Accept a peer's rotated identity. Bumps the per-peer identity-version
* counter so any earlier verification automatically goes stale, then