release(v4.8.3): cross-channel msgId dedup + Shade.aliasSession
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user