@decentnetwork/peer 0.1.135 → 0.1.137
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/dist/ice-servers.d.ts +60 -0
- package/dist/ice-servers.js +78 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/peer.d.ts +30 -5
- package/dist/peer.js +73 -10
- package/dist/turn.js +50 -16
- package/dist/types/peer.d.ts +4 -3
- package/package.json +1 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebRTC ICE helpers matching Android Carrier WebrtcClient.getIceServers() /
|
|
3
|
+
* CarrierExtension.getTurnServerInfo() (carrier_get_turn_server).
|
|
4
|
+
*
|
|
5
|
+
* Online Android Beagle gathers STUN+TURN against a Carrier bootstrap node's
|
|
6
|
+
* coturn (UDP/3478) using Carrier long-term credentials. Browser / Node WebRTC
|
|
7
|
+
* stacks need the same list — not the tokyo.fi.chat static relay used only as
|
|
8
|
+
* a fallback for the peer SDK's own messaging UDP relay path.
|
|
9
|
+
*/
|
|
10
|
+
import { CARRIER_TURN_PORT, CARRIER_TURN_REALM } from "./turn-creds.js";
|
|
11
|
+
import type { NetworkNode } from "./types/peer.js";
|
|
12
|
+
/** Same shape as Android CarrierExtension.TurnServerInfo / CarrierTurnServer. */
|
|
13
|
+
export interface CarrierTurnServerInfo {
|
|
14
|
+
server: string;
|
|
15
|
+
port: number;
|
|
16
|
+
username: string;
|
|
17
|
+
password: string;
|
|
18
|
+
realm: string;
|
|
19
|
+
}
|
|
20
|
+
/** Browser / wrtc RTCIceServer-compatible entry. */
|
|
21
|
+
export interface RtcIceServer {
|
|
22
|
+
urls: string | string[];
|
|
23
|
+
username?: string;
|
|
24
|
+
credential?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface BuildIceServersOptions {
|
|
27
|
+
/** Bootstrap / bootnodes that run Carrier TURN on port 3478. */
|
|
28
|
+
bootstrapNodes: NetworkNode[];
|
|
29
|
+
ourUserid: string;
|
|
30
|
+
ourSecretKey: Uint8Array;
|
|
31
|
+
/**
|
|
32
|
+
* How many bootstrap TURNs to advertise. Android returns one; browsers often
|
|
33
|
+
* benefit from a few (default 3). Cap with this.
|
|
34
|
+
*/
|
|
35
|
+
limit?: number;
|
|
36
|
+
/** Also advertise turn:?transport=tcp (useful for UDP-blocked browsers). */
|
|
37
|
+
includeTcpTurn?: boolean;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Derive TURN info for one bootnode — peer analogue of
|
|
41
|
+
* carrier_get_turn_server / CarrierExtension.getTurnServerInfo().
|
|
42
|
+
*/
|
|
43
|
+
export declare function getTurnServerInfoForBootnode(opts: {
|
|
44
|
+
bootnode: NetworkNode;
|
|
45
|
+
ourUserid: string;
|
|
46
|
+
ourSecretKey: Uint8Array;
|
|
47
|
+
}): CarrierTurnServerInfo;
|
|
48
|
+
/**
|
|
49
|
+
* Build RTCIceServer entries from TURN info the way Android WebrtcClient does:
|
|
50
|
+
* stun:host:port + turn:host:port with the same username/password.
|
|
51
|
+
*/
|
|
52
|
+
export declare function iceServersFromTurnInfo(info: CarrierTurnServerInfo, opts?: {
|
|
53
|
+
includeTcpTurn?: boolean;
|
|
54
|
+
}): RtcIceServer[];
|
|
55
|
+
/**
|
|
56
|
+
* Build a WebRTC iceServers list from Carrier bootstrap nodes.
|
|
57
|
+
* Fresh nonce/credentials on every call (same as Android).
|
|
58
|
+
*/
|
|
59
|
+
export declare function getIceServers(opts: BuildIceServersOptions): RtcIceServer[];
|
|
60
|
+
export { CARRIER_TURN_PORT, CARRIER_TURN_REALM };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebRTC ICE helpers matching Android Carrier WebrtcClient.getIceServers() /
|
|
3
|
+
* CarrierExtension.getTurnServerInfo() (carrier_get_turn_server).
|
|
4
|
+
*
|
|
5
|
+
* Online Android Beagle gathers STUN+TURN against a Carrier bootstrap node's
|
|
6
|
+
* coturn (UDP/3478) using Carrier long-term credentials. Browser / Node WebRTC
|
|
7
|
+
* stacks need the same list — not the tokyo.fi.chat static relay used only as
|
|
8
|
+
* a fallback for the peer SDK's own messaging UDP relay path.
|
|
9
|
+
*/
|
|
10
|
+
import { base58ToBytes } from "./utils/base58.js";
|
|
11
|
+
import { CARRIER_TURN_PORT, CARRIER_TURN_REALM, deriveCarrierTurnCreds } from "./turn-creds.js";
|
|
12
|
+
function bootnodePublicKey(node) {
|
|
13
|
+
if (node.pkBytes && node.pkBytes.length === 32)
|
|
14
|
+
return node.pkBytes;
|
|
15
|
+
if (!node.pk)
|
|
16
|
+
throw new Error(`bootstrap node ${node.host} has no pk`);
|
|
17
|
+
return base58ToBytes(node.pk);
|
|
18
|
+
}
|
|
19
|
+
function toTurnServerInfo(creds) {
|
|
20
|
+
return {
|
|
21
|
+
server: creds.host,
|
|
22
|
+
port: creds.port,
|
|
23
|
+
username: creds.username,
|
|
24
|
+
password: creds.password,
|
|
25
|
+
realm: creds.realm
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Derive TURN info for one bootnode — peer analogue of
|
|
30
|
+
* carrier_get_turn_server / CarrierExtension.getTurnServerInfo().
|
|
31
|
+
*/
|
|
32
|
+
export function getTurnServerInfoForBootnode(opts) {
|
|
33
|
+
const creds = deriveCarrierTurnCreds({
|
|
34
|
+
bootnodeHost: opts.bootnode.host,
|
|
35
|
+
bootnodePublicKey: bootnodePublicKey(opts.bootnode),
|
|
36
|
+
ourUserid: opts.ourUserid,
|
|
37
|
+
ourSecretKey: opts.ourSecretKey
|
|
38
|
+
});
|
|
39
|
+
return toTurnServerInfo(creds);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Build RTCIceServer entries from TURN info the way Android WebrtcClient does:
|
|
43
|
+
* stun:host:port + turn:host:port with the same username/password.
|
|
44
|
+
*/
|
|
45
|
+
export function iceServersFromTurnInfo(info, opts) {
|
|
46
|
+
const base = `${info.server}:${info.port}`;
|
|
47
|
+
const auth = { username: info.username, credential: info.password };
|
|
48
|
+
const out = [
|
|
49
|
+
{ urls: `stun:${base}`, ...auth },
|
|
50
|
+
{ urls: `turn:${base}`, ...auth }
|
|
51
|
+
];
|
|
52
|
+
if (opts?.includeTcpTurn !== false) {
|
|
53
|
+
out.push({ urls: `turn:${base}?transport=tcp`, ...auth });
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Build a WebRTC iceServers list from Carrier bootstrap nodes.
|
|
59
|
+
* Fresh nonce/credentials on every call (same as Android).
|
|
60
|
+
*/
|
|
61
|
+
export function getIceServers(opts) {
|
|
62
|
+
const limit = opts.limit ?? 3;
|
|
63
|
+
const nodes = opts.bootstrapNodes.filter((n) => n?.host && (n.pk || n.pkBytes)).slice(0, limit);
|
|
64
|
+
if (!nodes.length) {
|
|
65
|
+
throw new Error("getIceServers: no bootstrap nodes with public keys");
|
|
66
|
+
}
|
|
67
|
+
const ice = [];
|
|
68
|
+
for (const bootnode of nodes) {
|
|
69
|
+
const info = getTurnServerInfoForBootnode({
|
|
70
|
+
bootnode,
|
|
71
|
+
ourUserid: opts.ourUserid,
|
|
72
|
+
ourSecretKey: opts.ourSecretKey
|
|
73
|
+
});
|
|
74
|
+
ice.push(...iceServersFromTurnInfo(info, { includeTcpTurn: opts.includeTcpTurn }));
|
|
75
|
+
}
|
|
76
|
+
return ice;
|
|
77
|
+
}
|
|
78
|
+
export { CARRIER_TURN_PORT, CARRIER_TURN_REALM };
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,10 @@ export { NET_PACKET_ONION_ANNOUNCE_REQUEST, NET_PACKET_ONION_ANNOUNCE_RESPONSE,
|
|
|
6
6
|
export { signDetached, verifyDetached, SIGNATURE_LENGTH } from "./crypto/sign.js";
|
|
7
7
|
export { createCarrierRendezvousAuthProof, RENDEZVOUS_AUTH_KEY_DOMAIN, RENDEZVOUS_AUTH_PROOF_LENGTH } from "./crypto/rendezvous.js";
|
|
8
8
|
export { base58ToBytes, bytesToBase58 } from "./utils/base58.js";
|
|
9
|
+
export { CARRIER_TURN_PORT, CARRIER_TURN_REALM, CARRIER_TURN_USER_SUFFIX, deriveCarrierTurnCreds } from "./turn-creds.js";
|
|
10
|
+
export type { CarrierTurnCreds } from "./turn-creds.js";
|
|
11
|
+
export { getIceServers, getTurnServerInfoForBootnode, iceServersFromTurnInfo } from "./ice-servers.js";
|
|
12
|
+
export type { BuildIceServersOptions, CarrierTurnServerInfo, RtcIceServer } from "./ice-servers.js";
|
|
9
13
|
export { LegacyProtocolNotImplementedError } from "./runtime/errors.js";
|
|
10
14
|
export type { CarrierPacket, FriendMessagePacket, FriendRequestPacket, InviteReqPacket, InviteRspPacket } from "./compat/packet.js";
|
|
11
15
|
export type { ToxDhtCryptoRequest } from "./compat/tox-dht-crypto.js";
|
package/dist/index.js
CHANGED
|
@@ -6,4 +6,6 @@ export { NET_PACKET_ONION_ANNOUNCE_REQUEST, NET_PACKET_ONION_ANNOUNCE_RESPONSE,
|
|
|
6
6
|
export { signDetached, verifyDetached, SIGNATURE_LENGTH } from "./crypto/sign.js";
|
|
7
7
|
export { createCarrierRendezvousAuthProof, RENDEZVOUS_AUTH_KEY_DOMAIN, RENDEZVOUS_AUTH_PROOF_LENGTH } from "./crypto/rendezvous.js";
|
|
8
8
|
export { base58ToBytes, bytesToBase58 } from "./utils/base58.js";
|
|
9
|
+
export { CARRIER_TURN_PORT, CARRIER_TURN_REALM, CARRIER_TURN_USER_SUFFIX, deriveCarrierTurnCreds } from "./turn-creds.js";
|
|
10
|
+
export { getIceServers, getTurnServerInfoForBootnode, iceServersFromTurnInfo } from "./ice-servers.js";
|
|
9
11
|
export { LegacyProtocolNotImplementedError } from "./runtime/errors.js";
|
package/dist/peer.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type BootstrapResult } from "./compat/bootstrap.js";
|
|
2
2
|
import type { FriendRecord } from "./store/friends.js";
|
|
3
|
+
import { type CarrierTurnServerInfo, type RtcIceServer } from "./ice-servers.js";
|
|
3
4
|
import type { CustomPacketEvent, FriendConnectionEvent, FriendRequest, FriendInfoEvent, InlineFileEvent, InviteEvent, InviteResponseEvent, LookupResult, NetworkNode, PeerOptions, SendTextUntilAckOptions, TextMessage } from "./types/peer.js";
|
|
4
5
|
export declare class Peer {
|
|
5
6
|
#private;
|
|
@@ -18,6 +19,21 @@ export declare class Peer {
|
|
|
18
19
|
*/
|
|
19
20
|
sign(message: Uint8Array): Uint8Array;
|
|
20
21
|
address(): string;
|
|
22
|
+
/**
|
|
23
|
+
* Carrier bootstrap TURN credentials — peer analogue of Android
|
|
24
|
+
* CarrierExtension.getTurnServerInfo() / carrier_get_turn_server().
|
|
25
|
+
* Uses the first configured bootstrap node with a public key (fresh nonce
|
|
26
|
+
* each call). For WebRTC, prefer {@link getIceServers}.
|
|
27
|
+
*/
|
|
28
|
+
getTurnServerInfo(bootnodeIndex?: number): CarrierTurnServerInfo;
|
|
29
|
+
/**
|
|
30
|
+
* RTCIceServer list for browser / native WebRTC, matching Android
|
|
31
|
+
* WebrtcClient.getIceServers() (Carrier bootstrap STUN+TURN, not tokyo).
|
|
32
|
+
*/
|
|
33
|
+
getIceServers(opts?: {
|
|
34
|
+
limit?: number;
|
|
35
|
+
includeTcpTurn?: boolean;
|
|
36
|
+
}): RtcIceServer[];
|
|
21
37
|
joinNetwork(): Promise<BootstrapResult>;
|
|
22
38
|
lookup(pubkey: string): Promise<LookupResult>;
|
|
23
39
|
announceSelf(timeoutMs?: number): Promise<NetworkNode[]>;
|
|
@@ -87,11 +103,13 @@ export declare class Peer {
|
|
|
87
103
|
/**
|
|
88
104
|
* Send text and keep retransmitting until the peer explicitly ACKs it.
|
|
89
105
|
*
|
|
90
|
-
* The
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
106
|
+
* The ACK means the far side's SDK durably received the message — it is
|
|
107
|
+
* sent BEFORE the peer's onText handlers run, so a peer that is busy in
|
|
108
|
+
* application code (a model call, a disk-bound job) still ACKs instantly.
|
|
109
|
+
* It does NOT mean the application processed the message; if you need that,
|
|
110
|
+
* have the far side send an application-level reply. Pass a stable
|
|
111
|
+
* deliveryId when retrying an item from an application outbox across
|
|
112
|
+
* process restarts.
|
|
95
113
|
*/
|
|
96
114
|
sendTextUntilAck(pubkey: string, text: string, opts?: SendTextUntilAckOptions): Promise<{
|
|
97
115
|
deliveryId: string;
|
|
@@ -99,6 +117,13 @@ export declare class Peer {
|
|
|
99
117
|
waitForFriendConnected(pubkey: string, timeoutMs?: number): Promise<boolean>;
|
|
100
118
|
onFriendRequest(cb: (req: FriendRequest) => void): void;
|
|
101
119
|
onText(cb: (msg: TextMessage) => unknown | Promise<unknown>): void;
|
|
120
|
+
/** @internal Test seam: deliver a text message exactly as if it arrived off
|
|
121
|
+
* the wire. Exists so the ACK-before-handlers ordering is testable without
|
|
122
|
+
* a network — do not use outside tests. */
|
|
123
|
+
_testDispatchTextMessage(msg: {
|
|
124
|
+
pubkey: string;
|
|
125
|
+
text: string;
|
|
126
|
+
}): Promise<void>;
|
|
102
127
|
/** Files received inline over (bulk)messages — the iOS/C Carrier apps'
|
|
103
128
|
* native way of sending images/audio online (FileModel JSON envelope). */
|
|
104
129
|
onInlineFile(cb: (evt: InlineFileEvent) => void): void;
|
package/dist/peer.js
CHANGED
|
@@ -23,6 +23,7 @@ import { UdpTransport } from "./transport/udp.js";
|
|
|
23
23
|
import { bytesToHex, concatBytes, randomBytes } from "./utils/bytes.js";
|
|
24
24
|
import { buildBindingRequest, decodeStun, decodeXorMappedAddress, findAttr, STUN_ATTR_XOR_MAPPED_ADDRESS, STUN_BINDING_SUCCESS } from "./stun.js";
|
|
25
25
|
import { TurnClient } from "./turn.js";
|
|
26
|
+
import { getIceServers as buildRtcIceServers, getTurnServerInfoForBootnode } from "./ice-servers.js";
|
|
26
27
|
import { createBoundUdp4Socket, closeDgramSocket } from "./transport/dgram-lifecycle.js";
|
|
27
28
|
// Dedicated TURN relay servers — fixed public relays used as a stable
|
|
28
29
|
// fallback path when direct UDP hole-punch fails or flaps (symmetric NAT,
|
|
@@ -831,6 +832,43 @@ export class Peer {
|
|
|
831
832
|
}
|
|
832
833
|
return carrierAddressFromPublicKey(this.#keyPair.publicKey);
|
|
833
834
|
}
|
|
835
|
+
/**
|
|
836
|
+
* Carrier bootstrap TURN credentials — peer analogue of Android
|
|
837
|
+
* CarrierExtension.getTurnServerInfo() / carrier_get_turn_server().
|
|
838
|
+
* Uses the first configured bootstrap node with a public key (fresh nonce
|
|
839
|
+
* each call). For WebRTC, prefer {@link getIceServers}.
|
|
840
|
+
*/
|
|
841
|
+
getTurnServerInfo(bootnodeIndex = 0) {
|
|
842
|
+
if (!this.#keyPair) {
|
|
843
|
+
throw new Error("Peer is not started");
|
|
844
|
+
}
|
|
845
|
+
const nodes = this.#opts.bootstrapNodes.filter((n) => n?.host && (n.pk || n.pkBytes));
|
|
846
|
+
const bootnode = nodes[bootnodeIndex] ?? nodes[0];
|
|
847
|
+
if (!bootnode) {
|
|
848
|
+
throw new Error("getTurnServerInfo: no bootstrap nodes configured");
|
|
849
|
+
}
|
|
850
|
+
return getTurnServerInfoForBootnode({
|
|
851
|
+
bootnode,
|
|
852
|
+
ourUserid: this.userid(),
|
|
853
|
+
ourSecretKey: this.#keyPair.secretKey
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* RTCIceServer list for browser / native WebRTC, matching Android
|
|
858
|
+
* WebrtcClient.getIceServers() (Carrier bootstrap STUN+TURN, not tokyo).
|
|
859
|
+
*/
|
|
860
|
+
getIceServers(opts) {
|
|
861
|
+
if (!this.#keyPair) {
|
|
862
|
+
throw new Error("Peer is not started");
|
|
863
|
+
}
|
|
864
|
+
return buildRtcIceServers({
|
|
865
|
+
bootstrapNodes: this.#opts.bootstrapNodes,
|
|
866
|
+
ourUserid: this.userid(),
|
|
867
|
+
ourSecretKey: this.#keyPair.secretKey,
|
|
868
|
+
limit: opts?.limit,
|
|
869
|
+
includeTcpTurn: opts?.includeTcpTurn
|
|
870
|
+
});
|
|
871
|
+
}
|
|
834
872
|
async joinNetwork() {
|
|
835
873
|
if (!this.#bootstrap) {
|
|
836
874
|
throw new Error("Peer is not started");
|
|
@@ -1330,11 +1368,13 @@ export class Peer {
|
|
|
1330
1368
|
/**
|
|
1331
1369
|
* Send text and keep retransmitting until the peer explicitly ACKs it.
|
|
1332
1370
|
*
|
|
1333
|
-
* The
|
|
1334
|
-
*
|
|
1335
|
-
*
|
|
1336
|
-
*
|
|
1337
|
-
*
|
|
1371
|
+
* The ACK means the far side's SDK durably received the message — it is
|
|
1372
|
+
* sent BEFORE the peer's onText handlers run, so a peer that is busy in
|
|
1373
|
+
* application code (a model call, a disk-bound job) still ACKs instantly.
|
|
1374
|
+
* It does NOT mean the application processed the message; if you need that,
|
|
1375
|
+
* have the far side send an application-level reply. Pass a stable
|
|
1376
|
+
* deliveryId when retrying an item from an application outbox across
|
|
1377
|
+
* process restarts.
|
|
1338
1378
|
*/
|
|
1339
1379
|
async sendTextUntilAck(pubkey, text, opts = {}) {
|
|
1340
1380
|
const deliveryId = opts.deliveryId ?? createTextDeliveryId();
|
|
@@ -1441,6 +1481,28 @@ export class Peer {
|
|
|
1441
1481
|
? async () => { await this.#sendTextAck(msg.pubkey, deliveryId); }
|
|
1442
1482
|
: undefined;
|
|
1443
1483
|
const delivered = { ...msg, text, deliveryId, ack };
|
|
1484
|
+
// ACK BEFORE the handlers, not after. The ACK means "the SDK durably
|
|
1485
|
+
// received this", never "the application finished processing it" — the
|
|
1486
|
+
// two were conflated here and it broke real chat twice over:
|
|
1487
|
+
//
|
|
1488
|
+
// 1. A peer whose handler was slow (beagle-help awaiting a 10s+ model
|
|
1489
|
+
// call) sent its ACK 10s late — inside sendText's 15s auto window —
|
|
1490
|
+
// so senders got "ACK timed out" for messages that were delivered
|
|
1491
|
+
// AND answered. The UI then dropped the sender's own message as
|
|
1492
|
+
// failed: "对方在回答一个看不见的问题".
|
|
1493
|
+
// 2. The delivery id was only registered after the handlers returned,
|
|
1494
|
+
// so the sender's 5s retransmits arriving DURING a slow handler
|
|
1495
|
+
// re-dispatched the same message — the busy peer processed it again
|
|
1496
|
+
// (and hit its model again), making the overload worse.
|
|
1497
|
+
//
|
|
1498
|
+
// Registering + ACKing up front fixes both: retries hit the dedupe path
|
|
1499
|
+
// and re-ACK, and a busy application can no longer turn into a transport
|
|
1500
|
+
// failure. A handler that needs "processed" semantics must build its own
|
|
1501
|
+
// application-level reply — that is what an answer message is.
|
|
1502
|
+
if (deliveryId) {
|
|
1503
|
+
this.#rememberDeliveredTextId(deliveryId);
|
|
1504
|
+
void ack?.();
|
|
1505
|
+
}
|
|
1444
1506
|
try {
|
|
1445
1507
|
for (const handler of this.#textHandlers) {
|
|
1446
1508
|
await handler(delivered);
|
|
@@ -1448,13 +1510,14 @@ export class Peer {
|
|
|
1448
1510
|
}
|
|
1449
1511
|
catch (error) {
|
|
1450
1512
|
this.#debugLog(`text handler failed for ${msg.pubkey}: ${error.message}`);
|
|
1451
|
-
return;
|
|
1452
|
-
}
|
|
1453
|
-
if (deliveryId) {
|
|
1454
|
-
this.#rememberDeliveredTextId(deliveryId);
|
|
1455
|
-
await ack?.();
|
|
1456
1513
|
}
|
|
1457
1514
|
}
|
|
1515
|
+
/** @internal Test seam: deliver a text message exactly as if it arrived off
|
|
1516
|
+
* the wire. Exists so the ACK-before-handlers ordering is testable without
|
|
1517
|
+
* a network — do not use outside tests. */
|
|
1518
|
+
_testDispatchTextMessage(msg) {
|
|
1519
|
+
return this.#dispatchTextMessage(msg);
|
|
1520
|
+
}
|
|
1458
1521
|
async #sendTextAck(pubkey, deliveryId) {
|
|
1459
1522
|
try {
|
|
1460
1523
|
await this.sendText(pubkey, encodeTextAckEnvelope({ t: "ack", id: deliveryId }));
|
package/dist/turn.js
CHANGED
|
@@ -92,12 +92,9 @@ export class TurnClient {
|
|
|
92
92
|
this.#nonce = nonceAttr;
|
|
93
93
|
this.#integrityKey = longTermIntegrityKey(this.creds.username, this.#realm, this.creds.password);
|
|
94
94
|
// Step 2: re-send ALLOCATE authenticated
|
|
95
|
-
const authed = await this.#
|
|
95
|
+
const authed = await this.#authedRequest(TURN_ALLOCATE_REQUEST, TURN_ALLOCATE_SUCCESS, [
|
|
96
96
|
{ type: STUN_ATTR_REQUESTED_TRANSPORT, value: Uint8Array.of(17, 0, 0, 0) },
|
|
97
|
-
{ type: STUN_ATTR_SOFTWARE, value: Buffer.from(SOFTWARE_NAME, "utf8") }
|
|
98
|
-
{ type: STUN_ATTR_USERNAME, value: Buffer.from(this.creds.username, "utf8") },
|
|
99
|
-
{ type: STUN_ATTR_REALM, value: Buffer.from(this.#realm, "utf8") },
|
|
100
|
-
{ type: STUN_ATTR_NONCE, value: this.#nonce }
|
|
97
|
+
{ type: STUN_ATTR_SOFTWARE, value: Buffer.from(SOFTWARE_NAME, "utf8") }
|
|
101
98
|
]);
|
|
102
99
|
if (authed.type !== TURN_ALLOCATE_SUCCESS) {
|
|
103
100
|
const err = findAttr(authed, STUN_ATTR_ERROR_CODE);
|
|
@@ -117,11 +114,8 @@ export class TurnClient {
|
|
|
117
114
|
if (!this.#integrityKey || !this.#nonce) {
|
|
118
115
|
throw new Error("createPermission called before allocate()");
|
|
119
116
|
}
|
|
120
|
-
const resp = await this.#
|
|
121
|
-
{ type: STUN_ATTR_XOR_PEER_ADDRESS, value: encodeXorMappedAddress(peer, newTransactionId()) }
|
|
122
|
-
{ type: STUN_ATTR_USERNAME, value: Buffer.from(this.creds.username, "utf8") },
|
|
123
|
-
{ type: STUN_ATTR_REALM, value: Buffer.from(this.#realm, "utf8") },
|
|
124
|
-
{ type: STUN_ATTR_NONCE, value: this.#nonce }
|
|
117
|
+
const resp = await this.#authedRequest(TURN_CREATE_PERMISSION_REQUEST, TURN_CREATE_PERMISSION_SUCCESS, [
|
|
118
|
+
{ type: STUN_ATTR_XOR_PEER_ADDRESS, value: encodeXorMappedAddress(peer, newTransactionId()) }
|
|
125
119
|
]);
|
|
126
120
|
if (resp.type !== TURN_CREATE_PERMISSION_SUCCESS) {
|
|
127
121
|
const err = findAttr(resp, STUN_ATTR_ERROR_CODE);
|
|
@@ -152,14 +146,12 @@ export class TurnClient {
|
|
|
152
146
|
if (!this.#integrityKey || !this.#nonce) {
|
|
153
147
|
throw new Error("refresh called before allocate()");
|
|
154
148
|
}
|
|
155
|
-
const resp = await this.#
|
|
156
|
-
{ type: STUN_ATTR_LIFETIME, value: u32Bytes(600) }
|
|
157
|
-
{ type: STUN_ATTR_USERNAME, value: Buffer.from(this.creds.username, "utf8") },
|
|
158
|
-
{ type: STUN_ATTR_REALM, value: Buffer.from(this.#realm, "utf8") },
|
|
159
|
-
{ type: STUN_ATTR_NONCE, value: this.#nonce }
|
|
149
|
+
const resp = await this.#authedRequest(TURN_REFRESH_REQUEST, TURN_REFRESH_SUCCESS, [
|
|
150
|
+
{ type: STUN_ATTR_LIFETIME, value: u32Bytes(600) }
|
|
160
151
|
]);
|
|
161
152
|
if (resp.type !== TURN_REFRESH_SUCCESS) {
|
|
162
|
-
//
|
|
153
|
+
// A genuine failure (438 was already retried with the fresh nonce by
|
|
154
|
+
// #authedRequest). Drop the allocation so the next caller re-allocates.
|
|
163
155
|
this.#allocation = undefined;
|
|
164
156
|
const err = findAttr(resp, STUN_ATTR_ERROR_CODE);
|
|
165
157
|
const decoded = err ? decodeErrorCode(err) : undefined;
|
|
@@ -206,6 +198,48 @@ export class TurnClient {
|
|
|
206
198
|
resolve(msg);
|
|
207
199
|
}
|
|
208
200
|
};
|
|
201
|
+
/**
|
|
202
|
+
* Send an authenticated request, honoring nonce rotation.
|
|
203
|
+
*
|
|
204
|
+
* 438 (Stale Nonce) is NOT a failure — it is the protocol's way of rotating
|
|
205
|
+
* the nonce (RFC 5766 §4.3 / RFC 8489 §9.2): the error response carries the
|
|
206
|
+
* NEW nonce (and possibly a new realm), and the client is expected to adopt
|
|
207
|
+
* it and re-send the same request. coturn rotates every few hours; treating
|
|
208
|
+
* the 438 as fatal tore down a healthy relay on every rotation and left the
|
|
209
|
+
* refresh loop dead until the next #ensureTurnRelay (INBOX 2026-08-05).
|
|
210
|
+
*
|
|
211
|
+
* One retry only: a server that answers the fresh nonce with another 438 is
|
|
212
|
+
* genuinely refusing us, and looping would spin.
|
|
213
|
+
*/
|
|
214
|
+
async #authedRequest(type, successType, attrs) {
|
|
215
|
+
const build = () => [
|
|
216
|
+
...attrs,
|
|
217
|
+
{ type: STUN_ATTR_USERNAME, value: Buffer.from(this.creds.username, "utf8") },
|
|
218
|
+
{ type: STUN_ATTR_REALM, value: Buffer.from(this.#realm, "utf8") },
|
|
219
|
+
{ type: STUN_ATTR_NONCE, value: this.#nonce }
|
|
220
|
+
];
|
|
221
|
+
const resp = await this.#request(type, build());
|
|
222
|
+
if (resp.type === successType)
|
|
223
|
+
return resp;
|
|
224
|
+
const err = findAttr(resp, STUN_ATTR_ERROR_CODE);
|
|
225
|
+
const decoded = err ? decodeErrorCode(err) : undefined;
|
|
226
|
+
if (decoded?.code !== 438)
|
|
227
|
+
return resp;
|
|
228
|
+
const newNonce = findAttr(resp, STUN_ATTR_NONCE);
|
|
229
|
+
if (!newNonce)
|
|
230
|
+
return resp; // malformed 438 — nothing to retry with
|
|
231
|
+
this.#nonce = newNonce;
|
|
232
|
+
const newRealm = findAttr(resp, STUN_ATTR_REALM);
|
|
233
|
+
if (newRealm) {
|
|
234
|
+
const realm = Buffer.from(newRealm).toString("utf8");
|
|
235
|
+
if (realm !== this.#realm) {
|
|
236
|
+
// Realm change means the integrity key derives differently too.
|
|
237
|
+
this.#realm = realm;
|
|
238
|
+
this.#integrityKey = longTermIntegrityKey(this.creds.username, this.#realm, this.creds.password);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return this.#request(type, build());
|
|
242
|
+
}
|
|
209
243
|
async #request(type, attrs) {
|
|
210
244
|
const txn = newTransactionId();
|
|
211
245
|
const key = Buffer.from(txn).toString("hex");
|
package/dist/types/peer.d.ts
CHANGED
|
@@ -125,9 +125,10 @@ export type TextMessage = {
|
|
|
125
125
|
/** Stable id for SDK-level acknowledged delivery. Present only for messages
|
|
126
126
|
* sent with sendTextUntilAck(). Receivers can persist this id for dedupe. */
|
|
127
127
|
deliveryId?: string;
|
|
128
|
-
/**
|
|
129
|
-
*
|
|
130
|
-
*
|
|
128
|
+
/** Re-send the delivery ACK. The SDK ACKs automatically BEFORE handlers run
|
|
129
|
+
* — the ACK means "SDK received", never "application processed", so a slow
|
|
130
|
+
* handler (model call, disk job) cannot fake a delivery failure at the
|
|
131
|
+
* sender. Kept on the message for idempotent manual re-ACKs only. */
|
|
131
132
|
ack?: () => Promise<void>;
|
|
132
133
|
/** Delivery path: "online" = live net_crypto session (direct/relay), "offline"
|
|
133
134
|
* = express store-and-forward. Lets the UI color the two differently so a user
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/peer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.137",
|
|
4
4
|
"description": "Pure TypeScript port of Elastos Carrier (toxcore-derived) P2P messaging. DHT, onion routing, TCP relay, FlatBuffers app payloads, Express offline relay. Wire-compatible with iOS Beagle and the Carrier C SDK.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|