import type { OutboxLease } from "../db/outboxTypes"; import type { IRTCDataChannel, IRTCPeerConnection } from "../api/webrtc/interfaces"; import type { LibCrypto } from "../cryptography/libcrypto"; import type { PqMessageKeyContext } from "../cryptography/pqMessageKey"; import type { RatchetHeader } from "../cryptography/ratchet"; import type { RatchetGateLease } from "./ratchetGate"; import type { ScheduledDeliveryWaitOptions } from "./coverTransfer"; import type { CoverSchedulerStatus } from "./coverScheduler"; import type { RoomPolicyV1 } from "../roomPolicy"; import type { BaseQueryApi } from "@reduxjs/toolkit/query"; export declare const SEND_RECEIPT_WINDOW: number; export declare const SEND_WINDOW_STALL_MS = 10000; export interface ReceiptWindowParams { readonly windowSize: number; readonly stallTimeoutMs: number; readonly pollMs: number; readonly isOpen: () => boolean; readonly inFlight: () => number; readonly signal?: AbortSignal; } /** * Resolve once in-flight drops below the window, the channel leaves "open", * or receipt progress stalls for stallTimeoutMs. Any receipt progress while * still over the window resets the stall clock. Throws only on transfer abort. */ export declare const waitForReceiptWindow: (params: ReceiptWindowParams) => Promise; /** * Is this connection a transport a resumed transfer may open its per-message * channel on? * * `connectionState === "connected"` alone means DTLS is complete — it says * nothing about the main-channel handshake. Opening the per-message channel on * such a transport is what let the peer's replayed receipts arrive ahead of * THIS side's ratchet gate: the identity responder authenticates on SENDING * finish and replays immediately, while the initiator authenticates on * RECEIVING it, one IndexedDB persist later (C7). Requiring the gate to be * open, on this connection's own lease, removes that window at its source * instead of relying on the receiving guard to park the early frame. * * Not finding one is not fatal: the caller polls until its resume budget runs * out, which is exactly what it already did for a transport that had not * reconnected yet. */ export declare const isResumableAuthenticatedTransport: (candidate: IRTCPeerConnection, roomId: string, peerId: string) => boolean; interface TransferCipher { epc: IRTCPeerConnection; /** * Gate lease of the ratchet generation `messageKey`/`header` were derived * from. An in-place re-bind (remoteTransportChange) keeps the * RTCPeerConnection and replaces this lease, so the epc identity alone does * NOT identify the crypto generation. */ lease?: RatchetGateLease; messageKey: Uint8Array; header: RatchetHeader; /** * OWNED copy of the PQ message context captured in the same edge * transaction as the ratchet step (v4). Null only on runtime-free * bootstrap/test edges. Wiped with the message key. */ pqContext?: PqMessageKeyContext | null; } /** * Stop a per-message transport without depending on its eventual `close` * event to release protocol accounting. This also covers cancellation while * the SCTP stream is still connecting and has not entered `dataChannels`. */ export declare const closeTransferChannel: (channel: IRTCDataChannel) => void; /** * Own the one normal terminal close for a message-scoped channel. Receipt * handling only records authenticated completion; sendWithReconcile reaches * this boundary after its send/reconcile work settles. */ export declare const runWithTerminalChannelClose: (currentChannel: () => IRTCDataChannel, operation: () => Promise) => Promise; type WaitForRatchetGate = (roomId: string, peerId: string, signal?: AbortSignal) => Promise; type DurableRatchetStep = (epc: IRTCPeerConnection, roomId: string, module: LibCrypto) => Promise<{ messageKey: Uint8Array; header: RatchetHeader; pqContext: PqMessageKeyContext | null; }>; /** * Bind an in-flight message to the active cryptographic transport. * * Reopening only its DataChannel on the same RTCPeerConnection AND the same * ratchet generation retains the session, so the per-message key/header remain * valid. Anything else — a replacement RTCPeerConnection, or the same one * re-bound in place onto a new remote DTLS certificate — performs a fresh * hybrid handshake and owns an unrelated ratchet. It must wait for that * generation's gate, advance its ratchet once and reseal the missing plaintext * chunks; replaying the old ciphertext/key/header cannot decrypt. * * The generation is identified by the gate lease, NOT by the connection: an * in-place re-bind keeps the very same epc while wiping its ratchet, PQ * runtime and message keys. Treating that as "same transport" would keep * sealing with a dead key and, worse, push frames onto an edge whose gate is * still closed — which the receiver can only reject as application data before * peer authentication, closing the channel and cascading into the edge. * * Exported only so the transport-identity and key-erasure contract has a focused * unit test. It is not part of the package's public root exports. */ export declare const bindTransferCipherToConnection: (transfer: TransferCipher, nextEpc: IRTCPeerConnection, roomId: string, module: LibCrypto, waitForGate?: WaitForRatchetGate, ratchetStep?: DurableRatchetStep, signal?: AbortSignal) => Promise; /** * True iff a per-message channel closing is the PEER cancelling this transfer. * * Immediate mode has no CANCEL frame: one DataChannel is one logical message, * so its close IS the signal — but only when nothing on OUR side retired it. * Three things must hold: the RTCPeerConnection is still connected, it is * still the registered transport for this room/peer, and the ratchet * generation the transfer was sealed under is still the current one. * * That last test is what separates a remote cancel from a local retirement. * An in-place edge re-bind (remoteTransportChange) closes the generation's * message channels while KEEPING the connection connected and registered, so * the first two tests still pass; the gate lease it replaces is the only * evidence that the close was ours. Without it the sender abandons a perfectly * resumable transfer — and reports "cancelled by peer", which an application * may act on by deleting its outgoing message. Mirrors the receiver's * `authenticatedTransportStillAlive` test in handleOpenChannel. */ export declare const isAuthenticatedPeerCancel: (currentEpc: IRTCPeerConnection, peerConnections: IRTCPeerConnection[], roomId: string, peerId: string, transferGateLease: RatchetGateLease | undefined, currentChannel: IRTCDataChannel | undefined) => boolean; /** * A resumed transfer failed to re-bind onto the transport it resumed on: is * another of its bounded resume attempts owed? * * The normal reason is that the transport was REPLACED before its gate could * open — an unauthenticated transport is granted at most one in-place re-bind * (decideRemoteTransportChangeResponse), and the replacement tears the old one * down, which rejects the gate every awaiter is sitting on. The peer is coming * back on the replacement, so giving up here would abandon a transfer that is * one attempt away from finishing. * * Two failures are terminal. A cancelled transfer resumes nothing. And a * gate-wait TIMEOUT means nothing replaced anything: the peer simply never * opened a handshake within the budget (a stale authenticated view), so * another attempt only buys another full budget of the same wait — the same * reason the send's first gate wait rethrows it. */ export declare const isResumableRebindFailure: (error: unknown, signal?: AbortSignal) => boolean; export interface PeerSendTarget { peerId: string; epc?: IRTCPeerConnection; } export type PeerDeliveryOutcome = { peerId: string; status: "delivered"; } | { peerId: string; status: "failed"; phase: "setup" | "transfer"; reason: unknown; } | { peerId: string; status: "skipped"; reason: "not-connected" | "cover-unavailable" | "unauthenticated" | "cancelled"; }; export interface PeerSendFanoutResult { outcomes: PeerDeliveryOutcome[]; startedTransfers: number; } type OpenPeerTransferChannel = (target: Required) => Promise; type StartPeerTransfer = (target: Required, channel: IRTCDataChannel) => Promise; /** * Set up every eligible room edge and settle every transfer that was started. * * Opening channels remains sequential so it cannot burst through the per-edge * channel budget. Transfer promises run concurrently, however, and are given a * rejection handler immediately. A later setup failure therefore becomes that * peer's outcome instead of unwinding past already-running sends and freeing * their shared `newChunks` staging records. */ export declare const runPeerSendFanout: (targets: readonly PeerSendTarget[], openChannel: OpenPeerTransferChannel, startTransfer: StartPeerTransfer, signal?: AbortSignal, onTransferStarted?: () => void) => Promise; export interface SendMessageResult extends PeerSendFanoutResult { transferId: string; merkleRootHex: string; } export declare class MessageDeliveryError extends AggregateError { readonly result: SendMessageResult; constructor(result: SendMessageResult, message: string); } /** * The sender's history is independent of remote delivery once splitToChunks * has committed its complete local copy. Network/setup failures may annotate * delivery status, but only an explicit local cancel removes that history. */ export declare const shouldDeleteLocalMessageAfterFailure: (localMessageCommitted: boolean, wireWorkStarted: boolean, explicitlyCancelled: boolean) => boolean; /** * Can this edge's cover runtime still admit a scheduled job? * * A scheduled job is dequeued only at a cycle open, and `CoverScheduler` * refuses to open one while suspended or stopped. Enqueuing into either and * then awaiting the schedule-derived delivery bound burns that whole budget — * 321 s on the shipped "5 min cycles" preset — for a job nothing was going to * send, and `sendScheduled` fans out serially, so it stalls every later peer * in the room behind it. * * "starting" and "degraded" both still open every subsequent cycle and both * still admit. Since C5 "degraded" is where a late or failed boundary lands, * so rejecting it here would turn one browser stall into a room that can never * send again — the same outcome, moved one layer up. */ export declare const coverRuntimeAdmitsScheduledJobs: (runtime: { readonly status: CoverSchedulerStatus; } | undefined) => boolean; /** * Test/fault-injection seams for `sendScheduled`. Production passes none of * them; they exist so the resume loop can be driven without a durable ratchet * write, a real gate, or a schedule-derived wall-clock wait. Same shape as * `bindTransferCipherToConnection`'s trailing defaults. */ export interface ScheduledSendDependencies { readonly waitForGate?: WaitForRatchetGate; readonly ratchetStep?: DurableRatchetStep; readonly waitForDelivery?: (options: ScheduledDeliveryWaitOptions) => Promise; /** How long to watch for a replacement cover runtime before giving up (C8). */ readonly resumeWaitMs?: number; } /** * Scheduled-cover send: instead of opening a per-message DataChannel and * bursting, step the ratchet once for the message and enqueue a lazy cover job * on each peer edge's cover runtime. The scheduler substitutes one chunk cell * per slot into the already-running cover lanes; the tail is dummy. Delivery is * confirmed by the receiver's terminal scheduled receipt (markTransferComplete). * * A send survives its edge being replaced. Every recovery path destroys the * cover runtime, which settles and deletes every job it holds and (since C3) * wipes the message key at that settlement; the replacement runtime starts * empty. So the retired job cannot be carried across — its key is already zero, * and even a live one belongs to a ratchet generation the peer has thrown away. * The send is instead RE-DERIVED on the replacement generation and re-offered, * exactly as `bindTransferCipherToConnection` does for immediate mode (C8). */ export declare const sendScheduled: (roomId: string, channelLabel: string, policy: RoomPolicyV1, targets: readonly PeerSendTarget[], totalChunks: number, chunkHashes: Uint8Array, merkleRoot: Uint8Array, merkleRootHex: string, transferId: string, hashHex: string, encryptionModule: LibCrypto, merkleModule: LibCrypto, signal?: AbortSignal, onTransferStarted?: () => void, /** * The room's LIVE connection array. A replacement RTCPeerConnection is a * different object, so `target.epc` is stale the moment the edge is rebuilt; * this is where the resume finds the transport that replaced it, exactly as * `resumeChannel` does on the immediate-mode path. */ peerConnections?: readonly IRTCPeerConnection[], dependencies?: ScheduledSendDependencies, outboxLease?: OutboxLease) => Promise; export declare const handleSendMessage: (data: string | File | undefined, api: BaseQueryApi, label: string, roomId: string, peerConnections: IRTCPeerConnection[], dataChannels: IRTCDataChannel[], encryptionModule: LibCrypto, merkleModule: LibCrypto, transferId: string, minChunks?: number, chunkSize?: number, percentageFilledChunk?: number, metadataSchemaVersion?: number) => Promise; export {};