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
package/README.md CHANGED
@@ -38,15 +38,24 @@ Apache-2.0 · [LICENSE.md](LICENSE.md)
38
38
  Browser builds use IndexedDB and, where available, OPFS for disk-backed large
39
39
  file receipt.
40
40
  - Room capabilities have a compact 43-character base64url form, a versioned
41
- fragment invite, and an optional checksum-protected 24-word
42
- [BIP-39](https://github.com/bitcoin/bips/blob/master/bip-0039/bip-0039-wordlists.md)
43
- representation.
41
+ fragment invite, and an optional checksum-protected 24-word representation
42
+ drawn from the
43
+ [BIP-39 English wordlist](https://github.com/bitcoin/bips/blob/master/bip-0039/bip-0039-wordlists.md).
44
+ It borrows the words, not the format: the checksum is SHA-256 over p2party's
45
+ own domain separator, so a BIP-39 phrase is not a valid invite. See
46
+ [docs/references.md](docs/references.md).
47
+ - An identity created from a recovery phrase can be restored on another device:
48
+ 12 to 48 words drawn from the same wordlist, checksummed with SHA-512 and
49
+ stretched into the Ed25519 seed with argon2id. The derivation runs one way, so
50
+ a randomly generated identity cannot be exported — it can only be replaced by
51
+ a recoverable one. See
52
+ [Back up and restore your identity](#back-up-and-restore-your-identity).
44
53
  - `p2party/session` exposes the cryptography without
45
54
  [Redux](https://redux-toolkit.js.org/), IndexedDB, WebRTC, signaling,
46
55
  `window`, or `localStorage`.
47
56
 
48
57
  Immediate delivery over the existing signaling rendezvous is the shipped
49
- default. Scheduled timing cover is also wired as of 0.14.2: a room policy may
58
+ default. Scheduled timing cover is also wired: a room policy may
50
59
  pin a cadence, lane count, and frames per cell, and every edge in the room then
51
60
  emits fixed-size cells on that schedule whether or not there is data to send.
52
61
  Sparse post-quantum healing (the OFFER/ADVANCE/ACK epoch exchange) is likewise
@@ -81,7 +90,7 @@ npm audit signatures
81
90
 
82
91
  To build the artifact yourself instead, see
83
92
  [Building from source](CONTRIBUTING.md#building-from-source). That path needs an
84
- exact toolchain (Node 24, Emscripten 6.0.3, pinned submodules), because the
93
+ exact toolchain (Node 24, Emscripten 6.0.9, pinned submodules), because the
85
94
  release build reproduces the pinned WASM and refuses to emit an artifact it
86
95
  cannot attest.
87
96
 
@@ -192,6 +201,63 @@ protocol-v4 acknowledgement: p2party next runs its authenticated HELLO plus
192
201
  three chained confirmation flights over the main channel. Message receipts are
193
202
  a third, delivery-level acknowledgement.
194
203
 
204
+ ### When a peer sleeps
205
+
206
+ A phone that backgrounds with its radio off for less than WebRTC's ~30 s ICE
207
+ consent timeout used to wake into a permanent split. Its peer had already torn
208
+ that edge down and built a fresh one, but the woken side still read
209
+ `connected`, reused the dead transport, and let its zombie `main` channel block
210
+ a replacement — so no handshake ever ran on the peer's new transport. One side
211
+ showed the peer reachable and every send failed undelivered; the other showed
212
+ no handshake at all and its sends hung forever. Only a manual reconnect cleared
213
+ it.
214
+
215
+ Seven behaviours now hold the edge together, and each is what the library does
216
+ rather than a guarantee about your network:
217
+
218
+ - **A peer edge is bound to the DTLS certificate it authenticated against.**
219
+ When the remote certificate changes, an already-authenticated transport is
220
+ replaced outright. One that never authenticated is re-authenticated in place
221
+ instead, at most once, so a peer that keeps presenting fresh certificates
222
+ still converges on a replacement rather than looping.
223
+ - **Descriptions from a retired transport are discarded**, so an answer
224
+ belonging to a dead connection can no longer poison the one that replaced it.
225
+ - **Every failure teardown re-dials the room** under its own debounce budget.
226
+ Deliberate teardowns — `disconnectFromPeer`, leaving, address-book removal —
227
+ do not. A superseded handshake attempt dies quietly instead of tearing down
228
+ the transport that superseded it.
229
+ - **A send to a peer that never authenticates fails inside a bounded wait**
230
+ with a stated reason, rather than hanging on the ratchet gate forever.
231
+ - **A half-open signaling socket is detected**, by a heartbeat watchdog (25 s
232
+ with no server ping) and on wake events, and marked disconnected so a
233
+ reconnect can run.
234
+ - **RTCPeerConnection operations settle when the connection closes underneath
235
+ them**, so a closing connection cannot strand the per-peer lock and wedge the
236
+ socket's ingress queue.
237
+ - **An interrupted transfer resumes across an edge re-authentication.** The
238
+ transfer's cipher and its cancel detection follow the cryptographic
239
+ generation rather than the connection object, so a channel closed by a
240
+ re-authentication reads as a resume, not as a peer cancelling on you.
241
+
242
+ Verified with two separate headless-Chromium processes against a Bun relay
243
+ mirroring the signaling server, one process SIGSTOPped whole-tree with its UDP
244
+ host-candidate buffers flooded — a phone asleep with the radio off. Across
245
+ seven scenarios (baseline, socket drop, a 20 s freeze in each identity role
246
+ with the socket dropped and with it kept, and a 45 s freeze) both sides
247
+ reconverge and exchange byte-exact messages 0.5–6.5 s after wake, and four
248
+ in-flight 8 MiB transfer configurations finish byte-exact with the sender
249
+ reporting `delivered`.
250
+
251
+ Two limits, since they are as real as the fixes. Resume follows a live page: a
252
+ send in flight does not survive reloading the sending page, though a reloaded
253
+ receiver replays receipts for what it already holds so a still-live sender
254
+ resends only the gaps. And when a frozen identity responder is carrying an
255
+ in-flight transfer, reconvergence takes about 37 s rather than the usual 5–7 s
256
+ — the surviving SCTP association accepts our new streams but not the peer's, so
257
+ recovery waits out the 30 s handshake step timeout. The transfer completes
258
+ correctly; the latency is a known open item on the
259
+ [roadmap](ROADMAP.md#near-term).
260
+
195
261
  ## Wire format
196
262
 
197
263
  Fixed 65,490-byte cells, a 65-byte receipt frame, and outer frame tags that make
@@ -331,6 +397,98 @@ shape; [docs/getting-started.md](docs/getting-started.md#send-cancel-and-read)
331
397
  covers reading inbound messages and the metadata-only read that avoids
332
398
  materializing large files.
333
399
 
400
+ ## Back up and restore your identity
401
+
402
+ An identity is an Ed25519 key pair in browser storage. Lose the profile and you
403
+ lose the identity — unless it was created from a recovery phrase, because the
404
+ phrase is the only thing that can reproduce it.
405
+
406
+ Three calls, introduced together: `getIdentityBackupStatus()` reports whether
407
+ the identity in use can be backed up at all, `createRecoverableIdentity()`
408
+ replaces it with one a phrase derives and hands back that phrase once, and
409
+ `restoreIdentityFromMnemonic()` adopts the identity a phrase encodes — on this
410
+ device or any other.
411
+
412
+ ```ts
413
+ // Can the identity in use be backed up at all?
414
+ const status = await p2party.getIdentityBackupStatus();
415
+ // status.derivation === "mnemonic" -> a phrase restores it
416
+ // status.derivation === "random" -> nothing can; it can only be replaced
417
+ // status.derivation === null -> unknown: no identity is stored yet, or
418
+ // one whose provenance was never recorded.
419
+ // Treat it as having no backup.
420
+
421
+ if (status.derivation !== "mnemonic") {
422
+ // Replaces the identity in use and hands back the only copy of the phrase.
423
+ const { mnemonic, publicKey } = await p2party.createRecoverableIdentity({
424
+ strength: 256, // 24 words; 128 gives 12. Default 256.
425
+ // password: folded into the derivation, and just as unrecoverable.
426
+ });
427
+
428
+ // Show it once, in the UI. Never log it: console output is storage, and
429
+ // devtools, extensions and webview log shippers all keep it.
430
+ showRecoveryPhraseOnce(mnemonic);
431
+ console.log("recoverable identity", publicKey);
432
+ }
433
+ ```
434
+
435
+ On another device, before connecting, adopt the identity a phrase encodes:
436
+
437
+ ```ts
438
+ try {
439
+ const { publicKey } = await p2party.restoreIdentityFromMnemonic(
440
+ typedPhrase,
441
+ password, // omit unless one was used at creation
442
+ );
443
+ console.log("restored", publicKey);
444
+ } catch (error) {
445
+ if (error instanceof p2party.InvalidRecoveryPhraseError) {
446
+ // A typo. Nothing was disconnected and nothing was replaced, so keep the
447
+ // form open and let the user fix it.
448
+ }
449
+ }
450
+
451
+ // Both calls leave signaling disconnected. Come back online yourself.
452
+ await p2party.connect(roomUrl);
453
+ ```
454
+
455
+ **What the phrase is.** 12 to 48 words from the shared wordlist, checksummed
456
+ with SHA-512 and stretched by argon2id — salted with the optional password —
457
+ into the Ed25519 seed. It borrows the BIP-39 English words and nothing else, so
458
+ no BIP-39 wallet can restore one of these identities —
459
+ [docs/references.md](docs/references.md) has the exact differences. The
460
+ derivation runs one way: an identity can be _created_ from a phrase, never
461
+ exported to one.
462
+
463
+ **Existing identities cannot be exported.** Every identity the SDK generated
464
+ before this — and any it still generates on first connect — comes from a random
465
+ seed, so no phrase describes it. A newly generated one is recorded as
466
+ `"random"`; one that predates the record, or whose record no longer binds to
467
+ the stored key, reads `null`. Both mean the same thing to an app, and reporting
468
+ it is the point: say the identity has no backup instead of offering one that
469
+ cannot exist. The only route to a recoverable identity is
470
+ `createRecoverableIdentity()`, which _replaces_ the current one: contacts see a
471
+ new identity key for this device and their clients flag the change, exactly as
472
+ they would after a purge.
473
+
474
+ **The phrase is shown once and stored nowhere.** Neither the SDK nor its
475
+ storage keeps a copy, by design — a phrase at rest is the identity at rest — so
476
+ an app that drops it before the user has written it down has lost the backup.
477
+ The password is not stored or checked either: restoring with a different one
478
+ silently derives a different identity.
479
+
480
+ **What restoring does to a running mesh.** It rotates the account key, so it
481
+ takes the same exclusive path a purge does: signaling is disconnected, every
482
+ room's WebRTC transport is brought to a terminal state, and the cross-signed
483
+ X25519 identity is dropped so the next connection regenerates and re-signs it.
484
+ Rooms, messages, the address book and cached room PINs are all kept — reconnect
485
+ and you are in the same rooms under the restored key. An invalid phrase rejects
486
+ with `InvalidRecoveryPhraseError` before any of that starts.
487
+
488
+ While a phrase is being typed, `validateMnemonicWords(words)` is synchronous
489
+ and names the first word that is not in the wordlist; `validateMnemonic(phrase)`
490
+ adds the SHA-512 checksum and returns a promise.
491
+
334
492
  ## Cryptography without WebRTC
335
493
 
336
494
  `p2party/session` is the same protocol-v4 cryptography with no Redux, no
@@ -446,7 +604,8 @@ const decoder = new TextDecoder();
446
604
  const sealed = await alice.encrypt(encoder.encode("hello bob"));
447
605
 
448
606
  // sealed.protocolVersion === 4
449
- // sealed.root -> 32-byte Merkle root, authenticated as AEAD additional data
607
+ // sealed.root -> 64-byte SHA-512 Merkle root, authenticated as AEAD
608
+ // additional data (decrypt() rejects any other length)
450
609
  // sealed.frames -> [Uint8Array(65490)] one uniform cell; a 9-byte message and
451
610
  // a 60 KiB message produce byte-identical frame sizes.
452
611
  // Each frame is:
@@ -517,8 +676,8 @@ envelope codec and the sparse-PQ healing hooks are in
517
676
  `joinRoom()` and `createSession()` are the batteries-included paths. Every step
518
677
  they take is also a public call, so you can drive the protocol yourself.
519
678
 
520
- **Identity, signing, and recovery phrases.** Keys are Ed25519; the recovery
521
- phrase is BIP-39, so a wallet-style backup flow works without a second library:
679
+ **Identity, signing, and recovery phrases.** Keys are Ed25519, and a phrase
680
+ derives one deterministically:
522
681
 
523
682
  ```ts
524
683
  const mnemonic = await p2party.generateMnemonic(256); // 24 words
@@ -530,6 +689,20 @@ const signature = await p2party.sign(bytes, keyPair.secretKey);
530
689
  const ok = await p2party.verify(bytes, signature, keyPair.publicKey);
531
690
  ```
532
691
 
692
+ **This phrase is not BIP-39, and a BIP-39 wallet cannot restore it.** It draws
693
+ its words from the BIP-39 English wordlist and nothing else: the checksum is
694
+ the leading bits of SHA-512 rather than SHA-256, entropy runs from 128 to 512
695
+ bits (12 to 48 words, where BIP-39 stops at 24), and the seed comes from
696
+ argon2id over the normalized phrase — with an optional password used as the
697
+ salt — rather than PBKDF2-HMAC-SHA512. The three canonical BIP-39 English test
698
+ vectors all fail `validateMnemonic()`. Treat it as a p2party-specific format.
699
+
700
+ These are the primitives. The identity the mesh actually uses is created,
701
+ replaced and restored through
702
+ [`createRecoverableIdentity()` and `restoreIdentityFromMnemonic()`](#back-up-and-restore-your-identity),
703
+ which take care of the storage, the cross-signed X25519 identity and the
704
+ transports authenticated by the outgoing key.
705
+
533
706
  **Room policy as data.** A policy is a value you can encode, hash, compare and
534
707
  validate before anything touches the network — useful for showing two peers
535
708
  that they really are about to join the same room:
@@ -622,7 +795,16 @@ is what this package compiles.
622
795
 
623
796
  ## No build step: a script tag and the CDN
624
797
 
625
- Every release publishes its browser bundle, its database worker and its
798
+ > **`0.14.7` is not on the CDN yet.** The release pipeline uploads its objects
799
+ > from the tarball it publishes, and that pipeline has not run for this
800
+ > version: every path under `https://cdn.p2party.com/@0.14.7/` currently
801
+ > answers 404, and npm still resolves `p2party` to an older release. The
802
+ > snippet below is generated from this build and is what the release will
803
+ > serve — until then, either pin the version `npm view p2party version` reports
804
+ > (its objects are live) or self-host the bundle, which works today and is
805
+ > described in the next section.
806
+
807
+ A release publishes its browser bundle, its database worker and its
626
808
  cryptographic module as immutable, versioned CDN objects. The version is in the
627
809
  path, so a URL names exactly one build and is safe to cache forever. Drop the
628
810
  script in and `window.p2party` is there — no npm, no bundler, no build:
@@ -633,8 +815,8 @@ script in and `window.p2party` is there — no npm, no bundler, no build:
633
815
  <title>p2party in one file</title>
634
816
 
635
817
  <script
636
- src="https://cdn.p2party.com/@0.14.2/p2party.min.js"
637
- integrity="sha384-KJLIhsZkQzkYg1lyk57oAacQBR3DeWHX92Ksv9QTO9tUJVw1pxUNHI9e2nIcacgZ"
818
+ src="https://cdn.p2party.com/@0.14.7/p2party.min.js"
819
+ integrity="sha384-Wh2Pxka9YN2oAZmzYgaoyLmfKQMXsMO4AfUmshluGmYjfS0+ahOZGCTBGHtKTcC9"
638
820
  crossorigin="anonymous"
639
821
  ></script>
640
822
 
@@ -665,15 +847,15 @@ connect directly to each other.
665
847
  The three published objects:
666
848
 
667
849
  ```text
668
- https://cdn.p2party.com/@0.14.2/p2party.min.js UMD bundle -> window.p2party
669
- https://cdn.p2party.com/@0.14.2/db.worker.js IndexedDB/OPFS worker
670
- https://cdn.p2party.com/@0.14.2/libcrypto.wasm the cryptographic module
850
+ https://cdn.p2party.com/@<version>/p2party.min.js UMD bundle -> window.p2party
851
+ https://cdn.p2party.com/@<version>/db.worker.js IndexedDB/OPFS worker
852
+ https://cdn.p2party.com/@<version>/libcrypto.wasm the cryptographic module
671
853
  ```
672
854
 
673
855
  The `integrity` value above is this release's bundle, and the release build
674
856
  fails if the README and the built artifact ever disagree — so it is safe to
675
857
  copy verbatim. The worker, if you host it yourself, is
676
- `sha384-qWbMGmyz8mCS/hgbn0jEf4Fo73iXiy6w+H/NfO6ymxKwzldUeHSxolNZtSZfV1nf`.
858
+ `integrity=sha384-BudWf1U+vDU/j4pf2sX+bLogS/7MuPyzKRA9AfLoZqdbduUCwYr42p38rEoQRXa8`.
677
859
 
678
860
  The WASM is integrity-checked whether or not you pin the script: that hash is
679
861
  compiled into the bundle and cannot be turned off.
@@ -688,40 +870,58 @@ same release bytes before calling `connect()`:
688
870
  import p2party from "p2party";
689
871
 
690
872
  p2party.setWasmSourceUrl(
691
- new URL("/vendor/p2party-0.14.2/libcrypto.wasm", window.location.href),
873
+ new URL("/vendor/p2party/libcrypto.wasm", window.location.href),
692
874
  );
693
875
  ```
694
876
 
695
877
  The SRI check remains active, so a URL serving different bytes fails closed.
696
878
 
697
- ### Download the WASM from the CDN
879
+ ### Get the WASM and check it
880
+
881
+ The npm tarball is the copy that always exists. It carries both files, and the
882
+ package exports them, so no network fetch is needed to obtain either:
883
+
884
+ ```sh
885
+ npm pack p2party && tar -xzf p2party-*.tgz
886
+ ls package/lib/libcrypto.wasm package/lib/libcrypto.provenance.json
887
+ # already installed? the subpaths resolve straight out of node_modules:
888
+ node -p "require.resolve('p2party/libcrypto.wasm')"
889
+ node -p "require.resolve('p2party/libcrypto.provenance.json')"
890
+ ```
698
891
 
699
- Every release publishes its cryptographic module as an immutable, versioned
700
- object. The path carries the version, so a URL always names exactly one build
701
- and is safe to cache forever:
892
+ A published release also mirrors its cryptographic module to the CDN under an
893
+ immutable, versioned path, so a URL names exactly one build and is safe to
894
+ cache forever:
702
895
 
703
896
  ```sh
704
- curl -O https://cdn.p2party.com/@0.14.2/libcrypto.wasm
705
- curl -O https://cdn.p2party.com/@0.14.2/libcrypto.provenance.json
897
+ curl -O "https://cdn.p2party.com/@$(npm view p2party version)/libcrypto.wasm"
706
898
  ```
707
899
 
900
+ Two caveats, both current as of this release. The CDN carries only three
901
+ objects — `p2party.min.js`, `db.worker.js` and `libcrypto.wasm` (see the
902
+ `assets` array in `scripts/uploadToCDN.mjs`) — so
903
+ **`libcrypto.provenance.json` is not on the CDN for any version**; take it from
904
+ the tarball. And nothing is uploaded for this release yet, which is why the
905
+ command above asks npm for a version that is actually published instead of
906
+ naming this one.
907
+
708
908
  Check what you downloaded before you serve it. The SHA-256 and the SRI value
709
- are both recorded in the provenance file that sits next to it:
909
+ are both recorded in the provenance file:
710
910
 
711
911
  ```sh
712
912
  shasum -a 256 libcrypto.wasm
713
913
  openssl dgst -sha384 -binary libcrypto.wasm | openssl base64 -A
714
914
  ```
715
915
 
716
- For 0.14.2 those are
717
- `7eea31157e69ac61f3a512b624d5c210296302fd289fc2b65c63ecc31a056267` and
718
- `sha384-pBMyUqQ3KBztxgeJMgDFZeohfj9QlAFNwt4/gRlqT0vlZ2kbkKxv+q5DwbZOBuUP`.
719
- They are the same bytes npm ships — the release workflow uploads the CDN object
720
- from the very tarball it publishes, then re-downloads and compares before
721
- `npm publish` runs, so the two can never diverge.
916
+ Compare them against `libcrypto.provenance.json`, which records the same
917
+ `sha256` and `sri` for the bytes the same release ships. CDN and npm cannot
918
+ diverge for a release the pipeline actually ran: `.github/workflows/release.yml`
919
+ unpacks the validated tarball, uploads the CDN object from it, and runs
920
+ `npm run verify:cdn` before `npm publish`.
722
921
 
723
922
  Serve the file yourself and point the browser root at it with
724
923
  `setWasmSourceUrl()` above, or hand the bytes straight to `p2party/session`.
924
+ Self-hosting is the path that works today for every version, published or not.
725
925
 
726
926
  On Node and Bun, `p2party/session` needs none of this: it reads the WASM from
727
927
  the installed package and checks it against the same pinned SHA-384, so an
@@ -750,7 +950,7 @@ release rather than surfacing later as a broken offline install.
750
950
  ## Development
751
951
 
752
952
  The reproducible release toolchain is Node 24.11.1, npm 11.6.2, Bun 1.3.14,
753
- Emscripten 6.0.3, and the repository's pinned libsodium source object. npm and
953
+ Emscripten 6.0.9, and the repository's pinned libsodium source object. npm and
754
954
  `package-lock.json` are the dependency authority; Bun is the test runner.
755
955
 
756
956
  ```sh
@@ -174,6 +174,21 @@ returned. The application limit is 10 GiB. Cancellation is scoped most
174
174
  precisely by the handle's transfer ID, so prefer `handle.cancel()` over a
175
175
  content-hash lookup for concurrent identical sends.
176
176
 
177
+ ## Back up and restore an identity
178
+
179
+ The identity behind every room is an Ed25519 key pair in browser storage, and
180
+ only an identity created from a recovery phrase can be brought back after that
181
+ storage is gone. `getIdentityBackupStatus()` says which kind is in use,
182
+ `createRecoverableIdentity()` replaces the current identity with one a phrase
183
+ reproduces and returns that phrase exactly once, and
184
+ `restoreIdentityFromMnemonic()` adopts the identity a phrase encodes. The
185
+ derivation is one way — an identity generated randomly, which is every identity
186
+ that existed before these calls, cannot be exported to a phrase — and both
187
+ calls replace the account key, so contacts see an identity change and signaling
188
+ is left disconnected until you `connect()` again. The full contract, including
189
+ what survives a restore and what a mistyped phrase costs, is in the
190
+ [README](../README.md#back-up-and-restore-your-identity).
191
+
177
192
  ## Package artifacts and WASM
178
193
 
179
194
  The 0.14 package exports:
@@ -126,6 +126,46 @@ Putting the capability in a URL fragment keeps it out of ordinary HTTP
126
126
  requests and common access logs, but the shipped legacy signaling path still
127
127
  receives its normalized value. A fragment is not a server-blind meeting point.
128
128
 
129
+ ## Signaling and TURN correctness boundary
130
+
131
+ SDK 0.14.7 makes legacy signaling state transitions correlated and bounded; it
132
+ does not make that signaling path blind.
133
+
134
+ - A room response is accepted only for the exact current socket, room
135
+ capability, and UUIDv4 request. A delayed response cannot configure another
136
+ room.
137
+ - Caller ICE configuration and signaling-managed TURN credentials are separate
138
+ inputs. Managed credentials carry an absolute expiry, are applied to every
139
+ live room edge before an ICE restart, refresh proactively, and are removed or
140
+ failed closed when they expire.
141
+ - `purgeRoom()` tears down local transports and durable state before waiting
142
+ for a correlated server acknowledgement. The leave is keyed by the stable
143
+ room capability rather than the server-assigned room UUID, so a purge racing
144
+ an in-flight join still revokes that membership. Matching `p2party-server`
145
+ 0.1.1 unlinks the authenticated peer from the room before acknowledging. An
146
+ offline or non-acknowledging server produces
147
+ `RemoteRoomRevocationUnconfirmedError`; it is never reported as confirmed.
148
+ - Signaling ingress, room rosters, live mesh edges, pending ICE, and channel
149
+ registries have explicit client-side bounds. Those bounds limit one client's
150
+ exposure; they are not server-side Sybil resistance or a global availability
151
+ proof.
152
+
153
+ TURN is a transport relay, not a privacy proxy. The legacy REST username is
154
+ expiry-bound but contains the client's Ed25519 public key, and the TURN
155
+ operator can observe source addresses, allocations, timing, and byte volume.
156
+ Neither relay use nor TLS makes the signaling operator unable to read SDP, ICE,
157
+ room capabilities, or membership.
158
+
159
+ The current challenge proves possession of the presented Ed25519 key to the
160
+ server by signing a fresh nonce. Its transcript is not bound to a server
161
+ identity or origin, so it does not resist a malicious server relaying another
162
+ server's nonce as a signing oracle. The legacy service also has no
163
+ cross-instance session-generation fence or Byzantine quorum. Consequently none
164
+ of malicious-rendezvous blindness, two-server BFT, social-graph hiding,
165
+ server-bound authentication, shutdown survival, or post-quantum signaling
166
+ authentication is a property of this path. Those remain requirements of the
167
+ versioned blind-rendezvous design.
168
+
129
169
  ## Shipped, implemented core, and research
130
170
 
131
171
  | 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.2}
175
+ note = {Version 0.14.7}
156
176
  }
157
177
  ```
@@ -450,8 +450,16 @@ interface SessionCryptoOptions {
450
450
 
451
451
  Supplying bytes is the reproducible, offline-safe path. Resolve the exported
452
452
  `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.
453
+ release.
454
+
455
+ Omitting `wasmBinary` does not always mean a network call. On Node and Bun the
456
+ loader reads this release's WASM from the installed package — the file next to
457
+ the bundle — and hashes it against the build-pinned SHA-384 before use, so an
458
+ offline or air-gapped install needs no configuration. It falls back to the
459
+ immutable versioned p2party CDN artifact, under the same SRI, only when that
460
+ read fails or the digest does not match. In a browser, where there is no
461
+ filesystem, the CDN fetch is the only path. An explicit `setWasmSourceUrl()` is
462
+ honoured everywhere, including on Node, in preference to the packaged copy.
455
463
 
456
464
  See [Protocol-v4 security](protocol-v4-security.md) for the exact claims and
457
465
  non-claims of the session this API constructs.
@@ -1,15 +1,26 @@
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
+ type CurrentSocketApi = Pick<BaseQueryApi, "dispatch" | "getState">;
12
+ /**
13
+ * Send a correlated leave on the exact authenticated socket and wait for its
14
+ * exact acknowledgement. The caller must make local room transport terminal
15
+ * before invoking this network-only boundary.
16
+ */
17
+ export declare const requestCurrentSocketRoomLeave: (api: CurrentSocketApi, roomUrl: string, options?: RequestRoomLeaveOptions) => Promise<void>;
9
18
  declare const signalingServerApi: import("@reduxjs/toolkit/query").Api<BaseQueryFn<WebSocketParams, undefined>, {
10
19
  connectWebSocket: import("@reduxjs/toolkit/query").MutationDefinition<string, BaseQueryFn<WebSocketParams, undefined>, never, undefined, "signalingServerApi", undefined>;
11
20
  disconnectWebSocket: import("@reduxjs/toolkit/query").MutationDefinition<undefined, BaseQueryFn<WebSocketParams, undefined>, never, undefined, "signalingServerApi", undefined>;
12
21
  sendMessage: import("@reduxjs/toolkit/query").MutationDefinition<WebSocketMessage, BaseQueryFn<WebSocketParams, undefined>, never, undefined, "signalingServerApi", undefined>;
13
22
  connectWithPeer: import("@reduxjs/toolkit/query").MutationDefinition<WebSocketPeerConnectionParams, BaseQueryFn<WebSocketParams, undefined>, never, undefined, "signalingServerApi", undefined>;
14
23
  }, "signalingServerApi", never, typeof import("@reduxjs/toolkit/query").coreModuleName>;
24
+ /** Queue one room request or reject immediately when RTK reports no send. */
25
+ export declare const sendRoomRequestMessage: (api: Pick<BaseQueryApi, "dispatch">, request: WebSocketMessageRoomIdRequest) => Promise<void>;
15
26
  export default signalingServerApi;
@@ -0,0 +1,9 @@
1
+ import type { IRTCPeerConnection } from "./interfaces";
2
+ /**
3
+ * Apply the room's current effective ICE servers to every live transport.
4
+ *
5
+ * getConfiguration() preserves construction-time WebRTC options that browsers
6
+ * do not allow setConfiguration() to change. Only the refreshable ICE list is
7
+ * replaced.
8
+ */
9
+ export declare const applyRoomIceConfiguration: (peerConnections: readonly IRTCPeerConnection[], roomId: string, rtcConfig: RTCConfiguration, restartIceAfterApply?: boolean, failurePolicy?: "rollback" | "close") => number;
@@ -0,0 +1,10 @@
1
+ import { handleOpenChannel } from "../../handlers/handleOpenChannel";
2
+ import type { IRTCDataChannel, IRTCPeerConnection } from "./interfaces";
3
+ /**
4
+ * Wires a transport's `ondatachannel` to handleOpenChannel: every inbound
5
+ * channel goes through the same admission as a locally created one, and an
6
+ * inbound `main` becomes the transport's main channel. Shared by the fresh
7
+ * transports built from a roster introduction (connectWithPeer) and from an
8
+ * inbound offer (setDescription), so the two never drift.
9
+ */
10
+ export declare const attachDataChannelHandler: (epc: IRTCPeerConnection, dataChannels: IRTCDataChannel[], api: Parameters<typeof handleOpenChannel>[1]) => void;
@@ -0,0 +1,13 @@
1
+ import type { MessageData } from "../../db/types";
2
+ export interface DeleteRoomDataDependencies {
3
+ readonly getMessages: (roomId: string) => Promise<MessageData[]>;
4
+ readonly deleteRoomRecord: (roomId: string) => Promise<void>;
5
+ readonly deleteReceive: (merkleRootHex: string) => Promise<void>;
6
+ readonly deleteOutgoing: (merkleRootHex: string) => Promise<void>;
7
+ }
8
+ /**
9
+ * Await every durable room-data deletion and report only after all reachable
10
+ * records were attempted. `removeLocalMessage` is reducer-only; no background
11
+ * listener owns storage completion after this function returns.
12
+ */
13
+ export declare const deleteRoomDataTerminal: (roomId: string, removeLocalMessage: (merkleRootHex: string) => void, dependencies?: DeleteRoomDataDependencies) => Promise<void>;
@@ -1,8 +1,9 @@
1
1
  import type { BaseQueryFn } from "@reduxjs/toolkit/query";
2
- import type { IRTCDataChannel, IRTCPeerConnection, RTCDisconnectFromAllRoomsParams } from "./interfaces";
2
+ import type { IRTCDataChannel, IRTCPeerConnection, RTCDisconnectFromAllRoomsParams, DisconnectPeerTransport } from "./interfaces";
3
3
  export interface RTCDisconnectFromAllRoomsParamsExtension extends RTCDisconnectFromAllRoomsParams {
4
4
  peerConnections: IRTCPeerConnection[];
5
5
  dataChannels: IRTCDataChannel[];
6
+ disconnectPeer: DisconnectPeerTransport;
6
7
  }
7
8
  declare const webrtcDisconnectAllRoomsQuery: BaseQueryFn<RTCDisconnectFromAllRoomsParamsExtension, undefined>;
8
9
  export default webrtcDisconnectAllRoomsQuery;
@@ -1,8 +1,9 @@
1
1
  import type { BaseQueryFn } from "@reduxjs/toolkit/query";
2
- import type { IRTCDataChannel, IRTCPeerConnection, RTCDisconnectFromRoomParams } from "./interfaces";
2
+ import type { IRTCDataChannel, IRTCPeerConnection, RTCDisconnectFromRoomParams, DisconnectPeerTransport } from "./interfaces";
3
3
  export interface RTCDisconnectFromRoomParamsExtension extends RTCDisconnectFromRoomParams {
4
4
  peerConnections: IRTCPeerConnection[];
5
5
  dataChannels: IRTCDataChannel[];
6
+ disconnectPeer: DisconnectPeerTransport;
6
7
  }
7
8
  declare const webrtcDisconnectRoomQuery: BaseQueryFn<RTCDisconnectFromRoomParamsExtension, undefined>;
8
9
  export default webrtcDisconnectRoomQuery;
@@ -1,8 +1,9 @@
1
1
  import type { BaseQueryFn } from "@reduxjs/toolkit/query";
2
- import type { IRTCDataChannel, IRTCPeerConnection, RTCDisconnectParams } from "./interfaces";
2
+ import type { IRTCDataChannel, IRTCPeerConnection, RTCDisconnectParams, DisconnectPeerTransport } from "./interfaces";
3
3
  export interface RTCDisconnectParamsExtension extends RTCDisconnectParams {
4
4
  peerConnections: IRTCPeerConnection[];
5
5
  dataChannels: IRTCDataChannel[];
6
+ disconnectPeer: DisconnectPeerTransport;
6
7
  }
7
8
  declare const webrtcDisconnectQuery: BaseQueryFn<RTCDisconnectParamsExtension, undefined>;
8
9
  export default webrtcDisconnectQuery;
@@ -1,4 +1,6 @@
1
- type IceRestartTarget = Pick<RTCPeerConnection, "connectionState" | "signalingState" | "restartIce">;
1
+ type IceRestartTarget = Pick<RTCPeerConnection, "connectionState" | "signalingState" | "restartIce"> & {
2
+ ensureFreshIceConfiguration?: () => Promise<boolean | void>;
3
+ };
2
4
  export type IceCandidateRepairResult = "restarted" | "disconnected";
3
5
  /**
4
6
  * Repair a candidate-add failure without ever calling restartIce() after