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

@@ -1,6 +1,6 @@
{
"name": "@shade/transport-bridge",
"version": "4.8.2",
"version": "4.8.3",
"type": "module",
"main": "src/index.ts",
"types": "src/index.ts",

View File

@@ -28,6 +28,14 @@ export interface IncomingMessage {
receivedAt: number;
/** Relay-assigned message id (sha256 of ciphertext). Useful for ack/dedup. */
msgId?: string;
/**
* Absolute expiry (ms since epoch) reported by the relay. Surfaced for
* symmetry with `Inbox.handleBlob` so that
* `Inbox.acceptBridgeFrame(msg)` can be wired directly without the
* caller having to invent a TTL. Optional — pre-V4.8.3 relays / non-
* inbox bridges don't populate it.
*/
expiresAt?: number;
}
/** Subscriber callback. Bridges MAY invoke it concurrently. */
@@ -83,6 +91,7 @@ export function decodeWireMessage(wire: BridgeWireMessage): IncomingMessage {
receivedAt: wire.receivedAt,
};
if (wire.msgId !== undefined) msg.msgId = wire.msgId;
if (wire.expiresAt !== undefined) msg.expiresAt = wire.expiresAt;
return msg;
}