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.
- package/README.md +234 -31
- package/THIRD_PARTY_NOTICES.md +33 -6
- package/docs/getting-started.md +118 -4
- package/docs/protocol-v4-security.md +96 -6
- package/docs/references.md +28 -8
- package/docs/session-api.md +19 -2
- package/docs/wire-format.md +102 -14
- package/lib/api/signalingServerApi.d.ts +51 -2
- package/lib/api/webrtc/interfaces.d.ts +70 -1
- package/lib/api/webrtc/peerSocketGeneration.d.ts +68 -0
- package/lib/cryptography/coverCell.d.ts +7 -0
- package/lib/cryptography/emscripten.d.ts +101 -0
- package/lib/cryptography/interfaces.d.ts +3 -0
- package/lib/cryptography/libcrypto.d.ts +181 -0
- package/lib/cryptography/mlkem.d.ts +27 -1
- package/lib/cryptography/mnemonic.d.ts +65 -1
- package/lib/db/outboxTypes.d.ts +135 -0
- package/lib/db/types.d.ts +81 -7
- package/lib/db.worker.js +1 -1
- package/lib/handlers/coverRuntime.d.ts +14 -1
- package/lib/handlers/coverScheduler.d.ts +59 -1
- package/lib/handlers/coverTransfer.d.ts +169 -3
- package/lib/handlers/handleSendMessage.d.ts +138 -9
- package/lib/handlers/handshakeCore.d.ts +2 -7
- package/lib/handlers/handshakeFrame.d.ts +25 -0
- package/lib/handlers/outbox.d.ts +47 -0
- package/lib/handlers/ratchetGate.d.ts +9 -0
- package/lib/handlers/reconcile.d.ts +5 -0
- package/lib/index.d.ts +102 -12
- package/lib/index.js +1 -1
- package/lib/index.min.js +1 -1
- package/lib/index.mjs +1 -1
- package/lib/libcrypto.provenance.json +5 -5
- package/lib/libcrypto.wasm +0 -0
- package/lib/reducers/commonSlice.d.ts +3 -2
- package/lib/reducers/keyPairSlice.d.ts +3 -6
- package/lib/reducers/roomSlice.d.ts +89 -6
- package/lib/reducers/signalingServerSlice.d.ts +3 -2
- package/lib/session.d.ts +6 -3
- package/lib/session.js +1 -1
- package/lib/session.mjs +1 -1
- package/lib/store.d.ts +4 -2
- package/lib/utils/constants.d.ts +19 -0
- package/lib/utils/identityRestore.d.ts +108 -0
- package/lib/utils/interfaces.d.ts +36 -6
- package/lib/utils/roomLeaveCoordinator.d.ts +38 -0
- package/lib/utils/signalingReconnectController.d.ts +44 -0
- package/lib/utils/terminalSettlement.d.ts +53 -0
- package/package.json +13 -15
- package/lib/api/webrtc/disconnectFromAllRoomsQuery.d.ts +0 -8
- package/lib/api/webrtc/disconnectFromChannelLabelQuery.d.ts +0 -7
- package/lib/api/webrtc/disconnectFromPeerChannelLabelQuery.d.ts +0 -8
- package/lib/api/webrtc/disconnectFromPeerQuery.d.ts +0 -9
- package/lib/api/webrtc/disconnectFromRoomQuery.d.ts +0 -8
- package/lib/api/webrtc/disconnectQuery.d.ts +0 -8
- package/lib/api/webrtc/iceGeneration.d.ts +0 -17
- package/lib/api/webrtc/iceRepair.d.ts +0 -10
- package/lib/api/webrtc/index.d.ts +0 -16
- package/lib/api/webrtc/negotiationLock.d.ts +0 -12
- package/lib/api/webrtc/openChannelQuery.d.ts +0 -8
- package/lib/api/webrtc/pendingIceCandidates.d.ts +0 -14
- package/lib/api/webrtc/roomPeer.d.ts +0 -11
- package/lib/api/webrtc/sendMessageQuery.d.ts +0 -14
- package/lib/api/webrtc/setCandidateQuery.d.ts +0 -8
- package/lib/api/webrtc/setDescriptionQuery.d.ts +0 -9
- package/lib/cryptography/cpace.d.ts +0 -41
- package/lib/cryptography/ed25519.d.ts +0 -11
- package/lib/cryptography/hashStream.d.ts +0 -15
- package/lib/cryptography/hkdf.d.ts +0 -3
- package/lib/cryptography/identityCrossSig.d.ts +0 -7
- package/lib/cryptography/memory.d.ts +0 -14
- package/lib/cryptography/merkle.d.ts +0 -42
- package/lib/cryptography/pqHealingFrame.d.ts +0 -39
- package/lib/cryptography/random.d.ts +0 -22
- package/lib/cryptography/testModule.d.ts +0 -7
- package/lib/cryptography/utils.d.ts +0 -13
- package/lib/cryptography/x25519.d.ts +0 -15
- package/lib/cryptography/x3dh.d.ts +0 -9
- package/lib/db/api.d.ts +0 -67
- package/lib/db/db.worker.d.ts +0 -1
- package/lib/db/ratchetWrap.d.ts +0 -36
- package/lib/db/src/getDB.d.ts +0 -81
- package/lib/handlers/chunkFrame.d.ts +0 -13
- package/lib/handlers/connectionSignal.d.ts +0 -26
- package/lib/handlers/coverEdge.d.ts +0 -42
- package/lib/handlers/frameType.d.ts +0 -10
- package/lib/handlers/handleChallenge.d.ts +0 -3
- package/lib/handlers/handleConnectToPeer.d.ts +0 -6
- package/lib/handlers/handleHandshake.d.ts +0 -74
- package/lib/handlers/handleMessageQueueing.d.ts +0 -54
- package/lib/handlers/handleOpenChannel.d.ts +0 -12
- package/lib/handlers/handleQueuedIceCandidates.d.ts +0 -2
- package/lib/handlers/handleReadReceipt.d.ts +0 -17
- package/lib/handlers/handleReceiveMessage.d.ts +0 -32
- package/lib/handlers/handleWebSocketMessage.d.ts +0 -3
- package/lib/handlers/messageChunkCrypto.d.ts +0 -70
- package/lib/handlers/peerRosterDelta.d.ts +0 -12
- package/lib/handlers/pqHealingOrchestrator.d.ts +0 -46
- package/lib/handlers/ratchetPersist.d.ts +0 -53
- package/lib/handlers/receiptFrame.d.ts +0 -18
- package/lib/handlers/receiveMessageKeyLifetime.d.ts +0 -24
- package/lib/handlers/transferAbort.d.ts +0 -42
- package/lib/middleware/keyPairListenerMiddleware.d.ts +0 -2
- package/lib/middleware/roomListenerMiddleware.d.ts +0 -2
- package/lib/roomInvite.d.ts +0 -18
- package/lib/roomPinAttempts.d.ts +0 -12
- package/lib/roomPinVault.d.ts +0 -26
- package/lib/utils/channelLabel.d.ts +0 -9
- package/lib/utils/chunkBounds.d.ts +0 -9
- package/lib/utils/drainAndClose.d.ts +0 -5
- package/lib/utils/identityRole.d.ts +0 -19
- package/lib/utils/leafHash.d.ts +0 -13
- package/lib/utils/metadata.d.ts +0 -22
- package/lib/utils/mutex.d.ts +0 -24
- package/lib/utils/protocolVersion.d.ts +0 -5
- package/lib/utils/receiptToken.d.ts +0 -7
- package/lib/utils/sendQueueKey.d.ts +0 -2
- package/lib/utils/signalingAuth.d.ts +0 -8
- package/lib/utils/signalingBounds.d.ts +0 -9
- package/lib/utils/splitToChunks.d.ts +0 -30
- package/lib/utils/uint8array.d.ts +0 -16
- package/lib/utils/waitForOpen.d.ts +0 -3
- 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](
|
|
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
|
|
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
|
-
|
|
43
|
-
|
|
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.
|
|
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 ->
|
|
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
|
|
521
|
-
|
|
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
|
-
|
|
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.
|
|
637
|
-
integrity="sha384-
|
|
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
|
|
669
|
-
https://cdn.p2party.com
|
|
670
|
-
https://cdn.p2party.com
|
|
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-
|
|
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
|
|
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
|
-
###
|
|
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
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
717
|
-
`
|
|
718
|
-
`
|
|
719
|
-
|
|
720
|
-
|
|
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.
|
|
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
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -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.
|
|
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
|
+
```
|
package/docs/getting-started.md
CHANGED
|
@@ -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
|
|
121
|
-
transfer ID
|
|
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,
|
|
151
|
-
because it is disconnected, unauthenticated, or the transfer was
|
|
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:
|