@decentnetwork/peer 0.1.136 → 0.1.138
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/peer.js +18 -1
- package/dist/turn.js +50 -16
- package/package.json +1 -1
package/dist/peer.js
CHANGED
|
@@ -1712,7 +1712,24 @@ export class Peer {
|
|
|
1712
1712
|
this.#events.on("friendInfo", cb);
|
|
1713
1713
|
}
|
|
1714
1714
|
friends() {
|
|
1715
|
-
|
|
1715
|
+
// Fill in `address` when the record lacks one (friends who requested US,
|
|
1716
|
+
// or were adopted from a session, never carried it). The address is
|
|
1717
|
+
// pubkey ‖ nospam(4B LE) ‖ checksum(2B) — locally derivable, no lookup.
|
|
1718
|
+
// It is the ONLY string another person can send a friend request to, so
|
|
1719
|
+
// a client that can't show it can't let its user recommend a contact
|
|
1720
|
+
// (INBOX 2026-08-05). Use the friend's recorded nospam when we have it
|
|
1721
|
+
// (their request told us); otherwise 0, which is what every current
|
|
1722
|
+
// deployment uses.
|
|
1723
|
+
return [...this.#friends.values()].map((f) => {
|
|
1724
|
+
if (f.address)
|
|
1725
|
+
return f;
|
|
1726
|
+
try {
|
|
1727
|
+
return { ...f, address: carrierAddressFromPublicKey(base58ToBytes(f.pubkey), f.nospam ?? 0) };
|
|
1728
|
+
}
|
|
1729
|
+
catch {
|
|
1730
|
+
return f; // malformed pubkey — better an absent address than a wrong one
|
|
1731
|
+
}
|
|
1732
|
+
});
|
|
1716
1733
|
}
|
|
1717
1734
|
/**
|
|
1718
1735
|
* Read-only snapshot of the live net_crypto session state for a
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/peer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.138",
|
|
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",
|