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
package/README.md CHANGED
@@ -9,7 +9,8 @@
9
9
  Protocol-v4 end-to-end encryption and reliable file transfer over a WebRTC
10
10
  room mesh.
11
11
 
12
- Apache-2.0 · [LICENSE.md](LICENSE.md)
12
+ Apache-2.0 · [License](LICENSE.md) · [Getting started](docs/getting-started.md) ·
13
+ [Contributing](CONTRIBUTING.md) · [Security policy](SECURITY.md)
13
14
 
14
15
  > Status: protocol v4 is an intentional wire break — v3 peers and persisted v3
15
16
  > crypto rows are not resumed. The current code has not completed an
@@ -33,14 +34,23 @@ Apache-2.0 · [LICENSE.md](LICENSE.md)
33
34
  decoy slots can hide a message's exact payload length within its transfer.
34
35
  - Each outbound message has its own transfer identity and data channel, a
35
36
  cancellable handle, authenticated receipts, selective retransmission, and
36
- reconnect resume.
37
+ reconnect and sender-reload recovery from a bounded durable outbox.
37
38
  - Text and files up to the enforced 10 GiB application limit are supported.
38
39
  Browser builds use IndexedDB and, where available, OPFS for disk-backed large
39
40
  file receipt.
40
41
  - 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.
42
+ fragment invite, and an optional checksum-protected 24-word representation
43
+ drawn from the
44
+ [BIP-39 English wordlist](https://github.com/bitcoin/bips/blob/master/bip-0039/bip-0039-wordlists.md).
45
+ It borrows the words, not the format: the checksum is SHA-256 over p2party's
46
+ own domain separator, so a BIP-39 phrase is not a valid invite. See
47
+ [docs/references.md](docs/references.md).
48
+ - An identity created from a recovery phrase can be restored on another device:
49
+ 12 to 48 words drawn from the same wordlist, checksummed with SHA-512 and
50
+ stretched into the Ed25519 seed with argon2id. The derivation runs one way, so
51
+ a randomly generated identity cannot be exported — it can only be replaced by
52
+ a recoverable one. See
53
+ [Back up and restore your identity](#back-up-and-restore-your-identity).
44
54
  - `p2party/session` exposes the cryptography without
45
55
  [Redux](https://redux-toolkit.js.org/), IndexedDB, WebRTC, signaling,
46
56
  `window`, or `localStorage`.
@@ -81,7 +91,7 @@ npm audit signatures
81
91
 
82
92
  To build the artifact yourself instead, see
83
93
  [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
94
+ exact toolchain (Node 24, Emscripten 6.0.9, pinned submodules), because the
85
95
  release build reproduces the pinned WASM and refuses to emit an artifact it
86
96
  cannot attest.
87
97
 
@@ -192,6 +202,62 @@ protocol-v4 acknowledgement: p2party next runs its authenticated HELLO plus
192
202
  three chained confirmation flights over the main channel. Message receipts are
193
203
  a third, delivery-level acknowledgement.
194
204
 
205
+ ### When a peer sleeps
206
+
207
+ A phone that backgrounds with its radio off for less than WebRTC's ~30 s ICE
208
+ consent timeout used to wake into a permanent split. Its peer had already torn
209
+ that edge down and built a fresh one, but the woken side still read
210
+ `connected`, reused the dead transport, and let its zombie `main` channel block
211
+ a replacement — so no handshake ever ran on the peer's new transport. One side
212
+ showed the peer reachable and every send failed undelivered; the other showed
213
+ no handshake at all and its sends hung forever. Only a manual reconnect cleared
214
+ it.
215
+
216
+ Seven behaviours now hold the edge together, and each is what the library does
217
+ rather than a guarantee about your network:
218
+
219
+ - **A peer edge is bound to the DTLS certificate it authenticated against.**
220
+ When the remote certificate changes, an already-authenticated transport is
221
+ replaced outright. One that never authenticated is re-authenticated in place
222
+ instead, at most once, so a peer that keeps presenting fresh certificates
223
+ still converges on a replacement rather than looping.
224
+ - **Descriptions from a retired transport are discarded**, so an answer
225
+ belonging to a dead connection can no longer poison the one that replaced it.
226
+ - **Every failure teardown re-dials the room** under its own debounce budget.
227
+ Deliberate teardowns — `disconnectFromPeer`, leaving, address-book removal —
228
+ do not. A superseded handshake attempt dies quietly instead of tearing down
229
+ the transport that superseded it.
230
+ - **A send to a peer that never authenticates fails inside a bounded wait**
231
+ with a stated reason, rather than hanging on the ratchet gate forever.
232
+ - **A half-open signaling socket is detected**, by a heartbeat watchdog (25 s
233
+ with no server ping) and on wake events, and marked disconnected so a
234
+ reconnect can run.
235
+ - **RTCPeerConnection operations settle when the connection closes underneath
236
+ them**, so a closing connection cannot strand the per-peer lock and wedge the
237
+ socket's ingress queue.
238
+ - **An interrupted transfer resumes across an edge re-authentication.** The
239
+ transfer's cipher and its cancel detection follow the cryptographic
240
+ generation rather than the connection object, so a channel closed by a
241
+ re-authentication reads as a resume, not as a peer cancelling on you.
242
+
243
+ Verified with two separate headless-Chromium processes against a Bun relay
244
+ mirroring the signaling server, one process SIGSTOPped whole-tree with its UDP
245
+ host-candidate buffers flooded — a phone asleep with the radio off. Across
246
+ seven scenarios (baseline, socket drop, a 20 s freeze in each identity role
247
+ with the socket dropped and with it kept, and a 45 s freeze) both sides
248
+ reconverge and exchange byte-exact messages 0.5–6.5 s after wake, and four
249
+ in-flight 8 MiB transfer configurations finish byte-exact with the sender
250
+ reporting `delivered`.
251
+
252
+ Prepared sends now survive sender reloads in the same browser storage and
253
+ identity. Pending state and retry/cancel APIs are described in
254
+ [durable transfer recovery](docs/getting-started.md). A failed `handle.done`
255
+ reports the outcome of that attempt; it does not mean its pending data was
256
+ removed. The outbox retains prepared sends for up to 24 hours and cannot recover
257
+ an upload whose preparation never finished or whose browser storage was erased.
258
+ Current measured recovery results and remaining compatibility limits are in
259
+ [the browser report](docs/e2e/README.md).
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,11 @@ 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.10` is a local release candidate.** This work does not publish npm
799
+ > packages or CDN assets. The snippet names the candidate's intended release
800
+ > path; use the generated local bundle until that version is published.
801
+
802
+ A release publishes its browser bundle, its database worker and its
626
803
  cryptographic module as immutable, versioned CDN objects. The version is in the
627
804
  path, so a URL names exactly one build and is safe to cache forever. Drop the
628
805
  script in and `window.p2party` is there — no npm, no bundler, no build:
@@ -633,8 +810,8 @@ script in and `window.p2party` is there — no npm, no bundler, no build:
633
810
  <title>p2party in one file</title>
634
811
 
635
812
  <script
636
- src="https://cdn.p2party.com/@0.14.3/p2party.min.js"
637
- integrity="sha384-dwBCnd3gj0k9HpDetSDOtYhv/CkADQs5h4mz+dI7hdYY1wWQpgvtiOGF4GUrnbqR"
813
+ src="https://cdn.p2party.com/@0.14.10/p2party.min.js"
814
+ integrity="sha384-z5PjsRDKjtPcQ9JhEwkPyr/7zthzQgjjh6pSTdjTmhZrwVhhhFS/KhRkGtmJwqJX"
638
815
  crossorigin="anonymous"
639
816
  ></script>
640
817
 
@@ -665,15 +842,15 @@ connect directly to each other.
665
842
  The three published objects:
666
843
 
667
844
  ```text
668
- https://cdn.p2party.com/@0.14.3/p2party.min.js UMD bundle -> window.p2party
669
- https://cdn.p2party.com/@0.14.3/db.worker.js IndexedDB/OPFS worker
670
- https://cdn.p2party.com/@0.14.3/libcrypto.wasm the cryptographic module
845
+ https://cdn.p2party.com/@<version>/p2party.min.js UMD bundle -> window.p2party
846
+ https://cdn.p2party.com/@<version>/db.worker.js IndexedDB/OPFS worker
847
+ https://cdn.p2party.com/@<version>/libcrypto.wasm the cryptographic module
671
848
  ```
672
849
 
673
850
  The `integrity` value above is this release's bundle, and the release build
674
851
  fails if the README and the built artifact ever disagree — so it is safe to
675
852
  copy verbatim. The worker, if you host it yourself, is
676
- `sha384-qWbMGmyz8mCS/hgbn0jEf4Fo73iXiy6w+H/NfO6ymxKwzldUeHSxolNZtSZfV1nf`.
853
+ `integrity=sha384-iZfBJVoMgpK1+s2shZQucC0OUSBaX4eHMTjHj4NqzGApUodsP9T16+EHgyDKTVQP`.
677
854
 
678
855
  The WASM is integrity-checked whether or not you pin the script: that hash is
679
856
  compiled into the bundle and cannot be turned off.
@@ -688,40 +865,66 @@ same release bytes before calling `connect()`:
688
865
  import p2party from "p2party";
689
866
 
690
867
  p2party.setWasmSourceUrl(
691
- new URL("/vendor/p2party-0.14.3/libcrypto.wasm", window.location.href),
868
+ new URL("/vendor/p2party/libcrypto.wasm", window.location.href),
692
869
  );
693
870
  ```
694
871
 
695
872
  The SRI check remains active, so a URL serving different bytes fails closed.
696
873
 
697
- ### Download the WASM from the CDN
874
+ ### Get the WASM and check it
875
+
876
+ The npm tarball is the copy that always exists. It carries both files, and the
877
+ package exports them, so no network fetch is needed to obtain either:
878
+
879
+ ```sh
880
+ npm pack p2party && tar -xzf p2party-*.tgz
881
+ ls package/lib/libcrypto.wasm package/lib/libcrypto.provenance.json
882
+ # already installed? the subpaths resolve straight out of node_modules:
883
+ node -p "require.resolve('p2party/libcrypto.wasm')"
884
+ node -p "require.resolve('p2party/libcrypto.provenance.json')"
885
+ ```
698
886
 
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:
887
+ A published release also mirrors its cryptographic module to the CDN under an
888
+ immutable, versioned path, so a URL names exactly one build and is safe to
889
+ cache forever:
702
890
 
703
891
  ```sh
704
- curl -O https://cdn.p2party.com/@0.14.3/libcrypto.wasm
705
- curl -O https://cdn.p2party.com/@0.14.3/libcrypto.provenance.json
892
+ curl --compressed -O "https://cdn.p2party.com/@$(npm view p2party version)/libcrypto.wasm"
706
893
  ```
707
894
 
895
+ Two caveats, both current as of this release. The CDN carries only three
896
+ objects — `p2party.min.js`, `db.worker.js` and `libcrypto.wasm` (see the
897
+ `assets` array in `scripts/uploadToCDN.mjs`) — so
898
+ **`libcrypto.provenance.json` is not on the CDN for any version**; take it from
899
+ the tarball. And nothing is uploaded for this release yet, which is why the
900
+ command above asks npm for a version that is actually published instead of
901
+ naming this one.
902
+
903
+ New CDN uploads gzip all three objects; WASM keeps `Content-Type:
904
+ application/wasm` and uses `Content-Encoding: gzip`. Browsers decode it before
905
+ SRI verification, and `curl --compressed` saves those same decoded bytes for
906
+ the checks below. Existing raw WASM objects remain untouched when their decoded
907
+ content matches; enabling compression never overwrites a published version.
908
+ Upload preparation compresses the validated artifacts already in `lib/`; it
909
+ does not replace them from a local development crypto build.
910
+
708
911
  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:
912
+ are both recorded in the provenance file:
710
913
 
711
914
  ```sh
712
915
  shasum -a 256 libcrypto.wasm
713
916
  openssl dgst -sha384 -binary libcrypto.wasm | openssl base64 -A
714
917
  ```
715
918
 
716
- For 0.14.3 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.
919
+ Compare them against `libcrypto.provenance.json`, which records the same
920
+ `sha256` and `sri` for the bytes the same release ships. CDN and npm cannot
921
+ diverge for a release the pipeline actually ran: `.github/workflows/release.yml`
922
+ unpacks the validated tarball, uploads the CDN object from it, and runs
923
+ `npm run verify:cdn` before `npm publish`.
722
924
 
723
925
  Serve the file yourself and point the browser root at it with
724
926
  `setWasmSourceUrl()` above, or hand the bytes straight to `p2party/session`.
927
+ Self-hosting is the path that works today for every version, published or not.
725
928
 
726
929
  On Node and Bun, `p2party/session` needs none of this: it reads the WASM from
727
930
  the installed package and checks it against the same pinned SHA-384, so an
@@ -750,7 +953,7 @@ release rather than surfacing later as a broken offline install.
750
953
  ## Development
751
954
 
752
955
  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
956
+ Emscripten 6.0.9, and the repository's pinned libsodium source object. npm and
754
957
  `package-lock.json` are the dependency authority; Bun is the test runner.
755
958
 
756
959
  ```sh
@@ -68,7 +68,7 @@ SOFTWARE.
68
68
 
69
69
  ## Emscripten-generated runtime
70
70
 
71
- The WebAssembly and JavaScript glue are built with Emscripten 6.0.2.
71
+ The WebAssembly and JavaScript glue are built with Emscripten 6.0.9.
72
72
 
73
73
  Verbatim upstream license:
74
74
 
@@ -416,11 +416,6 @@ The browser bundle and/or installed runtime dependency graph includes:
416
416
  - `redux-thunk` 3.1.0 — Copyright (c) 2015-present Dan Abramov
417
417
  - `reselect` 5.1.1 — Copyright (c) 2015-2018 Reselect Contributors
418
418
  - `immer` 11.1.15 — Copyright (c) 2017 Michel Weststrate
419
- - `class-validator` 0.15.1 — Copyright (c) 2015-2020 TypeStack
420
- - `validator` 13.15.23 — Copyright (c) 2018 Chris O'Hara
421
- `<cohara87@gmail.com>`
422
- - `libphonenumber-js` 1.12.27 — Copyright (c) 2016 @catamphetamine
423
- `<purecatamphetamine@gmail.com>`
424
419
 
425
420
  Each component above is MIT-licensed. Verbatim grant and disclaimer:
426
421
 
@@ -456,3 +451,35 @@ Permission to use, copy, modify, and/or distribute this software for any purpose
456
451
 
457
452
  THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
458
453
  ```
454
+
455
+ ## Emscripten TypeScript declarations
456
+
457
+ The module-scoped `EmscriptenModule` and factory types are adapted from
458
+ `@types/emscripten` 1.41.5 in DefinitelyTyped. All inherited LibCrypto members
459
+ are retained; unrelated global declarations are not included.
460
+
461
+ Verbatim upstream license:
462
+
463
+ ```text
464
+ MIT License
465
+
466
+ Copyright (c) Microsoft Corporation.
467
+
468
+ Permission is hereby granted, free of charge, to any person obtaining a copy
469
+ of this software and associated documentation files (the "Software"), to deal
470
+ in the Software without restriction, including without limitation the rights
471
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
472
+ copies of the Software, and to permit persons to whom the Software is
473
+ furnished to do so, subject to the following conditions:
474
+
475
+ The above copyright notice and this permission notice shall be included in all
476
+ copies or substantial portions of the Software.
477
+
478
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
479
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
480
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
481
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
482
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
483
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
484
+ SOFTWARE
485
+ ```
@@ -109,6 +109,14 @@ bytes are deliberately absent from the public policy, Redux, persistent room
109
109
  records, and logs. PIN mode adds CPace authentication to the identity and
110
110
  ML-KEM handshake; it does not replace identity possession.
111
111
 
112
+ The SDK retries an unexpectedly closed signaling socket with bounded backoff.
113
+ Let that controller own recovery: do not call `connect()` on every disconnected
114
+ render or browser focus event. Call it for initial admission and a deliberate
115
+ retry, such as entering a corrected PIN. Terminal PIN or policy failures stay
116
+ visible and do not trigger automatic peer discovery or transport retries.
117
+ A fresh connection request from the other peer can retry that peer's failed
118
+ edge; the existing PIN-attempt throttle still applies.
119
+
112
120
  Scheduled timing cover is wired as of 0.14: a policy may pin `coverMode:
113
121
  "scheduled"` with a cadence, lane count, and frames per cell, and every edge in
114
122
  the room then emits fixed-size cells on that schedule whether or not data is
@@ -117,8 +125,10 @@ their live transport wiring is not complete.
117
125
 
118
126
  ## Send, cancel, and read
119
127
 
120
- Wait until the room has peers before sending. Each logical send has a random
121
- transfer ID and opens a per-message data channel on every eligible peer edge.
128
+ Wait until the room has known recipient identities before sending. Each logical
129
+ send has a random transfer ID. Immediate mode opens a per-message data channel
130
+ on each eligible edge; scheduled mode places its chunks into the existing
131
+ fixed-cadence cover lanes.
122
132
 
123
133
  ```ts
124
134
  const handle = p2party.sendMessage("hello room", "chat", room.id);
@@ -147,8 +157,97 @@ void cancel; // Remove when a UI event uses it.
147
157
 
148
158
  `sendMessage()` returns a `MessageTransferHandle`, not a promise. `done`
149
159
  settles after all started peer sends and cleanup and reports ordered per-peer
150
- outcomes. A peer may be delivered, failed during setup/transfer, or skipped
151
- because it is disconnected, unauthenticated, or the transfer was cancelled.
160
+ outcomes for that attempt. A peer may be delivered, failed during setup/transfer,
161
+ or skipped because it is disconnected, unauthenticated, or the transfer was
162
+ cancelled. A rejected attempt can still have a durable pending transfer: do not
163
+ label it delivered, or assume that its resend source was discarded.
164
+
165
+ The sender publishes its outbox record only after every staged chunk, the
166
+ Merkle tree, and its local message copy have committed. A reload before that
167
+ point cannot resume preparation; select the original file again. After
168
+ publication, a reload or an exhausted reconnect budget retains the original
169
+ transfer ID, Merkle root and staged bytes for retry. Nothing is rehashed or
170
+ randomly repadded on a resumed send.
171
+
172
+ ```ts
173
+ import type { PendingMessage } from "p2party";
174
+
175
+ const pending: PendingMessage[] = await p2party.listPendingMessages(room.id);
176
+ for (const message of pending) {
177
+ console.log(message.filename, message.phase, message.remainingRecipients);
178
+ }
179
+
180
+ // Use a deliberate retry button. It may take the room's scheduled delivery
181
+ // time to settle; leave cancellation available while this promise is pending.
182
+ try {
183
+ const attempts = await p2party.resumePendingMessages(room.id);
184
+ for (const attempt of attempts) console.table(attempt.outcomes);
185
+ } catch (error) {
186
+ // AggregateError.errors contains failed attempts; their pending records stay
187
+ // available unless explicitly cancelled or expired.
188
+ console.error(error);
189
+ }
190
+
191
+ // Exact logical-send cancellation also works after the original handle was
192
+ // lost to a reload. It fences queued writes before removing retained staging.
193
+ if (pending[0])
194
+ await p2party.cancelPendingMessage(room.id, pending[0].transferId);
195
+ ```
196
+
197
+ Authenticated-peer events trigger a serialized retry pass, including a bounded
198
+ claim after an old tab's lease expires. A failed pass does not start an endless
199
+ retry timer. Explicit retry requires verified signaling and a matching
200
+ currently authenticated recipient; it never calls `connect()`, prompts for a
201
+ passkey, or supplies a PIN. A PIN room must first be admitted with the user's
202
+ current in-memory PIN. PIN and policy failures remain terminal until a deliberate
203
+ authentication retry.
204
+
205
+ The immutable outbox binds the sender's public key and durable identity
206
+ revision, room capability and canonical policy, channel, and recipient public
207
+ keys. Session peer IDs may change. Only one tab can own a transfer's 30-second
208
+ lease at a time; it renews every 10 seconds. A stale tab cannot publish queued
209
+ writes or renew a replacement owner's lease. Terminal signaling refusal stops
210
+ its current owners, and switching identity invalidates the old outbox.
211
+
212
+ Confirmed recipients are recorded separately and never retried. A recipient's
213
+ explicit cancellation also ends retries to that recipient without cancelling
214
+ other recipients. Immediate retries rebuild the have-set from authenticated
215
+ receipt replay, so older receivers that discarded a closed channel's partial
216
+ can still recover. Scheduled retries preserve the receipt bitmap and send
217
+ missing chunks plus at most one already-received real chunk as a completion
218
+ probe. The fixed cover cadence and lane count remain unchanged.
219
+
220
+ Retention and admission have explicit limits:
221
+
222
+ - Published pending transfers expire 24 hours after publication. Active owners
223
+ are protected while they run; a completed recipient is never reported as a
224
+ new delivery merely because staging remains.
225
+ - Interrupted preparation expires after one hour. Its `phase` remains
226
+ `"staging"`; an expired `retryAfter` means its owner is gone, not that the
227
+ unfinished file can resume.
228
+ - At most 32 pending transfers and 64 recipients per transfer are admitted.
229
+ Their aggregate storage reservation is capped at 12 GiB and charges padded
230
+ cells, metadata, proofs and the sender's local copy. Browser quota can impose
231
+ a smaller limit than the 10 GiB per-message protocol maximum. Admission
232
+ refuses excess work rather than evicting another pending send.
233
+ - Incomplete received transfers expire after 24 hours without local receive
234
+ activity. Cleanup uses the device's activity clock, not the sender's message
235
+ timestamp. It protects active writes, completed messages and sender copies,
236
+ checks the room/receive owner, and fences writes queued before cancellation.
237
+
238
+ Storage sweeps run when pending transfers are listed or an authentication event
239
+ checks for resumable work. Maintenance runs immediately on the first check,
240
+ then at most once every 30 seconds, and defers while receive queues are busy.
241
+ Committed expiry also removes the old room view and retires its mapped receive
242
+ key, preserving newer receives and connection generations.
243
+ Cancelling or deleting a message removes its pending
244
+ outbox; leaving a room discards that room's pending work. Successful completion
245
+ removes resend staging while retaining completed local history.
246
+
247
+ The outbox reuses the already-staged message bytes in browser storage; it is
248
+ not a new encryption-at-rest guarantee for files or message history. It stores
249
+ no PIN, passkey assertion, plaintext ratchet key or message key. A resumed send
250
+ derives fresh message encryption state from the currently authenticated edge.
152
251
 
153
252
  For an inbound message, take `merkleRootHex` (and, if needed, `sha512Hex`) from
154
253
  the room's exported `messages` state:
@@ -174,6 +273,21 @@ returned. The application limit is 10 GiB. Cancellation is scoped most
174
273
  precisely by the handle's transfer ID, so prefer `handle.cancel()` over a
175
274
  content-hash lookup for concurrent identical sends.
176
275
 
276
+ ## Back up and restore an identity
277
+
278
+ The identity behind every room is an Ed25519 key pair in browser storage, and
279
+ only an identity created from a recovery phrase can be brought back after that
280
+ storage is gone. `getIdentityBackupStatus()` says which kind is in use,
281
+ `createRecoverableIdentity()` replaces the current identity with one a phrase
282
+ reproduces and returns that phrase exactly once, and
283
+ `restoreIdentityFromMnemonic()` adopts the identity a phrase encodes. The
284
+ derivation is one way — an identity generated randomly, which is every identity
285
+ that existed before these calls, cannot be exported to a phrase — and both
286
+ calls replace the account key, so contacts see an identity change and signaling
287
+ is left disconnected until you `connect()` again. The full contract, including
288
+ what survives a restore and what a mistyped phrase costs, is in the
289
+ [README](../README.md#back-up-and-restore-your-identity).
290
+
177
291
  ## Package artifacts and WASM
178
292
 
179
293
  The 0.14 package exports: