@decentnetwork/peer 0.1.136 → 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.
Files changed (2) hide show
  1. package/dist/turn.js +50 -16
  2. package/package.json +1 -1
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.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",