p2party 0.14.2 → 0.14.7

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.
Files changed (126) hide show
  1. package/README.md +230 -30
  2. package/docs/getting-started.md +15 -0
  3. package/docs/protocol-v4-security.md +40 -0
  4. package/docs/references.md +28 -8
  5. package/docs/session-api.md +10 -2
  6. package/lib/api/signalingServerApi.d.ts +13 -2
  7. package/lib/api/webrtc/applyIceConfiguration.d.ts +9 -0
  8. package/lib/api/webrtc/dataChannelHandler.d.ts +10 -0
  9. package/lib/api/webrtc/deleteRoomData.d.ts +13 -0
  10. package/lib/api/webrtc/disconnectFromAllRoomsQuery.d.ts +2 -1
  11. package/lib/api/webrtc/disconnectFromRoomQuery.d.ts +2 -1
  12. package/lib/api/webrtc/disconnectQuery.d.ts +2 -1
  13. package/lib/api/webrtc/iceRepair.d.ts +3 -1
  14. package/lib/api/webrtc/index.d.ts +2 -1
  15. package/lib/api/webrtc/interfaces.d.ts +37 -0
  16. package/lib/api/webrtc/negotiationLock.d.ts +24 -0
  17. package/lib/api/webrtc/pendingOffer.d.ts +29 -0
  18. package/lib/api/webrtc/rebindDeadline.d.ts +61 -0
  19. package/lib/api/webrtc/remoteTransportChange.d.ts +142 -0
  20. package/lib/api/webrtc/retiredRemoteTransports.d.ts +70 -0
  21. package/lib/api/webrtc/roomEdgeBudget.d.ts +10 -0
  22. package/lib/api/webrtc/setDescriptionQuery.d.ts +8 -1
  23. package/lib/api/webrtc/settleOnClose.d.ts +36 -0
  24. package/lib/blindRendezvous/admissionInput.d.ts +11 -0
  25. package/lib/blindRendezvous/admissionPolicy.d.ts +217 -0
  26. package/lib/blindRendezvous/canonicalSchema.d.ts +139 -0
  27. package/lib/blindRendezvous/carrierEnvelope.d.ts +105 -0
  28. package/lib/blindRendezvous/coreReduce.d.ts +199 -0
  29. package/lib/blindRendezvous/derivations.d.ts +381 -0
  30. package/lib/blindRendezvous/identityHelloAdmission.d.ts +89 -0
  31. package/lib/blindRendezvous/identityHelloAdmissionPersistence.d.ts +133 -0
  32. package/lib/blindRendezvous/identityHelloHistoricalEvidence.d.ts +52 -0
  33. package/lib/blindRendezvous/identityHelloOpen.d.ts +167 -0
  34. package/lib/blindRendezvous/identityHelloTrial.d.ts +271 -0
  35. package/lib/blindRendezvous/localAdmissibleProject.d.ts +101 -0
  36. package/lib/blindRendezvous/localPresenceKeyBinding.d.ts +108 -0
  37. package/lib/blindRendezvous/localStableIdentityCustody.d.ts +46 -0
  38. package/lib/blindRendezvous/normativeKernel.d.ts +222 -0
  39. package/lib/blindRendezvous/operation.d.ts +137 -0
  40. package/lib/blindRendezvous/operationPayload.d.ts +134 -0
  41. package/lib/blindRendezvous/pairBootstrapSuite.d.ts +93 -0
  42. package/lib/blindRendezvous/pairCrypto.d.ts +122 -0
  43. package/lib/blindRendezvous/pairLedger.d.ts +154 -0
  44. package/lib/blindRendezvous/pairOpenBudget.d.ts +67 -0
  45. package/lib/blindRendezvous/pairOpenReservation.d.ts +157 -0
  46. package/lib/blindRendezvous/pairPlaintext.d.ts +240 -0
  47. package/lib/blindRendezvous/pairStepEnvelope.d.ts +60 -0
  48. package/lib/blindRendezvous/plannerState.d.ts +61 -0
  49. package/lib/blindRendezvous/projections.d.ts +71 -0
  50. package/lib/blindRendezvous/publicWindow.d.ts +330 -0
  51. package/lib/blindRendezvous/retainedOperationStore.d.ts +187 -0
  52. package/lib/blindRendezvous/roomInviteV2.d.ts +92 -0
  53. package/lib/blindRendezvous/roomPolicyV4.d.ts +90 -0
  54. package/lib/blindRendezvous/schemas.d.ts +221 -0
  55. package/lib/blindRendezvous/storeDescriptor.d.ts +160 -0
  56. package/lib/cryptography/aeadWasm.d.ts +18 -0
  57. package/lib/cryptography/byteInput.d.ts +12 -0
  58. package/lib/cryptography/carrierAead.d.ts +23 -0
  59. package/lib/cryptography/chacha20poly1305.d.ts +7 -0
  60. package/lib/cryptography/fips202.d.ts +10 -0
  61. package/lib/cryptography/hpke.d.ts +84 -0
  62. package/lib/cryptography/hpkeSuite.d.ts +39 -0
  63. package/lib/cryptography/hybridKem.d.ts +42 -0
  64. package/lib/cryptography/hybridKemSuite.d.ts +22 -0
  65. package/lib/cryptography/interfaces.d.ts +3 -0
  66. package/lib/cryptography/mlkem.d.ts +27 -1
  67. package/lib/cryptography/mnemonic.d.ts +65 -1
  68. package/lib/cryptography/ownedEd25519KeyMaterial.d.ts +27 -0
  69. package/lib/cryptography/webCryptoSha256.d.ts +20 -0
  70. package/lib/cryptography/x25519.d.ts +18 -0
  71. package/lib/cryptography/xchacha20poly1305.d.ts +7 -0
  72. package/lib/db/api.d.ts +6 -3
  73. package/lib/db/identityEd25519ClientBoundary.d.ts +13 -0
  74. package/lib/db/src/getDB.d.ts +2 -2
  75. package/lib/db/types.d.ts +52 -4
  76. package/lib/db.worker.js +1 -1
  77. package/lib/handlers/coverChannelRegistry.d.ts +22 -0
  78. package/lib/handlers/edgeTeardownFollowUp.d.ts +35 -0
  79. package/lib/handlers/handleChallenge.d.ts +2 -1
  80. package/lib/handlers/handleConnectToPeer.d.ts +1 -1
  81. package/lib/handlers/handleHandshake.d.ts +2 -6
  82. package/lib/handlers/handleSendMessage.d.ts +60 -7
  83. package/lib/handlers/handleWebSocketMessage.d.ts +1 -1
  84. package/lib/handlers/handshakeCore.d.ts +2 -7
  85. package/lib/handlers/handshakeFrame.d.ts +25 -0
  86. package/lib/handlers/ratchetGateWait.d.ts +13 -0
  87. package/lib/handlers/reconcile.d.ts +24 -0
  88. package/lib/handlers/requestRoom.d.ts +30 -0
  89. package/lib/handlers/roomResponse.d.ts +53 -0
  90. package/lib/index.d.ts +99 -12
  91. package/lib/index.js +1 -1
  92. package/lib/index.min.js +1 -1
  93. package/lib/index.mjs +1 -1
  94. package/lib/libcrypto.provenance.json +5 -5
  95. package/lib/libcrypto.wasm +0 -0
  96. package/lib/middleware/roomListenerMiddleware.d.ts +4 -1
  97. package/lib/reducers/commonSlice.d.ts +3 -2
  98. package/lib/reducers/keyPairSlice.d.ts +3 -6
  99. package/lib/reducers/roomSlice.d.ts +81 -5
  100. package/lib/reducers/signalingServerSlice.d.ts +3 -2
  101. package/lib/session.d.ts +6 -3
  102. package/lib/session.js +1 -1
  103. package/lib/session.mjs +1 -1
  104. package/lib/store.d.ts +4 -2
  105. package/lib/utils/constants.d.ts +12 -0
  106. package/lib/utils/correlationId.d.ts +4 -0
  107. package/lib/utils/identityRestore.d.ts +108 -0
  108. package/lib/utils/interfaces.d.ts +25 -6
  109. package/lib/utils/roomConnectAdmission.d.ts +15 -0
  110. package/lib/utils/roomLeaveCoordinator.d.ts +38 -0
  111. package/lib/utils/roomRequestCoordinator.d.ts +104 -0
  112. package/lib/utils/roomRosterReconciler.d.ts +83 -0
  113. package/lib/utils/sdpFingerprint.d.ts +6 -0
  114. package/lib/utils/signalingAttemptLifecycle.d.ts +33 -0
  115. package/lib/utils/signalingAuth.d.ts +21 -2
  116. package/lib/utils/signalingBounds.d.ts +6 -0
  117. package/lib/utils/signalingFrame.d.ts +21 -0
  118. package/lib/utils/signalingHeartbeatWatchdog.d.ts +45 -0
  119. package/lib/utils/signalingIngressQueue.d.ts +49 -0
  120. package/lib/utils/signalingPeerRequestDebouncer.d.ts +13 -0
  121. package/lib/utils/signalingRoomLifecycle.d.ts +7 -0
  122. package/lib/utils/signalingRoomOperationLifecycle.d.ts +14 -0
  123. package/lib/utils/signalingServerBoundary.d.ts +11 -0
  124. package/lib/utils/terminalSettlement.d.ts +53 -0
  125. package/lib/utils/transportIdentity.d.ts +5 -0
  126. package/package.json +1 -1
@@ -1,9 +1,10 @@
1
- import type { RTCPeerConnectionParams, RTCSendMessageParams, RTCSetDescriptionParams, RTCSetCandidateParams, RTCOpenChannelParams, RTCDisconnectFromRoomParams, RTCDisconnectFromAllRoomsParams, RTCDisconnectFromPeerParams, RTCDisconnectFromChannelLabelParams, RTCDisconnectFromPeerChannelLabelParams, RTCDisconnectParams } from "./interfaces";
1
+ import type { RTCPeerConnectionParams, RTCSendMessageParams, RTCSetDescriptionParams, RTCSetCandidateParams, RTCOpenChannelParams, RTCDisconnectFromRoomParams, RTCDisconnectFromAllRoomsParams, RTCDisconnectFromPeerParams, RTCDisconnectFromChannelLabelParams, RTCDisconnectFromPeerChannelLabelParams, RTCDisconnectParams, RTCApplyRoomConfigurationParams } from "./interfaces";
2
2
  import type { SendMessageResult } from "../../handlers/handleSendMessage";
3
3
  declare const webrtcApi: import("@reduxjs/toolkit/query").Api<import("@reduxjs/toolkit/query").BaseQueryFn<import("./baseQuery").RTCPeerConnectionParamsExtend, undefined>, {
4
4
  connectWithPeer: import("@reduxjs/toolkit/query").MutationDefinition<RTCPeerConnectionParams, import("@reduxjs/toolkit/query").BaseQueryFn<import("./baseQuery").RTCPeerConnectionParamsExtend, undefined>, never, undefined, "webrtcApi", undefined>;
5
5
  setDescription: import("@reduxjs/toolkit/query").MutationDefinition<RTCSetDescriptionParams, import("@reduxjs/toolkit/query").BaseQueryFn<import("./baseQuery").RTCPeerConnectionParamsExtend, undefined>, never, undefined, "webrtcApi", undefined>;
6
6
  setCandidate: import("@reduxjs/toolkit/query").MutationDefinition<RTCSetCandidateParams, import("@reduxjs/toolkit/query").BaseQueryFn<import("./baseQuery").RTCPeerConnectionParamsExtend, undefined>, never, undefined, "webrtcApi", undefined>;
7
+ applyRoomConfiguration: import("@reduxjs/toolkit/query").MutationDefinition<RTCApplyRoomConfigurationParams, import("@reduxjs/toolkit/query").BaseQueryFn<import("./baseQuery").RTCPeerConnectionParamsExtend, undefined>, never, undefined, "webrtcApi", undefined>;
7
8
  openChannel: import("@reduxjs/toolkit/query").MutationDefinition<RTCOpenChannelParams, import("@reduxjs/toolkit/query").BaseQueryFn<import("./baseQuery").RTCPeerConnectionParamsExtend, undefined>, never, undefined, "webrtcApi", undefined>;
8
9
  sendMessage: import("@reduxjs/toolkit/query").MutationDefinition<RTCSendMessageParams, import("@reduxjs/toolkit/query").BaseQueryFn<import("./baseQuery").RTCPeerConnectionParamsExtend, undefined>, never, SendMessageResult | undefined, "webrtcApi", undefined>;
9
10
  disconnect: import("@reduxjs/toolkit/query").MutationDefinition<RTCDisconnectParams, import("@reduxjs/toolkit/query").BaseQueryFn<import("./baseQuery").RTCPeerConnectionParamsExtend, undefined>, never, undefined, "webrtcApi", undefined>;
@@ -16,8 +16,35 @@ export interface IRTCPeerConnection extends RTCPeerConnection {
16
16
  mainChannel?: IRTCDataChannel;
17
17
  /** Gate ownership captured when this concrete transport is created. */
18
18
  ratchetGateLease: RatchetGateLease;
19
+ /**
20
+ * How many times this RTCPeerConnection has been re-bound IN PLACE onto a
21
+ * new remote DTLS certificate since it last authenticated. The re-bind
22
+ * budget (see decideRemoteTransportChangeResponse) reads it to guarantee
23
+ * that a peer which keeps presenting fresh certificates converges on a
24
+ * replacement transport instead of looping; the handshake success path
25
+ * clears it.
26
+ */
27
+ remoteTransportRebinds: number;
28
+ /**
29
+ * Deadline armed by the in-place re-bind above: if this transport has not
30
+ * re-authenticated when it fires, the edge is torn down and rebuilt instead
31
+ * of waiting out the peer's 30 s handshake step timeout. Cleared by the
32
+ * handshake success path, by the teardown, and re-armed by a later re-bind.
33
+ */
34
+ rebindDeadline?: ReturnType<typeof setTimeout>;
35
+ /**
36
+ * The `clearTimeout` that matches whatever scheduled `rebindDeadline`. The
37
+ * arming site owns the scheduler (tests inject one), while the clearing
38
+ * sites — handshake success, peer teardown — only hold the connection.
39
+ */
40
+ rebindDeadlineClear?: (handle: ReturnType<typeof setTimeout>) => void;
19
41
  /** Resolves once the per-transport WASM dependency is ready. */
20
42
  initialization?: Promise<void>;
43
+ /**
44
+ * Re-proves signaling and applies the latest room ICE configuration before
45
+ * any restart. Installed by the connection owner and shared by repair paths.
46
+ */
47
+ ensureFreshIceConfiguration?: () => Promise<boolean | void>;
21
48
  ratchetState?: RatchetState;
22
49
  ratchetEstablished?: Promise<void>;
23
50
  messageKeyCache?: Map<string, Uint8Array>;
@@ -92,6 +119,14 @@ export interface RTCSetCandidateParams {
92
119
  roomId: string;
93
120
  candidate: RTCIceCandidateInit | RTCIceCandidate;
94
121
  }
122
+ export interface RTCApplyRoomConfigurationParams {
123
+ roomId: string;
124
+ rtcConfig: RTCConfiguration;
125
+ /** Gather a new ICE generation after every live PC accepts the config. */
126
+ restartIce?: boolean;
127
+ /** Expiry must close on failure; rollback would restore expired secrets. */
128
+ failurePolicy?: "rollback" | "close";
129
+ }
95
130
  export interface RTCOpenChannelParams {
96
131
  roomId: string;
97
132
  channel: string | RTCDataChannel;
@@ -148,6 +183,8 @@ export interface RTCDisconnectFromPeerParams {
148
183
  roomId?: string;
149
184
  alsoDeleteData?: boolean;
150
185
  }
186
+ /** Internal dependency injected into bulk teardown queries to avoid API cycles. */
187
+ export type DisconnectPeerTransport = (params: RTCDisconnectFromPeerParams) => Promise<void>;
151
188
  export interface RTCDisconnectFromChannelLabelParams {
152
189
  roomId: string;
153
190
  label: string;
@@ -6,7 +6,31 @@ export declare const getRoomPeerMutex: (roomId: string, peerId: string) => Async
6
6
  * enter the same critical section concurrently.
7
7
  */
8
8
  export declare const releaseRoomPeerMutex: (roomId: string, peerId: string) => void;
9
+ /**
10
+ * Serialize one negotiation operation and release its registry key once every
11
+ * queued owner drains. This must wrap nonterminal frames too: otherwise valid
12
+ * unknown-peer SDP/ICE identifiers can grow the mutex registry forever.
13
+ */
14
+ export declare const runRoomPeerExclusive: <T>(roomId: string, peerId: string, operation: () => Promise<T>) => Promise<T>;
9
15
  /** Narrow test visibility without exposing the mutable registry. */
10
16
  export declare const hasRoomPeerMutex: (roomId: string, peerId: string) => boolean;
17
+ /** Narrow test visibility without exposing the mutable registry. */
18
+ export declare const roomPeerMutexCount: () => number;
11
19
  export declare const getRoomIdentityMutex: (roomId: string, peerPublicKey: string) => AsyncMutex;
12
20
  export declare const releaseRoomIdentityMutex: (roomId: string, peerPublicKey: string) => void;
21
+ /**
22
+ * Serializes caller configuration mutation and a room-response live sweep +
23
+ * Redux publication with the final configuration read, construction, and
24
+ * registry insertion of a new PC. The key is the stable room URL, including
25
+ * while the room is still awaiting its server-assigned id.
26
+ */
27
+ export declare const getRoomIceConfigurationMutex: (roomUrl: string) => AsyncMutex;
28
+ export declare const releaseRoomIceConfigurationMutex: (roomUrl: string) => void;
29
+ /** Narrow test visibility without exposing the mutable registry. */
30
+ export declare const hasRoomIceConfigurationMutex: (roomUrl: string) => boolean;
31
+ /**
32
+ * Server-assigned room ids are globally unique, so validation, live apply, and
33
+ * publication of assignments must be one global transaction. Room responses
34
+ * are rare; serializing them is simpler and safer than multi-key lock ordering.
35
+ */
36
+ export declare const runRoomResponseCommitExclusive: <T>(operation: () => Promise<T>) => Promise<T>;
@@ -0,0 +1,29 @@
1
+ import type { IRTCPeerConnection } from "./interfaces";
2
+ /**
3
+ * How long a fresh transport may hold its first offer without any remote
4
+ * description before the transport is given up on and the room re-dialed.
5
+ *
6
+ * Must outlive the signaling server's dark-socket drop (one missed 10 s ping
7
+ * plus its 5 s pong window): an offer sent to a peer that was still on the
8
+ * roster but whose socket had already gone dark is lost when the server
9
+ * closes that socket, and by the time this fires the roster no longer lists
10
+ * the peer, so the re-dial builds nothing for it until it is really back.
11
+ */
12
+ export declare const PENDING_OFFER_TIMEOUT_MS = 15000;
13
+ /**
14
+ * True iff this transport holds a local offer that no remote description has
15
+ * ever answered, i.e. its FIRST offer is still pending.
16
+ *
17
+ * An ICE-restart offer on a negotiated transport is excluded: there the
18
+ * remote description of the previous round is still applied, and the ICE
19
+ * repair paths own that timeout. Only the identity initiator ever offers on
20
+ * a fresh transport (the responder creates no channel there), so a pending
21
+ * first offer belongs to exactly one side of an edge.
22
+ *
23
+ * Such a transport never fails on its own: without a remote description no
24
+ * ICE checks run, so connectionState stays "new" and nothing tears it down.
25
+ * A peer that later re-dials with a `connection` frame (its old transport
26
+ * died while its socket was dark, so it never saw the offer) finds this PC
27
+ * reused by ensureConnection, which would leave the edge half-built forever.
28
+ */
29
+ export declare const hasUnansweredFirstOffer: (epc: Pick<IRTCPeerConnection, "signalingState" | "remoteDescription">) => boolean;
@@ -0,0 +1,61 @@
1
+ import type { BaseQueryApi } from "@reduxjs/toolkit/query";
2
+ import type { DisconnectPeerTransport, IRTCPeerConnection } from "./interfaces";
3
+ /**
4
+ * The timer primitives the deadline uses. Injected so a test drives the
5
+ * deadline by hand instead of waiting out real seconds; production passes
6
+ * nothing and gets the platform's own.
7
+ */
8
+ export interface RebindDeadlineScheduler {
9
+ readonly setTimeout: (callback: () => void, delayMs: number) => ReturnType<typeof setTimeout>;
10
+ readonly clearTimeout: (handle: ReturnType<typeof setTimeout>) => void;
11
+ }
12
+ /**
13
+ * Release a pending re-bind deadline. Idempotent, and safe on a connection
14
+ * that never armed one.
15
+ */
16
+ export declare const clearRebindAuthenticationDeadline: (epc: Pick<IRTCPeerConnection, "rebindDeadline" | "rebindDeadlineClear">) => void;
17
+ export interface ArmRebindAuthenticationDeadlineParams {
18
+ epc: IRTCPeerConnection;
19
+ roomId: string;
20
+ peerId: string;
21
+ api: Pick<BaseQueryApi, "dispatch" | "getState">;
22
+ /** Tears this room/peer transport down through the transport-failure path. */
23
+ disconnectPeer: DisconnectPeerTransport;
24
+ deadlineMs?: number;
25
+ schedule?: RebindDeadlineScheduler;
26
+ }
27
+ /**
28
+ * Give one in-place re-bind a wall-clock deadline to re-authenticate in.
29
+ *
30
+ * A re-bind keeps the RTCPeerConnection — and with it the SCTP association
31
+ * that was negotiated against the RETIRED remote certificate. Chromium will
32
+ * happily open our own new outbound streams on it while never delivering the
33
+ * peer's: on the process-freeze harness the woken responder re-binds its
34
+ * surviving PC, its own message channel reaches the peer two seconds later,
35
+ * and the peer's `main` — opened on its brand-new PC before the re-bind even
36
+ * happened — never arrives at all. The re-bind is then a silent black hole:
37
+ * ICE stays connected so no failure path replaces the transport, and the edge
38
+ * only heals when the PEER's handshake step timeout (30 s) tears its own side
39
+ * down and the normal re-dial rebuilds everything, ~37 s after the wake.
40
+ *
41
+ * So the re-bind gets a budget. When it expires with nothing authenticated,
42
+ * this side does what the peer's timeout would eventually have done, sooner:
43
+ * tear the edge down through the transport-failure path (which mints a fresh
44
+ * local certificate on the rebuilt transport) and ask the room to re-introduce
45
+ * the pair.
46
+ *
47
+ * Three guards make a late timer harmless, because the same connection object
48
+ * outlives the re-bind that armed the deadline:
49
+ *
50
+ * - the connection still carries the gate lease this re-bind installed (a
51
+ * LATER re-bind of the same transport installed its own and re-armed);
52
+ * - that lease is still the edge's current one (a replacement transport, from
53
+ * a re-dial or a "replace" decision, claims a fresh lease — this deadline
54
+ * then belongs to a transport nobody uses any more);
55
+ * - the edge has not authenticated in the meantime (a live ratchet, or the
56
+ * gate open under this very lease).
57
+ *
58
+ * The teardown itself is not a guard: `webrtcDisconnectPeerQuery` clears the
59
+ * deadline, so a torn-down connection never fires it at all.
60
+ */
61
+ export declare const armRebindAuthenticationDeadline: ({ epc, roomId, peerId, api, disconnectPeer, deadlineMs, schedule, }: ArmRebindAuthenticationDeadlineParams) => void;
@@ -0,0 +1,142 @@
1
+ import type { BaseQueryApi } from "@reduxjs/toolkit/query";
2
+ import type { RebindDeadlineScheduler } from "./rebindDeadline";
3
+ import type { DisconnectPeerTransport, IRTCDataChannel, IRTCPeerConnection } from "./interfaces";
4
+ /**
5
+ * True iff both SDPs declare a sha-256 DTLS fingerprint and the two differ.
6
+ *
7
+ * A remote description whose fingerprint differs from the one already applied
8
+ * on this RTCPeerConnection means the peer built a NEW RTCPeerConnection (a
9
+ * fresh DTLS certificate is minted per PC) while this side kept its old one.
10
+ * The browser accepts the change and re-runs DTLS on the same PC, but every
11
+ * piece of edge authentication this side holds — handshake transcript (which
12
+ * binds both fingerprints), ratchet, PQ runtime, message keys — was proven
13
+ * against the OLD certificate and must not survive onto the new transport.
14
+ *
15
+ * Missing or malformed fingerprints on either side are not a change: there is
16
+ * nothing to compare, and the handshake itself rejects such SDPs later.
17
+ */
18
+ export declare const detectRemoteTransportChange: (previousSdp: string | undefined, nextSdp: string) => boolean;
19
+ /**
20
+ * True iff this room/peer edge has been authenticated on THIS
21
+ * RTCPeerConnection: a live ratchet, a ratchet gate opened under this
22
+ * transport's own lease, or an open `main` (a handshake in flight over a live
23
+ * SCTP association). A transport that only ever negotiated (main still
24
+ * connecting, gate pending) has proven nothing yet.
25
+ */
26
+ export declare const isEdgeAuthenticatedOnTransport: (epc: Pick<IRTCPeerConnection, "ratchetState" | "ratchetGateLease" | "mainChannel">, roomId: string, peerId: string) => boolean;
27
+ export type RemoteTransportChangeResponse = "reset-in-place" | "replace";
28
+ export interface RemoteTransportChangeDecisionParams {
29
+ epc: Pick<IRTCPeerConnection, "ratchetState" | "ratchetGateLease" | "mainChannel" | "remoteTransportRebinds">;
30
+ roomId: string;
31
+ peerId: string;
32
+ descriptionType: RTCSdpType;
33
+ /** Whether this side is the identity initiator (the side that opens main). */
34
+ isInitiator: boolean;
35
+ }
36
+ /**
37
+ * What to do with an existing RTCPeerConnection whose remote DTLS fingerprint
38
+ * is about to change.
39
+ *
40
+ * A never-authenticated transport is simply re-bound in place: nothing proven
41
+ * against the old certificate exists, and the browser renegotiates DTLS on
42
+ * the same PC (the T1 path, `resetEdgeAuthenticationForNewRemoteTransport`).
43
+ *
44
+ * An AUTHENTICATED transport whose peer shows up with a new certificate is
45
+ * the stale side of the edge: its SCTP association is dead on the far side
46
+ * while it is still writable locally. Whether it can be re-bound in place
47
+ * depends on who has to open the next `main` on it:
48
+ *
49
+ * - The peer's fresh transport OFFERS: it carries the peer's channels, so a
50
+ * fresh PC of our own answers it cleanly whatever our role. Replace.
51
+ * - We are the identity initiator and receive an ANSWER: re-binding in place
52
+ * would make us create the replacement `main` on the OLD, still-writable
53
+ * SCTP association, whose onopen fires before DTLS re-runs against the new
54
+ * certificate; the handshake then reads the old remote certificate from
55
+ * getStats and fails as a fingerprint mismatch. Replace the PC and re-dial.
56
+ * - We are the responder and receive an ANSWER: we open no channel on the
57
+ * reset PC. The peer's fresh transport (the initiator) already holds the
58
+ * next `main`, which opens only once the browser has re-run DTLS and SCTP
59
+ * against the new certificate, so the in-place re-bind completes with a
60
+ * clean handshake. Replacing here would instead leave the peer's fresh PC
61
+ * bound to our closed one with nothing to re-offer to our channel-less
62
+ * replacement (a never-connected transport does not re-dial on failure).
63
+ *
64
+ * All of that is bounded by one invariant: AT MOST ONE in-place re-bind per
65
+ * unauthenticated transport. An in-place re-bind changes only the REMOTE
66
+ * certificate — ours is minted with the RTCPeerConnection and lives as long as
67
+ * it does. A PC whose ICE stays connected never fails, so neither the
68
+ * ICE-failure re-dial nor the grace-timer teardown ever replaces it, and it
69
+ * keeps answering with a certificate the peer may already have retired (T9)
70
+ * while the peer keeps offering from ever newer ones: a livelock in which both
71
+ * sides negotiate forever and no `main` ever opens. Requiring the first
72
+ * re-bind to have actually authenticated before granting a second one caps the
73
+ * loop at one round — the second fingerprint change replaces the transport,
74
+ * which mints a new local certificate the peer has never retired, and the edge
75
+ * converges.
76
+ */
77
+ export declare const decideRemoteTransportChangeResponse: ({ epc, roomId, peerId, descriptionType, isInitiator, }: RemoteTransportChangeDecisionParams) => RemoteTransportChangeResponse;
78
+ export interface ResetEdgeAuthenticationParams {
79
+ epc: IRTCPeerConnection;
80
+ roomId: string;
81
+ peerId: string;
82
+ api: Pick<BaseQueryApi, "dispatch" | "getState">;
83
+ /**
84
+ * The shared open-channel registry. Passed in explicitly (the caller already
85
+ * holds it) so this helper stays store-free and unit-testable: it owns the
86
+ * retirement of the generation's channels, which live in BOTH this array and
87
+ * `epc.messageChannels`.
88
+ */
89
+ dataChannels: IRTCDataChannel[];
90
+ /**
91
+ * Tears this room/peer transport down through the transport-failure path.
92
+ * Injected exactly as setDescriptionQuery's own teardown is, so the deadline
93
+ * armed below needs no import of the API that registers it.
94
+ */
95
+ disconnectPeer: DisconnectPeerTransport;
96
+ /** Test seams for the re-bind deadline; production takes the defaults. */
97
+ deadlineMs?: number;
98
+ schedule?: RebindDeadlineScheduler;
99
+ reason: Error;
100
+ }
101
+ /**
102
+ * Drop everything that authenticated this room/peer edge against the previous
103
+ * remote DTLS certificate, WITHOUT tearing the RTCPeerConnection down.
104
+ *
105
+ * This is the "reset-in-place" response of decideRemoteTransportChangeResponse:
106
+ * a transport that never authenticated, or the responder's transport when the
107
+ * initiator answers from a fresh one. The PC itself survives: the browser
108
+ * renegotiates DTLS on it against the new remote certificate, and the next
109
+ * `main` (created by the identity initiator on a never-authenticated PC, or
110
+ * arriving from the initiator's fresh transport on the responder's) opens
111
+ * only over the re-run DTLS/SCTP, so the new handshake binds the new
112
+ * fingerprint pair. The old `main` is closed here; its onclose handler
113
+ * observes a stale gate lease (replaced below) and returns early, so closing
114
+ * it never cascades into a disconnectFromPeer of the edge we are keeping.
115
+ *
116
+ * EVERY other channel of the retired generation goes with it. A per-message
117
+ * channel belongs to the ratchet generation that opened it: it captured that
118
+ * generation's gate lease, so once the lease is replaced here its next inbound
119
+ * frame can only be rejected as application data before peer authentication —
120
+ * and that rejection closes the channel, which on a live transfer cascades
121
+ * into closing `main` and tearing down the very edge this re-bind exists to
122
+ * keep. (Observed on the process-freeze harness: an 8 MiB transfer in flight
123
+ * across the freeze left its data channel OPEN on the surviving PC, and the
124
+ * first frame after the re-bind killed the edge.) A send in flight over such a
125
+ * channel loses nothing: the gate lease replaced here is exactly what tells
126
+ * handleSendMessage that the close was OURS, so the transfer resumes on the
127
+ * re-authenticated generation (re-opening its channel, re-stepping the ratchet)
128
+ * instead of being read as a cancel by the peer. Cover lanes are NOT swept
129
+ * here — `teardownCoverEdge` above closes each one, and the close listener
130
+ * `registerCoverChannelUntilClose` installed unregisters it from both
131
+ * `epc.coverChannels` and the shared array.
132
+ *
133
+ * Detaching those handlers also skips the receive-side bookkeeping a normal
134
+ * close would have run — `forgetCompletedReceiveMessageKey`. That is
135
+ * deliberate: retiring a key durably is asynchronous I/O and this reset is a
136
+ * synchronous step on the description path. Every key is wiped from RAM below
137
+ * (message-key cache filled with zeros, ratchet wiped, PQ runtime destroyed),
138
+ * and the durable edge checkpoint is keyed by (roomId, peerPublicKey) and
139
+ * rewritten whole by the next handshake's persist, so a completed transfer's
140
+ * key survives only until this edge re-authenticates.
141
+ */
142
+ export declare const resetEdgeAuthenticationForNewRemoteTransport: ({ epc, roomId, peerId, api, dataChannels, disconnectPeer, deadlineMs, schedule, reason, }: ResetEdgeAuthenticationParams) => void;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Remote DTLS certificates this side has stopped believing in, per
3
+ * (roomId, peerId) edge.
4
+ *
5
+ * A description is only ever meaningful for the transport pair it belongs to,
6
+ * but nothing on the wire correlates an answer with the offer it answers (the
7
+ * SDK cannot add signaling fields without a server change). So a description
8
+ * that a peer produced on a transport we already retired can land on a
9
+ * transport of ours that has no remote description yet — where the
10
+ * fingerprint-change detection of `remoteTransportChange` returns false BY
11
+ * CONSTRUCTION, because there is nothing to compare against — and bind our
12
+ * fresh RTCPeerConnection to a certificate that is already dead.
13
+ *
14
+ * That is exactly how a frozen phone whose signaling socket stayed open
15
+ * deadlocks an edge: the peer's queued ICE-restart offer is answered from its
16
+ * old, still-authenticated transport after the thaw; the answer arrives at
17
+ * our brand-new PC, is accepted, and the correct answer that follows it is
18
+ * then dropped as "stale" because the PC is already `stable`. Neither side's
19
+ * transport ever connects, so no failure path re-dials either.
20
+ *
21
+ * Remembering the fingerprint of every transport we retire closes that hole:
22
+ * an ANSWER carrying one is discarded before it is applied (setDescriptionQuery
23
+ * explains why an offer is instead accepted, and un-retires the transport it
24
+ * binds through `forgetRetiredRemoteTransport`).
25
+ *
26
+ * The set is bounded twice, because it is keyed by a peer-supplied identifier
27
+ * and must never grow without limit nor lock a peer out forever:
28
+ *
29
+ * - at most MAX_RETIRED_REMOTE_TRANSPORTS_PER_EDGE fingerprints per edge, the
30
+ * oldest evicted first;
31
+ * - each entry expires RETIRED_REMOTE_TRANSPORT_TTL_MS after it was retired.
32
+ *
33
+ * Chromium mints a fresh certificate for every RTCPeerConnection and this
34
+ * codebase never passes `RTCConfiguration.certificates` for its own
35
+ * connections (roomSlice only CLONES a caller-supplied configuration), so a
36
+ * rebuilt peer transport always presents a fingerprint we have never retired.
37
+ * If a caller DOES pin a certificate, its peer's replacement transport
38
+ * presents the very fingerprint we retired and its descriptions are discarded
39
+ * until the TTL expires; the edge then heals on the next re-dial, one TTL
40
+ * later, instead of immediately.
41
+ */
42
+ export declare const MAX_RETIRED_REMOTE_TRANSPORTS_PER_EDGE = 4;
43
+ export declare const RETIRED_REMOTE_TRANSPORT_TTL_MS = 120000;
44
+ /**
45
+ * Remember that this side has stopped believing in the remote transport that
46
+ * presented `transport` (an SDP blob or its raw sha-256 fingerprint) on the
47
+ * (roomId, peerId) edge. Returns whether a fingerprint was actually recorded.
48
+ */
49
+ export declare const retireRemoteTransport: (roomId: string, peerId: string, transport: string | Uint8Array | null | undefined, now?: number) => boolean;
50
+ /**
51
+ * True iff `sdp` declares the certificate of a remote transport already
52
+ * retired for this edge, i.e. the description must not be applied.
53
+ */
54
+ export declare const isRetiredRemoteTransport: (roomId: string, peerId: string, sdp: string | null | undefined, now?: number) => boolean;
55
+ /**
56
+ * Un-retire one transport of an edge: this side is bound to it again.
57
+ *
58
+ * A retired fingerprint means "we are not bound to this remote certificate",
59
+ * so the moment a description from it IS applied — the peer offered on the
60
+ * transport we had given up on, proving it negotiates there — the retirement
61
+ * is void. Without this, the answer to our own next ICE restart on that very
62
+ * binding would be discarded as retired.
63
+ */
64
+ export declare const forgetRetiredRemoteTransport: (roomId: string, peerId: string, transport: string | Uint8Array | null | undefined) => void;
65
+ /**
66
+ * Forget every retired transport of one edge. Called when the edge itself is
67
+ * terminal — its room was left or purged — so a room the user rejoins later
68
+ * starts with a clean slate.
69
+ */
70
+ export declare const forgetRetiredRemoteTransports: (roomId: string, peerId: string) => void;
@@ -0,0 +1,10 @@
1
+ import type { Peer } from "../../reducers/roomSlice";
2
+ import type { IRTCPeerConnection } from "./interfaces";
3
+ /**
4
+ * Enforce one cumulative edge budget across Redux roster state and concrete
5
+ * transports, including PCs inserted while their WASM initialization awaits.
6
+ *
7
+ * Callers run this under the stable room-configuration mutex immediately
8
+ * before registry insertion, making concurrent identities observe each other.
9
+ */
10
+ export declare const assertRoomEdgeBudget: (roomId: string, peers: readonly Peer[], peerConnections: readonly IRTCPeerConnection[], candidatePublicKey: string) => void;
@@ -1,9 +1,16 @@
1
1
  import type { BaseQueryFn } from "@reduxjs/toolkit/query";
2
- import type { RTCSetDescriptionParams, IRTCPeerConnection, IRTCIceCandidate, IRTCDataChannel } from "./interfaces";
2
+ import type { RTCSetDescriptionParams, IRTCPeerConnection, IRTCIceCandidate, IRTCDataChannel, DisconnectPeerTransport } from "./interfaces";
3
3
  export interface RTCSetDescriptionParamsExtension extends RTCSetDescriptionParams {
4
4
  peerConnections: IRTCPeerConnection[];
5
5
  iceCandidates: IRTCIceCandidate[];
6
6
  dataChannels: IRTCDataChannel[];
7
+ /**
8
+ * Tears one room/peer transport down through the same path as a transport
9
+ * failure (disconnectFromPeer). Injected, like the bulk teardown queries
10
+ * do, so this module needs no import of the API that registers it and a
11
+ * test can bind the real query to its own transport registry.
12
+ */
13
+ disconnectPeer: DisconnectPeerTransport;
7
14
  }
8
15
  declare const webrtcSetDescriptionQuery: BaseQueryFn<RTCSetDescriptionParamsExtension, undefined>;
9
16
  export default webrtcSetDescriptionQuery;
@@ -0,0 +1,36 @@
1
+ /** The slice of an RTCPeerConnection that proves the transport is gone. */
2
+ export type PcOperationTarget = Pick<RTCPeerConnection, "signalingState" | "connectionState">;
3
+ export interface AwaitPcOperationOptions {
4
+ /** How often the transport is inspected while the operation is in flight. */
5
+ readonly pollMs?: number;
6
+ /** Wall-clock budget after which the operation is abandoned. */
7
+ readonly maxMs?: number;
8
+ /** Named in the timeout warning so a stuck operation is identifiable. */
9
+ readonly name?: string;
10
+ }
11
+ export declare const isPcOperationTargetClosed: (epc: PcOperationTarget) => boolean;
12
+ /**
13
+ * Await one RTCPeerConnection operation without ever waiting forever on it.
14
+ *
15
+ * Closing an RTCPeerConnection aborts every operation in flight on it, and the
16
+ * specification aborts them *without settling* their promises — while close()
17
+ * itself fires no `connectionstatechange`. An `await pc.addIceCandidate(...)`
18
+ * or `await pc.setRemoteDescription(...)` on a connection that closes
19
+ * underneath it therefore never returns. In this codebase those awaits run
20
+ * inside the (room, peer) negotiation mutex, which is entered from the socket's
21
+ * strictly serial ingress queue: one such await strands the edge's mutex
22
+ * forever AND stops every later signaling frame on that socket, so a peer's
23
+ * re-introductions go unanswered indefinitely.
24
+ *
25
+ * The returned promise settles three ways:
26
+ * - the operation's own result (or rejection, rethrown for the caller's
27
+ * existing error handling),
28
+ * - `"closed"` as soon as a poll observes the transport closed — the operation
29
+ * is abandoned, since nothing it could still do matters,
30
+ * - `"timeout"` once the budget expires, which callers treat exactly like a
31
+ * failure of that operation.
32
+ *
33
+ * An abandoned operation's later rejection is consumed here, so walking away
34
+ * from it can never surface as an unhandled rejection.
35
+ */
36
+ export declare const awaitPcOperation: <T>(epc: PcOperationTarget, operation: Promise<T>, { pollMs, maxMs, name, }?: AwaitPcOperationOptions) => Promise<T | "closed" | "timeout">;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Closed error for malformed or fabricated inputs at the trusted admission
3
+ * kernel boundary. Policy denials are ordinary result values and never use
4
+ * this error.
5
+ */
6
+ export declare class AdmissionInputError extends Error {
7
+ readonly name = "AdmissionInputError";
8
+ readonly code: "invalid-admission-kernel-input";
9
+ constructor(message: string);
10
+ }
11
+ export declare const invalidAdmissionInput: (message: string) => never;