@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 CHANGED
@@ -1712,7 +1712,24 @@ export class Peer {
1712
1712
  this.#events.on("friendInfo", cb);
1713
1713
  }
1714
1714
  friends() {
1715
- return [...this.#friends.values()];
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.#request(TURN_ALLOCATE_REQUEST, [
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.#request(TURN_CREATE_PERMISSION_REQUEST, [
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.#request(TURN_REFRESH_REQUEST, [
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
- // Stale nonce server may have rotated. Re-allocate on next call.
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.136",
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",