p2party 0.14.3 → 0.14.10

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 (123) hide show
  1. package/README.md +234 -31
  2. package/THIRD_PARTY_NOTICES.md +33 -6
  3. package/docs/getting-started.md +118 -4
  4. package/docs/protocol-v4-security.md +96 -6
  5. package/docs/references.md +28 -8
  6. package/docs/session-api.md +19 -2
  7. package/docs/wire-format.md +102 -14
  8. package/lib/api/signalingServerApi.d.ts +51 -2
  9. package/lib/api/webrtc/interfaces.d.ts +70 -1
  10. package/lib/api/webrtc/peerSocketGeneration.d.ts +68 -0
  11. package/lib/cryptography/coverCell.d.ts +7 -0
  12. package/lib/cryptography/emscripten.d.ts +101 -0
  13. package/lib/cryptography/interfaces.d.ts +3 -0
  14. package/lib/cryptography/libcrypto.d.ts +181 -0
  15. package/lib/cryptography/mlkem.d.ts +27 -1
  16. package/lib/cryptography/mnemonic.d.ts +65 -1
  17. package/lib/db/outboxTypes.d.ts +135 -0
  18. package/lib/db/types.d.ts +81 -7
  19. package/lib/db.worker.js +1 -1
  20. package/lib/handlers/coverRuntime.d.ts +14 -1
  21. package/lib/handlers/coverScheduler.d.ts +59 -1
  22. package/lib/handlers/coverTransfer.d.ts +169 -3
  23. package/lib/handlers/handleSendMessage.d.ts +138 -9
  24. package/lib/handlers/handshakeCore.d.ts +2 -7
  25. package/lib/handlers/handshakeFrame.d.ts +25 -0
  26. package/lib/handlers/outbox.d.ts +47 -0
  27. package/lib/handlers/ratchetGate.d.ts +9 -0
  28. package/lib/handlers/reconcile.d.ts +5 -0
  29. package/lib/index.d.ts +102 -12
  30. package/lib/index.js +1 -1
  31. package/lib/index.min.js +1 -1
  32. package/lib/index.mjs +1 -1
  33. package/lib/libcrypto.provenance.json +5 -5
  34. package/lib/libcrypto.wasm +0 -0
  35. package/lib/reducers/commonSlice.d.ts +3 -2
  36. package/lib/reducers/keyPairSlice.d.ts +3 -6
  37. package/lib/reducers/roomSlice.d.ts +89 -6
  38. package/lib/reducers/signalingServerSlice.d.ts +3 -2
  39. package/lib/session.d.ts +6 -3
  40. package/lib/session.js +1 -1
  41. package/lib/session.mjs +1 -1
  42. package/lib/store.d.ts +4 -2
  43. package/lib/utils/constants.d.ts +19 -0
  44. package/lib/utils/identityRestore.d.ts +108 -0
  45. package/lib/utils/interfaces.d.ts +36 -6
  46. package/lib/utils/roomLeaveCoordinator.d.ts +38 -0
  47. package/lib/utils/signalingReconnectController.d.ts +44 -0
  48. package/lib/utils/terminalSettlement.d.ts +53 -0
  49. package/package.json +13 -15
  50. package/lib/api/webrtc/disconnectFromAllRoomsQuery.d.ts +0 -8
  51. package/lib/api/webrtc/disconnectFromChannelLabelQuery.d.ts +0 -7
  52. package/lib/api/webrtc/disconnectFromPeerChannelLabelQuery.d.ts +0 -8
  53. package/lib/api/webrtc/disconnectFromPeerQuery.d.ts +0 -9
  54. package/lib/api/webrtc/disconnectFromRoomQuery.d.ts +0 -8
  55. package/lib/api/webrtc/disconnectQuery.d.ts +0 -8
  56. package/lib/api/webrtc/iceGeneration.d.ts +0 -17
  57. package/lib/api/webrtc/iceRepair.d.ts +0 -10
  58. package/lib/api/webrtc/index.d.ts +0 -16
  59. package/lib/api/webrtc/negotiationLock.d.ts +0 -12
  60. package/lib/api/webrtc/openChannelQuery.d.ts +0 -8
  61. package/lib/api/webrtc/pendingIceCandidates.d.ts +0 -14
  62. package/lib/api/webrtc/roomPeer.d.ts +0 -11
  63. package/lib/api/webrtc/sendMessageQuery.d.ts +0 -14
  64. package/lib/api/webrtc/setCandidateQuery.d.ts +0 -8
  65. package/lib/api/webrtc/setDescriptionQuery.d.ts +0 -9
  66. package/lib/cryptography/cpace.d.ts +0 -41
  67. package/lib/cryptography/ed25519.d.ts +0 -11
  68. package/lib/cryptography/hashStream.d.ts +0 -15
  69. package/lib/cryptography/hkdf.d.ts +0 -3
  70. package/lib/cryptography/identityCrossSig.d.ts +0 -7
  71. package/lib/cryptography/memory.d.ts +0 -14
  72. package/lib/cryptography/merkle.d.ts +0 -42
  73. package/lib/cryptography/pqHealingFrame.d.ts +0 -39
  74. package/lib/cryptography/random.d.ts +0 -22
  75. package/lib/cryptography/testModule.d.ts +0 -7
  76. package/lib/cryptography/utils.d.ts +0 -13
  77. package/lib/cryptography/x25519.d.ts +0 -15
  78. package/lib/cryptography/x3dh.d.ts +0 -9
  79. package/lib/db/api.d.ts +0 -67
  80. package/lib/db/db.worker.d.ts +0 -1
  81. package/lib/db/ratchetWrap.d.ts +0 -36
  82. package/lib/db/src/getDB.d.ts +0 -81
  83. package/lib/handlers/chunkFrame.d.ts +0 -13
  84. package/lib/handlers/connectionSignal.d.ts +0 -26
  85. package/lib/handlers/coverEdge.d.ts +0 -42
  86. package/lib/handlers/frameType.d.ts +0 -10
  87. package/lib/handlers/handleChallenge.d.ts +0 -3
  88. package/lib/handlers/handleConnectToPeer.d.ts +0 -6
  89. package/lib/handlers/handleHandshake.d.ts +0 -74
  90. package/lib/handlers/handleMessageQueueing.d.ts +0 -54
  91. package/lib/handlers/handleOpenChannel.d.ts +0 -12
  92. package/lib/handlers/handleQueuedIceCandidates.d.ts +0 -2
  93. package/lib/handlers/handleReadReceipt.d.ts +0 -17
  94. package/lib/handlers/handleReceiveMessage.d.ts +0 -32
  95. package/lib/handlers/handleWebSocketMessage.d.ts +0 -3
  96. package/lib/handlers/messageChunkCrypto.d.ts +0 -70
  97. package/lib/handlers/peerRosterDelta.d.ts +0 -12
  98. package/lib/handlers/pqHealingOrchestrator.d.ts +0 -46
  99. package/lib/handlers/ratchetPersist.d.ts +0 -53
  100. package/lib/handlers/receiptFrame.d.ts +0 -18
  101. package/lib/handlers/receiveMessageKeyLifetime.d.ts +0 -24
  102. package/lib/handlers/transferAbort.d.ts +0 -42
  103. package/lib/middleware/keyPairListenerMiddleware.d.ts +0 -2
  104. package/lib/middleware/roomListenerMiddleware.d.ts +0 -2
  105. package/lib/roomInvite.d.ts +0 -18
  106. package/lib/roomPinAttempts.d.ts +0 -12
  107. package/lib/roomPinVault.d.ts +0 -26
  108. package/lib/utils/channelLabel.d.ts +0 -9
  109. package/lib/utils/chunkBounds.d.ts +0 -9
  110. package/lib/utils/drainAndClose.d.ts +0 -5
  111. package/lib/utils/identityRole.d.ts +0 -19
  112. package/lib/utils/leafHash.d.ts +0 -13
  113. package/lib/utils/metadata.d.ts +0 -22
  114. package/lib/utils/mutex.d.ts +0 -24
  115. package/lib/utils/protocolVersion.d.ts +0 -5
  116. package/lib/utils/receiptToken.d.ts +0 -7
  117. package/lib/utils/sendQueueKey.d.ts +0 -2
  118. package/lib/utils/signalingAuth.d.ts +0 -8
  119. package/lib/utils/signalingBounds.d.ts +0 -9
  120. package/lib/utils/splitToChunks.d.ts +0 -30
  121. package/lib/utils/uint8array.d.ts +0 -16
  122. package/lib/utils/waitForOpen.d.ts +0 -3
  123. package/lib/utils/zeroFree.d.ts +0 -10
@@ -17,9 +17,9 @@ There are three different acknowledgements in the system:
17
17
  2. The protocol-v4 handshake runs over the open main channel. After HELLO,
18
18
  responder CONFIRM, initiator CONFIRM, and responder FINISH, the peers have
19
19
  authenticated the same hybrid root and initial ratchet keys.
20
- 3. Authenticated 65-byte receipt frames acknowledge message chunks and final
21
- transfer completion. They are delivery/reconciliation signals, not handshake
22
- establishment.
20
+ 3. Immediate 65-byte receipts on authenticated message channels, or encrypted
21
+ scheduled control cells, acknowledge chunks and transfer completion. They
22
+ are delivery/reconciliation signals, not handshake establishment.
23
23
 
24
24
  An open data channel therefore is necessary transport readiness, not an
25
25
  established p2party crypto session.
@@ -66,15 +66,65 @@ byte so the sparse post-quantum healing epoch cannot wrap.
66
66
 
67
67
  The fixed frame geometry absorbs the ratchet and AEAD overhead into the cell
68
68
  budget. Random padding and decoy slots can hide the exact payload length within
69
- one transfer's chosen number of frames. Receipts have a distinct fixed 65-byte
70
- geometry. Handshake flights are suite- and step-dependent rather than
71
- 65,490-byte cells.
69
+ one transfer's chosen number of frames. Immediate receipts have a distinct
70
+ 65-byte geometry and immediate pause/cancel controls are 66 bytes. Scheduled
71
+ receipt batches, terminal receipts and CANCELs occupy existing 65,490-byte
72
+ cover slots. Handshake flights are suite- and step-dependent.
72
73
 
73
74
  Per-message channels give each transfer an independent lifecycle for
74
75
  cancellation, bounded channel accounting, receipts, selective retransmission,
75
76
  and reconnect resume. Channel isolation is a UX and reliability property; it
76
77
  does not make timing or channel count invisible.
77
78
 
79
+ ### Durable selective resume and cancellation
80
+
81
+ A chunk acknowledgement becomes live only after its per-recipient outbox
82
+ update commits. Receipt acceptance binds room, peer ID, peer public key,
83
+ random transfer ID, Merkle root and original chunk index. Scheduled cells
84
+ add direction, PQ epoch, AEAD and counter replay checks; their terminal token
85
+ must equal the transfer root. Stale asynchronous storage responses cannot
86
+ advance a replacement owner's live state. Recipient refusal is persisted
87
+ separately from completion and stops only that recipient's retry.
88
+
89
+ Scheduled chunk receipts use a separate encrypted subtype with up to 64 tokens
90
+ per root. Existing subtype 3 remains terminal-only: older clients interpreted
91
+ any token in that subtype as completion. Older protocol-v4 clients drop the
92
+ new subtype and retain terminal-only scheduled delivery, including full replay
93
+ when necessary. Selective scheduled resume therefore requires supporting peers.
94
+
95
+ Have-set membership is O(1) in memory and does not allocate a set copy per
96
+ scheduled slot. A resume scans indices once, sends missing real chunks and
97
+ may send one completion probe. Storage lookup costs, initial file preparation
98
+ and scanning remain; this is not a claim of sublinear total file processing.
99
+ Batching and selective resume reduce real work within the existing schedule,
100
+ whose observable cell count remains fixed.
101
+
102
+ Immediate resume uses the channel protocol `p2party-resume-v1` and a capability
103
+ echo to distinguish pause from explicit cancellation. Type-6 controls are
104
+ authenticated by the concrete DTLS/SCTP channel and current identity gate,
105
+ with an exact-root check; they do not have a separate application AEAD.
106
+ Legacy peers retain close-as-cancel behavior. A fresh immediate attempt
107
+ rebuilds its have-set from paced receiver receipts instead of assuming a
108
+ legacy receiver retained its partial. Remote cancellation drains pending
109
+ writes, then atomically checks room, sender and incomplete status before
110
+ deleting receiver data; completed files and sender self-copies are preserved.
111
+
112
+ ### Sparse-PQ healing during scheduled sends
113
+
114
+ Local healing waits for queued or admitted real jobs, message channels,
115
+ derive-to-enqueue reservations and queued receive work to drain. An atomic
116
+ application reservation closes the race between deriving a message key and
117
+ admitting its scheduled producer. Dummy cover does not block healing.
118
+
119
+ An incoming exchange blocks new admission and drains reservations, then
120
+ persists its new epoch before adopting it. Epoch adoption invalidates old-key
121
+ real producers on the same scheduler and wakes bounded sender recovery.
122
+ Recovery requires an actual epoch advance when reusing that runtime and
123
+ re-derives the missing chunks. If sealing was already in flight, its retired
124
+ cell is wiped and the slot remains dummy; pending explicit CANCEL is resealed
125
+ under the new epoch. Lane timing continues throughout this transition.
126
+ Persistence failure leaves the prior live epoch unchanged for exact retry.
127
+
78
128
  ## Guarantees, assuming authenticated peer keys
79
129
 
80
130
  With a correctly pinned peer Ed25519 identity, uncompromised endpoints, matching
@@ -126,6 +176,46 @@ Putting the capability in a URL fragment keeps it out of ordinary HTTP
126
176
  requests and common access logs, but the shipped legacy signaling path still
127
177
  receives its normalized value. A fragment is not a server-blind meeting point.
128
178
 
179
+ ## Signaling and TURN correctness boundary
180
+
181
+ SDK 0.14.10 makes legacy signaling state transitions correlated and bounded; it
182
+ does not make that signaling path blind.
183
+
184
+ - A room response is accepted only for the exact current socket, room
185
+ capability, and UUIDv4 request. A delayed response cannot configure another
186
+ room.
187
+ - Caller ICE configuration and signaling-managed TURN credentials are separate
188
+ inputs. Managed credentials carry an absolute expiry, are applied to every
189
+ live room edge before an ICE restart, refresh proactively, and are removed or
190
+ failed closed when they expire.
191
+ - `purgeRoom()` tears down local transports and durable state before waiting
192
+ for a correlated server acknowledgement. The leave is keyed by the stable
193
+ room capability rather than the server-assigned room UUID, so a purge racing
194
+ an in-flight join still revokes that membership. Matching `p2party-server`
195
+ 0.1.1 unlinks the authenticated peer from the room before acknowledging. An
196
+ offline or non-acknowledging server produces
197
+ `RemoteRoomRevocationUnconfirmedError`; it is never reported as confirmed.
198
+ - Signaling ingress, room rosters, live mesh edges, pending ICE, and channel
199
+ registries have explicit client-side bounds. Those bounds limit one client's
200
+ exposure; they are not server-side Sybil resistance or a global availability
201
+ proof.
202
+
203
+ TURN is a transport relay, not a privacy proxy. The legacy REST username is
204
+ expiry-bound but contains the client's Ed25519 public key, and the TURN
205
+ operator can observe source addresses, allocations, timing, and byte volume.
206
+ Neither relay use nor TLS makes the signaling operator unable to read SDP, ICE,
207
+ room capabilities, or membership.
208
+
209
+ The current challenge proves possession of the presented Ed25519 key to the
210
+ server by signing a fresh nonce. Its transcript is not bound to a server
211
+ identity or origin, so it does not resist a malicious server relaying another
212
+ server's nonce as a signing oracle. The legacy service also has no
213
+ cross-instance session-generation fence or Byzantine quorum. Consequently none
214
+ of malicious-rendezvous blindness, two-server BFT, social-graph hiding,
215
+ server-bound authentication, shutdown survival, or post-quantum signaling
216
+ authentication is a property of this path. Those remain requirements of the
217
+ versioned blind-rendezvous design.
218
+
129
219
  ## Shipped, implemented core, and research
130
220
 
131
221
  | Status | Exact boundary in 0.14 |
@@ -13,13 +13,33 @@ These are compiled into the shipped `libcrypto.wasm` or bundled into the
13
13
  package. Licences and notices are reproduced in
14
14
  [THIRD_PARTY_NOTICES.md](../THIRD_PARTY_NOTICES.md).
15
15
 
16
- | Project | Used for | Link |
17
- | ----------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ |
18
- | libsodium | X25519, Ed25519, ChaCha20-Poly1305, BLAKE2b, HKDF, Argon2, SHA-512 | [github.com/jedisct1/libsodium](https://github.com/jedisct1/libsodium) |
19
- | mlkem-native | ML-KEM-512/768/1024 (FIPS 203) | [github.com/pq-code-package/mlkem-native](https://github.com/pq-code-package/mlkem-native) |
20
- | Emscripten | Compiles the C cryptography to the pinned WASM module | [emscripten.org](https://emscripten.org/) |
21
- | BIP-39 English wordlist | The 24-word room-capability encoding | [BIP-39](https://github.com/bitcoin/bips/blob/master/bip-0039/bip-0039-wordlists.md) |
22
- | Redux Toolkit | Browser-root state store | [redux-toolkit.js.org](https://redux-toolkit.js.org/) |
16
+ | Project | Used for | Link |
17
+ | ----------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
18
+ | libsodium | X25519, Ed25519, ChaCha20-Poly1305, BLAKE2b, HKDF, Argon2, SHA-512 | [github.com/jedisct1/libsodium](https://github.com/jedisct1/libsodium) |
19
+ | mlkem-native | ML-KEM-512/768/1024 (FIPS 203) | [github.com/pq-code-package/mlkem-native](https://github.com/pq-code-package/mlkem-native) |
20
+ | Emscripten | Compiles the C cryptography to the pinned WASM module | [emscripten.org](https://emscripten.org/) |
21
+ | BIP-39 English wordlist | The 2,048 words only, shared by room invites and identity recovery phrases — see the note below | [BIP-39](https://github.com/bitcoin/bips/blob/master/bip-0039/bip-0039-wordlists.md) |
22
+ | Redux Toolkit | Browser-root state store | [redux-toolkit.js.org](https://redux-toolkit.js.org/) |
23
+
24
+ `src/utils/wordlist.json` is the BIP-39 English list, and that is the whole
25
+ extent of the relationship. Two unrelated encodings share it, and **neither
26
+ uses BIP-39 checksum semantics**:
27
+
28
+ - the 24-word room invite (`src/roomInvite.ts`), which encodes a 32-byte room
29
+ capability and checksums it with SHA-256 over the domain separator
30
+ `p2party/room-invite/checksum/v1\0`, under its own wordlist id
31
+ `p2party-invite-en-v1`; and
32
+ - the identity recovery phrase (`src/cryptography/mnemonic.ts`), which takes
33
+ 128–512 bits of entropy (12–48 words), checksums with the leading bits of
34
+ SHA-512, and derives its seed with argon2id rather than PBKDF2-HMAC-SHA512.
35
+ This is the phrase
36
+ [identity backup and restore](../README.md#back-up-and-restore-your-identity)
37
+ derives an Ed25519 identity from; the invite encodes a room capability and
38
+ restores no identity at all.
39
+
40
+ Both reject the canonical BIP-39 English test vectors — the invite decoder with
41
+ `Room invite checksum is invalid`, `validateMnemonic()` by returning `false` —
42
+ and no BIP-39 tool can restore either. Do not describe them as BIP-39.
23
43
 
24
44
  ## 2. Standards the wire format implements
25
45
 
@@ -152,6 +172,6 @@ third-party security audit. Cite the implementation:
152
172
  @software{p2party,
153
173
  title = {p2party: protocol-v4 end-to-end encryption over a WebRTC room mesh},
154
174
  url = {https://github.com/p2party/p2party-js},
155
- note = {Version 0.14.3}
175
+ note = {Version 0.14.10}
156
176
  }
157
177
  ```
@@ -381,6 +381,15 @@ The bootstrap ML-KEM exchange protects the initial root. Healing periodically
381
381
  re-runs it so a later post-quantum compromise cannot unwind an old session.
382
382
  The session owns the state machine; the caller owns scheduling and transport.
383
383
 
384
+ The browser SDK separately coordinates queued scheduled producers with epoch
385
+ changes, persists per-recipient chunk receipts, and implements WebRTC pause and
386
+ CANCEL controls. Those transport/outbox services are not part of
387
+ `p2party/session`. A session integrator must coordinate its own pending output
388
+ and recovery when `pqEpoch` changes; the session does not selectively resend
389
+ previously returned frames or interpret cover receipt subtypes. See the
390
+ [wire format](wire-format.md#scheduled-control-cells--65490-bytes) for the browser
391
+ contract.
392
+
384
393
  Control frames are the same 65,490-byte size as chunk frames, so the outer
385
394
  framing MUST record which kind a record is — the session will reject a control
386
395
  frame handed to `decrypt()` and vice versa.
@@ -450,8 +459,16 @@ interface SessionCryptoOptions {
450
459
 
451
460
  Supplying bytes is the reproducible, offline-safe path. Resolve the exported
452
461
  `p2party/libcrypto.wasm` package subpath or pin a self-hosted copy from the same
453
- release. When `wasmBinary` is omitted, the loader fetches the immutable
454
- versioned p2party CDN artifact and checks the build-pinned SHA-384 SRI.
462
+ release.
463
+
464
+ Omitting `wasmBinary` does not always mean a network call. On Node and Bun the
465
+ loader reads this release's WASM from the installed package — the file next to
466
+ the bundle — and hashes it against the build-pinned SHA-384 before use, so an
467
+ offline or air-gapped install needs no configuration. It falls back to the
468
+ immutable versioned p2party CDN artifact, under the same SRI, only when that
469
+ read fails or the digest does not match. In a browser, where there is no
470
+ filesystem, the CDN fetch is the only path. An explicit `setWasmSourceUrl()` is
471
+ honoured everywhere, including on Node, in preference to the packaged copy.
455
472
 
456
473
  See [Protocol-v4 security](protocol-v4-security.md) for the exact claims and
457
474
  non-claims of the session this API constructs.
@@ -1,26 +1,29 @@
1
1
  # Wire format
2
2
 
3
- Every byte here is derived from
4
- [`src/utils/constants.ts`](../src/utils/constants.ts), which is the single
5
- source of truth and is byte-matched in `utils.h`. For what these frames do and
6
- do not protect, read the
3
+ Chunk geometry is defined in
4
+ [`src/utils/constants.ts`](../src/utils/constants.ts) and byte-matched in
5
+ `utils.h`. Scheduled control subtypes are defined in
6
+ [`coverCell.ts`](../src/cryptography/coverCell.ts); immediate transfer controls
7
+ are defined in [`immediateDisposition.ts`](../src/handlers/immediateDisposition.ts).
8
+ For what these frames do and do not protect, read the
7
9
  [protocol-v4 security boundary](protocol-v4-security.md).
8
10
 
9
-
10
11
  ## Outer frame types
11
12
 
12
13
  One tag byte leads every frame on a data channel.
13
14
 
14
- | Tag | Name | Size on the wire | Carries |
15
- | --- | ------------ | ---------------- | ------------------------------- |
16
- | 1 | `HANDSHAKE` | step-dependent | HELLO, CONFIRM, FINISH |
17
- | 2 | `CHUNK` | 65,490 B | one fixed application cell |
18
- | 3 | `RECEIPT` | 65 B | SHA-512 acknowledgement token |
19
- | 4 | `COVER` | 65,490 B | a scheduled cell, real or decoy |
20
- | 5 | `PQ_CONTROL` | 65,490 B | sparse-PQ OFFER / ADVANCE / ACK |
15
+ | Tag | Name | Size on the wire | Carries |
16
+ | --- | ----------------------- | ---------------- | ------------------------------------- |
17
+ | 1 | `HANDSHAKE` | step-dependent | HELLO, CONFIRM, FINISH |
18
+ | 2 | `CHUNK` | 65,490 B | one fixed application cell |
19
+ | 3 | `RECEIPT` | 65 B | SHA-512 acknowledgement token |
20
+ | 4 | `COVER` | 65,490 B | scheduled dummy or encrypted control |
21
+ | 5 | `PQ_CONTROL` | 65,490 B | sparse-PQ OFFER / ADVANCE / ACK |
22
+ | 6 | `IMMEDIATE_DISPOSITION` | 66 B | immediate resume capability or CANCEL |
21
23
 
22
- Tags 2, 4, and 5 are deliberately identical in size. An observer cannot tell an
23
- application cell from a decoy or from a healing exchange by looking at the wire.
24
+ Tags 2, 4, and 5 have equal payload lengths. The frame tags travel inside
25
+ WebRTC's DTLS transport; packetization, timing and total traffic remain
26
+ observable. Scheduled real data uses tag 2 in a scheduled lane slot.
24
27
 
25
28
  ## Chunk frame — 65,490 bytes
26
29
 
@@ -52,6 +55,81 @@ application cell from a decoy or from a healing exchange by looking at the wire.
52
55
 
53
56
  Both per-chunk acknowledgements and the terminal content-hash acknowledgement
54
57
  use this exact geometry, so a completion is not distinguishable by size.
58
+ Chunk tokens bind the transfer Merkle root, original chunk index and leaf hash.
59
+ The sender resolves them through its staged chunk index and persists the
60
+ per-recipient acknowledgement before updating the live have-set. A terminal
61
+ immediate receipt must equal the message's content hash. These frames use the
62
+ authenticated concrete message channel and its current identity gate.
63
+
64
+ ## Scheduled control cells — 65,490 bytes
65
+
66
+ ```text
67
+ type=4(1) || edge binding(32) || counter(8) || reserved zero(8) ||
68
+ PQ epoch(8) || nonce(12) || ciphertext(65,405) || AEAD tag(16)
69
+ ```
70
+
71
+ All 69 header bytes are authenticated. The key binds the room's suite, edge,
72
+ direction and PQ epoch. After authentication and counter replay rejection,
73
+ the plaintext is `subtype(1) || payload length(4, BE) || payload || zero pad`:
74
+
75
+ | Subtype | Meaning | Payload |
76
+ | ------- | ---------------- | ---------------------------------------------------------------------- |
77
+ | 1 | Dummy | Empty |
78
+ | 2 | CANCEL | Transfer Merkle root (64 B) |
79
+ | 3 | Terminal receipt | Transfer Merkle root (64 B), followed by the same root as token (64 B) |
80
+ | 4 | Chunk receipts | Transfer Merkle root (64 B), followed by 1–64 chunk tokens (64 B each) |
81
+
82
+ Subtype 4's authenticated payload length determines the token count; partial
83
+ tokens, empty batches and batches over 64 tokens are rejected. Only tokens for
84
+ one transfer root share a batch. A subtype 3 token unequal to its root cannot
85
+ complete a send. Each chunk token must resolve to the tracked transfer, root
86
+ and a valid original index. The durable owner also binds room, recipient ID
87
+ and public key; a stale owner cannot advance live state after a storage await.
88
+ A recipient's CANCEL records refusal only for that recipient.
89
+
90
+ Receipts and CANCELs replace dummy slots in the existing schedule. The pending
91
+ outbound control queue is bounded to 4,096 token/control units; dropping a
92
+ queued acknowledgement may require a later retransmission. A terminal receipt
93
+ supersedes unsent chunk acknowledgements for the same root, preserving the
94
+ first available reverse slot for completion even at long cadences. Other roots
95
+ and queued CANCEL controls retain their order. Live have-set
96
+ membership uses an O(1) `Set` lookup without copying the set per slot. A resumed
97
+ producer scans original indices once and emits missing chunks. It also selects
98
+ index zero once per attempt to recover a lost terminal receipt: if that index
99
+ was already acknowledged, this costs one extra completion probe. Index scanning
100
+ and durable token lookups do not imply globally sublinear processing, and fewer
101
+ real chunks do not reduce the fixed scheduled cell count.
102
+
103
+ Subtype 4 is a protocol-v4 wire addition. Earlier clients reject and drop this
104
+ unknown subtype and keep using subtype 3 completion. They cannot selectively
105
+ resume scheduled chunks; a missing terminal receipt can require whole-transfer
106
+ replay. Subtype 3 is never reused for chunk tokens, because earlier clients
107
+ treated any subtype 3 receipt for a tracked root as completion. These additions
108
+ do not introduce a fallback to an earlier cryptographic protocol version.
109
+
110
+ ## Immediate pause and cancellation — 66 bytes
111
+
112
+ ```text
113
+ type=6(1) || kind(1) || transfer Merkle root(64)
114
+ ```
115
+
116
+ Kind 0 announces resume support; kind 1 is explicit CANCEL. An immediate
117
+ message channel opened with DCEP protocol `p2party-resume-v1` promises that a
118
+ bare close may pause the transfer. A supporting receiver echoes kind 0 after
119
+ the identity gate opens; the opener then also treats a bare remote close as
120
+ pause. CANCEL is accepted once per concrete channel, only under its current
121
+ authenticated gate and exact root. Malformed, unknown, stale and cross-root
122
+ controls have no effect. This control relies on the authenticated DTLS/SCTP
123
+ channel; it has no separate application AEAD envelope.
124
+
125
+ An explicit cancellation sends kind 1 before closing. A transient failed
126
+ attempt closes without CANCEL, allowing a supporting receiver to retain its
127
+ partial data for bounded resume. Earlier clients ignore type 6 and retain
128
+ their close-as-cancel behavior. At a fresh immediate attempt the new sender
129
+ starts a fresh live chunk have-set and rebuilds it from the receiver's paced
130
+ receipt replay, so deleted legacy partials cause full resend
131
+ instead of trusting stale acknowledgements. The store-free session API does
132
+ not implement this WebRTC channel contract.
55
133
 
56
134
  ## Handshake ladder
57
135
 
@@ -88,6 +166,16 @@ rather than something both sides have to reconcile. Each side persists its
88
166
  mutated state **before** dispatching, and application traffic is blocked while
89
167
  an epoch is in flight.
90
168
 
169
+ The browser scheduler starts a local exchange only when no real producer,
170
+ derive-to-enqueue reservation, message channel or queued receive work remains.
171
+ Dummy/control cover alone does not hold it back. A remote exchange blocks new
172
+ application admission and drains reservations before the durable transition;
173
+ it does not wait for a long scheduled send to finish. A committed new epoch
174
+ retires old-key real producers and publishes a cover generation change on the
175
+ same runtime. The sender re-derives the remaining transfer under that epoch;
176
+ fixed lane timing continues with dummy tails. An unrelated generation notice
177
+ cannot authorize reuse of the same runtime without an actual PQ epoch advance.
178
+
91
179
  ## Room policy — 32 bytes
92
180
 
93
181
  A room's policy is a fixed 32-byte record (magic `"P2RP"`) that pins the ML-KEM
@@ -1,15 +1,64 @@
1
+ import { type RequestRoomLeaveOptions } from "../utils/roomLeaveCoordinator";
1
2
  import type { BaseQueryFn } from "@reduxjs/toolkit/query";
2
- import type { WebSocketMessageCandidateSend, WebSocketMessageDescriptionSend, WebSocketMessageChallengeResponse, WebSocketMessageRoomIdRequest, WebSocketMessagePeersRequest, WebSocketMessageConnectionResponse, WebSocketMessagePongResponse, WebSocketPeerConnectionParams } from "../utils/interfaces";
3
+ import type { WebSocketMessageCandidateSend, WebSocketMessageDescriptionSend, WebSocketMessageChallengeResponse, WebSocketMessageRoomIdRequest, WebSocketMessagePeersRequest, WebSocketMessageConnectionResponse, WebSocketMessagePongResponse, WebSocketMessageLeaveRoomRequest, WebSocketPeerConnectionParams, WebSocketMessagePeerConnectionRequest } from "../utils/interfaces";
4
+ import type { BaseQueryApi } from "@reduxjs/toolkit/query";
3
5
  export interface WebSocketParams {
4
6
  signalingServerUrl: string;
5
7
  }
6
8
  export interface WebSocketMessage {
7
- content: WebSocketMessagePongResponse | WebSocketMessageCandidateSend | WebSocketMessageDescriptionSend | WebSocketMessageChallengeResponse | WebSocketMessageRoomIdRequest | WebSocketMessagePeersRequest | WebSocketMessageConnectionResponse;
9
+ content: WebSocketMessagePongResponse | WebSocketMessageCandidateSend | WebSocketMessageDescriptionSend | WebSocketMessageChallengeResponse | WebSocketMessageRoomIdRequest | WebSocketMessageLeaveRoomRequest | WebSocketMessagePeersRequest | WebSocketMessageConnectionResponse | WebSocketMessagePeerConnectionRequest;
8
10
  }
11
+ /**
12
+ * The host the reconnect controller reads Redux state from and dispatches
13
+ * through. Bound on every connect attempt; the tests rebind it directly. It is
14
+ * NOT the store import, so this module keeps its existing dependency edges.
15
+ */
16
+ type SignalingReconnectHost = {
17
+ dispatch: (action: unknown) => unknown;
18
+ getState: () => unknown;
19
+ };
20
+ export declare const setSignalingReconnectHost: (host: SignalingReconnectHost | null) => void;
21
+ /**
22
+ * Record how a socket ended, so the reconnect ladder can refuse a close the
23
+ * server means as final. `selfInitiated` is what keeps the SDK's own protective
24
+ * closes out of the policy: `onOverflow` closes with 1008 when this tab's
25
+ * ingress backlog is exceeded, and local overload is exactly what bounded
26
+ * backoff is for.
27
+ */
28
+ export declare const noteSignalingSocketClose: (close: Readonly<{
29
+ code: number;
30
+ selfInitiated: boolean;
31
+ }>) => void;
32
+ /**
33
+ * A ladder rung is a floor, never shortened, and at most 25 % longer. Without
34
+ * this every client a server restart disconnected walks the identical
35
+ * 1/2/4/8 s ladder and re-handshakes in lockstep, which is the same stampede
36
+ * the restart caused. The spread lives here rather than in the controller
37
+ * because the controller's ladder is exact by contract and its tests assert
38
+ * exact delays; the scheduler this module injects is the seam that perturbs it.
39
+ */
40
+ export declare const signalingReconnectDelayWithJitter: (delayMs: number) => number;
41
+ /**
42
+ * C4: a socket close that carries no browser wake event is otherwise terminal.
43
+ * `settleClosedSocket` detects; this reconnects. Shared by the close handler,
44
+ * the error handler and the heartbeat watchdog, so a peer whose socket died
45
+ * while its edges stayed authenticated is found again within 1-30 s instead of
46
+ * never.
47
+ */
48
+ export declare const signalingReconnect: import("../utils/signalingReconnectController").SignalingReconnectController;
49
+ type CurrentSocketApi = Pick<BaseQueryApi, "dispatch" | "getState">;
50
+ /**
51
+ * Send a correlated leave on the exact authenticated socket and wait for its
52
+ * exact acknowledgement. The caller must make local room transport terminal
53
+ * before invoking this network-only boundary.
54
+ */
55
+ export declare const requestCurrentSocketRoomLeave: (api: CurrentSocketApi, roomUrl: string, options?: RequestRoomLeaveOptions) => Promise<void>;
9
56
  declare const signalingServerApi: import("@reduxjs/toolkit/query").Api<BaseQueryFn<WebSocketParams, undefined>, {
10
57
  connectWebSocket: import("@reduxjs/toolkit/query").MutationDefinition<string, BaseQueryFn<WebSocketParams, undefined>, never, undefined, "signalingServerApi", undefined>;
11
58
  disconnectWebSocket: import("@reduxjs/toolkit/query").MutationDefinition<undefined, BaseQueryFn<WebSocketParams, undefined>, never, undefined, "signalingServerApi", undefined>;
12
59
  sendMessage: import("@reduxjs/toolkit/query").MutationDefinition<WebSocketMessage, BaseQueryFn<WebSocketParams, undefined>, never, undefined, "signalingServerApi", undefined>;
13
60
  connectWithPeer: import("@reduxjs/toolkit/query").MutationDefinition<WebSocketPeerConnectionParams, BaseQueryFn<WebSocketParams, undefined>, never, undefined, "signalingServerApi", undefined>;
14
61
  }, "signalingServerApi", never, typeof import("@reduxjs/toolkit/query").coreModuleName>;
62
+ /** Queue one room request or reject immediately when RTK reports no send. */
63
+ export declare const sendRoomRequestMessage: (api: Pick<BaseQueryApi, "dispatch">, request: WebSocketMessageRoomIdRequest) => Promise<void>;
15
64
  export default signalingServerApi;
@@ -3,11 +3,20 @@ import type { RatchetState } from "../../cryptography/ratchet";
3
3
  import type { RatchetGateLease } from "../../handlers/ratchetGate";
4
4
  import type { CoverRuntime } from "../../handlers/coverRuntime";
5
5
  import type { SparsePqHealingState } from "../../handlers/pqHealingRuntime";
6
+ import type { PeerHandshakeFailureReason } from "../../reducers/roomSlice";
7
+ import type { PeerSocketGenerationSource } from "./peerSocketGeneration";
6
8
  export interface IRTCPeerConnection extends RTCPeerConnection {
7
9
  /** The room this transport belongs to. A room/peer pair owns one PC. */
8
10
  roomId: string;
9
11
  withPeerId: string;
10
12
  withPeerPublicKey: string;
13
+ /**
14
+ * The peer signaling socket this transport was built against. A different
15
+ * generation for the same peerId means the peer's session was replaced, and
16
+ * this transport can never be signalled again (C1). Absent against a
17
+ * pre-generation server, where it must never be read as a change.
18
+ */
19
+ peerSocketGeneration?: string;
11
20
  makingOffer: boolean;
12
21
  ignoreOffer: boolean;
13
22
  receiveMessageModule: LibCrypto;
@@ -16,8 +25,42 @@ export interface IRTCPeerConnection extends RTCPeerConnection {
16
25
  mainChannel?: IRTCDataChannel;
17
26
  /** Gate ownership captured when this concrete transport is created. */
18
27
  ratchetGateLease: RatchetGateLease;
28
+ /**
29
+ * Set when this edge's handshake failed for a reason a fresh transport
30
+ * cannot fix (a wrong PIN, a PIN throttle, a policy/suite mismatch). The
31
+ * `main` channel's close handler consults it so a terminal failure is never
32
+ * followed by a re-dial. Cleared only by building a new transport.
33
+ */
34
+ handshakeTerminalFailure?: PeerHandshakeFailureReason;
35
+ /**
36
+ * How many times this RTCPeerConnection has been re-bound IN PLACE onto a
37
+ * new remote DTLS certificate since it last authenticated. The re-bind
38
+ * budget (see decideRemoteTransportChangeResponse) reads it to guarantee
39
+ * that a peer which keeps presenting fresh certificates converges on a
40
+ * replacement transport instead of looping; the handshake success path
41
+ * clears it.
42
+ */
43
+ remoteTransportRebinds: number;
44
+ /**
45
+ * Deadline armed by the in-place re-bind above: if this transport has not
46
+ * re-authenticated when it fires, the edge is torn down and rebuilt instead
47
+ * of waiting out the peer's 30 s handshake step timeout. Cleared by the
48
+ * handshake success path, by the teardown, and re-armed by a later re-bind.
49
+ */
50
+ rebindDeadline?: ReturnType<typeof setTimeout>;
51
+ /**
52
+ * The `clearTimeout` that matches whatever scheduled `rebindDeadline`. The
53
+ * arming site owns the scheduler (tests inject one), while the clearing
54
+ * sites — handshake success, peer teardown — only hold the connection.
55
+ */
56
+ rebindDeadlineClear?: (handle: ReturnType<typeof setTimeout>) => void;
19
57
  /** Resolves once the per-transport WASM dependency is ready. */
20
58
  initialization?: Promise<void>;
59
+ /**
60
+ * Re-proves signaling and applies the latest room ICE configuration before
61
+ * any restart. Installed by the connection owner and shared by repair paths.
62
+ */
63
+ ensureFreshIceConfiguration?: () => Promise<boolean | void>;
21
64
  ratchetState?: RatchetState;
22
65
  ratchetEstablished?: Promise<void>;
23
66
  messageKeyCache?: Map<string, Uint8Array>;
@@ -61,6 +104,13 @@ export interface IRTCDataChannel extends RTCDataChannel {
61
104
  * delete its storage artifacts after the active handler reaches quiescence.
62
105
  */
63
106
  cancelReceiveTransfer?: () => Promise<void>;
107
+ /**
108
+ * Set by any close THIS side initiated (the pre-authentication frame guard,
109
+ * the transfer's terminal-close runner, the resume path's cleanup). Read by
110
+ * `isAuthenticatedPeerCancel` so our own close is never reported as the peer
111
+ * cancelling the transfer (C7).
112
+ */
113
+ closedLocally?: boolean;
64
114
  }
65
115
  export interface IRTCMessage {
66
116
  id: string;
@@ -79,6 +129,15 @@ export interface RTCPeerConnectionParams {
79
129
  peerPublicKey: string;
80
130
  roomId: string;
81
131
  rtcConfig?: RTCConfiguration;
132
+ /** The peer's signaling-socket generation, as last named by the server. */
133
+ peerSocketGeneration?: string;
134
+ /**
135
+ * Which inbound frame named that generation. Only a `connection` frame
136
+ * proves a fresh remote transport; a roster introduction is record-only
137
+ * while this side's transport is connected (see peerSocketGeneration).
138
+ * Omitted means advisory.
139
+ */
140
+ peerSocketGenerationSource?: PeerSocketGenerationSource;
82
141
  }
83
142
  export interface RTCSetDescriptionParams {
84
143
  peerId: string;
@@ -92,6 +151,14 @@ export interface RTCSetCandidateParams {
92
151
  roomId: string;
93
152
  candidate: RTCIceCandidateInit | RTCIceCandidate;
94
153
  }
154
+ export interface RTCApplyRoomConfigurationParams {
155
+ roomId: string;
156
+ rtcConfig: RTCConfiguration;
157
+ /** Gather a new ICE generation after every live PC accepts the config. */
158
+ restartIce?: boolean;
159
+ /** Expiry must close on failure; rollback would restore expired secrets. */
160
+ failurePolicy?: "rollback" | "close";
161
+ }
95
162
  export interface RTCOpenChannelParams {
96
163
  roomId: string;
97
164
  channel: string | RTCDataChannel;
@@ -103,7 +170,7 @@ export interface RTCOpenChannelParams {
103
170
  export interface RTCSendMessageParams {
104
171
  /** Random 32-byte lowercase-hex identity allocated by the public API. */
105
172
  transferId: string;
106
- data: string | File;
173
+ data?: string | File;
107
174
  label: string;
108
175
  roomId: string;
109
176
  minChunks?: number;
@@ -148,6 +215,8 @@ export interface RTCDisconnectFromPeerParams {
148
215
  roomId?: string;
149
216
  alsoDeleteData?: boolean;
150
217
  }
218
+ /** Internal dependency injected into bulk teardown queries to avoid API cycles. */
219
+ export type DisconnectPeerTransport = (params: RTCDisconnectFromPeerParams) => Promise<void>;
151
220
  export interface RTCDisconnectFromChannelLabelParams {
152
221
  roomId: string;
153
222
  label: string;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Whether a peerId still names the same signaling session.
3
+ *
4
+ * The server binds one stable peerId to an identity public key, so a second
5
+ * tab or a phrase-restored second device reconnects as the SAME peerId and
6
+ * evicts its predecessor. Without a per-socket generation the edge stayed
7
+ * bound to the evicted socket: the transport never failed (no SDP, no ICE
8
+ * event), so nothing rebuilt it, and a responder on the new session creates no
9
+ * channel and can never offer — the split was permanent (C1).
10
+ *
11
+ * "unknown" is the fail-safe verdict: an absent or malformed generation (a
12
+ * pre-generation server, or a fresh transport that has not recorded one yet)
13
+ * must behave exactly as 0.14.7 does, never as a teardown trigger. The server
14
+ * leaves the field empty until a socket has proved ownership of its key and
15
+ * empties it again whenever a fresh challenge is issued, so "" and undefined
16
+ * are the same statement: no information.
17
+ */
18
+ export type PeerSocketGenerationVerdict = "unknown" | "unchanged" | "changed";
19
+ /**
20
+ * Which inbound frame carried a generation, and therefore how much authority
21
+ * it has.
22
+ *
23
+ * - "connection": the peer's own `connection` frame. It is emitted from one
24
+ * place only (handleConnectToPeer's connectWithPeer), and that place is
25
+ * reachable only after a FRESH transport has been built — both baseQuery and
26
+ * handleConnectToPeer early-return on a live one. A changed generation there
27
+ * always means a genuinely new remote transport.
28
+ * - "roster": a `peers` introduction. The roster is re-requested on join, on
29
+ * re-dial AND on reconcile, and a signaling socket now reconnects on its
30
+ * own, so a benign reconnect of a healthy session routinely delivers a
31
+ * changed generation for an edge that is working perfectly. Acting on that
32
+ * is a regression of cluster C4, so it is record-only while the local
33
+ * transport is connected.
34
+ */
35
+ export type PeerSocketGenerationSource = "connection" | "roster";
36
+ export declare const isCanonicalSocketGeneration: (value: unknown) => value is string;
37
+ export declare const comparePeerSocketGeneration: (recorded: string | undefined, observed: unknown) => PeerSocketGenerationVerdict;
38
+ export declare const markPeerSocketGenerationRetired: (roomId: string, peerId: string, generation: unknown) => void;
39
+ export declare const isPeerSocketGenerationRetired: (roomId: string, peerId: string, generation: unknown) => boolean;
40
+ export declare const clearRetiredPeerSocketGenerations: (roomId: string, peerId: string) => void;
41
+ export interface PeerSocketGenerationObservation {
42
+ roomId: string;
43
+ peerId: string;
44
+ /** The generation the local transport was built against, if any. */
45
+ recorded: string | undefined;
46
+ /** The generation this frame names for the peer. */
47
+ observed: unknown;
48
+ /** Which inbound frame carried it; an unattributed frame is advisory. */
49
+ source: PeerSocketGenerationSource | undefined;
50
+ /** The local transport's current state. */
51
+ connectionState: RTCPeerConnectionState;
52
+ }
53
+ /**
54
+ * Whether an observed generation is reason enough to retire the local
55
+ * transport for this edge and rebuild it against the peer's live session.
56
+ *
57
+ * Deliberately conservative in two directions. A roster introduction is acted
58
+ * on only when the local transport is demonstrably not working — a healthy
59
+ * edge must survive an ordinary signaling reconnect (C4). And a generation
60
+ * this edge has already retired never triggers a second teardown, however it
61
+ * arrives.
62
+ *
63
+ * "not working" excludes "disconnected". handleConnectToPeer answers that state
64
+ * with an ICE restart and only tears the edge down if it is still disconnected
65
+ * 8 s later, and the restart usually succeeds — so a roster frame arriving in
66
+ * that window would replace a transport that was about to heal itself.
67
+ */
68
+ export declare const shouldRetireTransportForSocketGeneration: ({ roomId, peerId, recorded, observed, source, connectionState, }: PeerSocketGenerationObservation) => boolean;
@@ -5,6 +5,8 @@ export declare const COVER_CELL_PLAINTEXT_BYTES: number;
5
5
  export declare const COVER_CELL_BINDING_BYTES = 32;
6
6
  export declare const COVER_CELL_ROOT_BYTES = 32;
7
7
  export declare const COVER_CELL_TOKEN_BYTES: number;
8
+ /** Bound token lookup/persistence work per authenticated scheduled cell. */
9
+ export declare const MAX_COVER_CHUNK_RECEIPTS = 64;
8
10
  export declare const COVER_CELL_MAX_PAYLOAD_BYTES: number;
9
11
  export type CoverCellDirection = "initiator-to-responder" | "responder-to-initiator";
10
12
  export type CoverCellContent = {
@@ -21,6 +23,11 @@ export type CoverCellContent = {
21
23
  readonly subtype: "receipt";
22
24
  readonly merkleRoot: Uint8Array;
23
25
  readonly token: Uint8Array;
26
+ } | {
27
+ /** Subtype 4: old clients drop this unknown subtype, never treat it as completion. */
28
+ readonly subtype: "chunk-receipts";
29
+ readonly merkleRoot: Uint8Array;
30
+ readonly tokens: readonly Uint8Array[];
24
31
  };
25
32
  export type CoverCellErrorCode = "authentication-failed" | "binding-mismatch" | "epoch-mismatch" | "invalid-cell" | "invalid-direction" | "invalid-padding" | "replayed-counter";
26
33
  export declare class CoverCellError extends Error {