@zero-server/sdk 0.9.6 → 0.9.8

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 (94) hide show
  1. package/README.md +54 -53
  2. package/index.js +116 -4
  3. package/lib/app.js +22 -22
  4. package/lib/auth/authorize.js +11 -11
  5. package/lib/auth/enrollment.js +5 -5
  6. package/lib/auth/jwt.js +9 -9
  7. package/lib/auth/oauth.js +1 -1
  8. package/lib/auth/session.js +5 -5
  9. package/lib/auth/trustedDevice.js +2 -2
  10. package/lib/auth/twoFactor.js +11 -11
  11. package/lib/auth/webauthn.js +6 -6
  12. package/lib/body/json.js +1 -1
  13. package/lib/body/raw.js +1 -1
  14. package/lib/body/rawBuffer.js +1 -1
  15. package/lib/body/text.js +1 -1
  16. package/lib/body/urlencoded.js +3 -3
  17. package/lib/cli.js +43 -28
  18. package/lib/cluster.js +3 -3
  19. package/lib/debug.js +10 -10
  20. package/lib/env/index.js +11 -11
  21. package/lib/errors.js +131 -16
  22. package/lib/fetch/index.js +1 -1
  23. package/lib/grpc/call.js +14 -14
  24. package/lib/grpc/client.js +4 -4
  25. package/lib/grpc/codec.js +7 -7
  26. package/lib/grpc/credentials.js +2 -2
  27. package/lib/grpc/frame.js +2 -2
  28. package/lib/grpc/health.js +3 -3
  29. package/lib/grpc/index.js +3 -3
  30. package/lib/grpc/metadata.js +3 -3
  31. package/lib/grpc/proto.js +5 -5
  32. package/lib/grpc/reflection.js +2 -2
  33. package/lib/grpc/server.js +3 -3
  34. package/lib/grpc/status.js +2 -2
  35. package/lib/grpc/watch.js +1 -1
  36. package/lib/http/request.js +13 -13
  37. package/lib/http/response.js +2 -2
  38. package/lib/lifecycle.js +5 -5
  39. package/lib/middleware/compress.js +4 -4
  40. package/lib/observe/health.js +1 -1
  41. package/lib/observe/index.js +1 -1
  42. package/lib/observe/logger.js +3 -3
  43. package/lib/observe/metrics.js +4 -4
  44. package/lib/observe/tracing.js +4 -4
  45. package/lib/orm/adapters/json.js +1 -1
  46. package/lib/orm/adapters/memory.js +2 -2
  47. package/lib/orm/adapters/mongo.js +2 -2
  48. package/lib/orm/adapters/mysql.js +2 -2
  49. package/lib/orm/adapters/postgres.js +2 -2
  50. package/lib/orm/adapters/sqlite.js +3 -3
  51. package/lib/orm/audit.js +1 -1
  52. package/lib/orm/index.js +7 -7
  53. package/lib/orm/migrate.js +1 -1
  54. package/lib/orm/model.js +15 -15
  55. package/lib/orm/procedures.js +1 -1
  56. package/lib/orm/profiler.js +1 -1
  57. package/lib/orm/query.js +9 -9
  58. package/lib/orm/schema.js +1 -1
  59. package/lib/orm/seed/data/person.js +1 -1
  60. package/lib/orm/seed/fake.js +10 -10
  61. package/lib/orm/seed/index.js +4 -4
  62. package/lib/orm/seed/rng.js +1 -1
  63. package/lib/orm/snapshot.js +3 -3
  64. package/lib/orm/tenancy.js +6 -6
  65. package/lib/orm/views.js +1 -1
  66. package/lib/router/index.js +9 -9
  67. package/lib/webrtc/bot.js +405 -0
  68. package/lib/webrtc/cli.js +182 -0
  69. package/lib/webrtc/cluster.js +338 -0
  70. package/lib/webrtc/e2ee.js +274 -0
  71. package/lib/webrtc/ice.js +363 -0
  72. package/lib/webrtc/index.js +212 -0
  73. package/lib/webrtc/joinToken.js +171 -0
  74. package/lib/webrtc/observe.js +260 -0
  75. package/lib/webrtc/peer.js +143 -0
  76. package/lib/webrtc/room.js +184 -0
  77. package/lib/webrtc/sdp.js +503 -0
  78. package/lib/webrtc/sfu/index.js +251 -0
  79. package/lib/webrtc/sfu/livekit.js +304 -0
  80. package/lib/webrtc/sfu/mediasoup.js +357 -0
  81. package/lib/webrtc/sfu/memory.js +221 -0
  82. package/lib/webrtc/signaling.js +590 -0
  83. package/lib/webrtc/stun.js +484 -0
  84. package/lib/webrtc/turn/codec.js +370 -0
  85. package/lib/webrtc/turn/credentials.js +156 -0
  86. package/lib/webrtc/turn/server.js +648 -0
  87. package/package.json +2 -2
  88. package/types/body.d.ts +82 -14
  89. package/types/cli.d.ts +40 -2
  90. package/types/index.d.ts +19 -6
  91. package/types/middleware.d.ts +18 -72
  92. package/types/orm.d.ts +4 -13
  93. package/types/request.d.ts +3 -3
  94. package/types/webrtc.d.ts +501 -0
@@ -0,0 +1,648 @@
1
+ /**
2
+ * @module webrtc/turn/server
3
+ * @description Zero-dependency embedded TURN server (RFC 5766). Pairs with
4
+ * `issueTurnCredentials` for long-term-credential auth and implements
5
+ * UDP allocations, permissions, Send/Data indications, channel bindings,
6
+ * lifetimes, and per-user quotas. TCP/TLS listeners are reserved and
7
+ * currently throw `TURN_TRANSPORT_UNSUPPORTED`.
8
+ *
9
+ * @example | Boot an embedded TURN server alongside Zero Server
10
+ * const { TurnServer, issueTurnCredentials } = require('@zero-server/webrtc');
11
+ *
12
+ * const turn = new TurnServer({
13
+ * secret: process.env.TURN_SECRET,
14
+ * realm: 'rtc.example.com',
15
+ * relayHost: process.env.PUBLIC_IP || '0.0.0.0',
16
+ * listeners: [{ proto: 'udp', port: 3478 }],
17
+ * quotas: { maxAllocationsPerUser: 4, maxBytesPerMinute: 50_000_000 },
18
+ * });
19
+ * await turn.start();
20
+ *
21
+ * turn.on('allocate', ({ user, relay }) => log.info({ user, relay }, 'turn allocate'));
22
+ * turn.on('permission', ({ user, peer }) => log.debug({ user, peer }, 'turn permission'));
23
+ * turn.on('relay', ({ user, bytes }) => metrics.turnBytes.inc(bytes));
24
+ *
25
+ * process.on('SIGTERM', () => turn.close());
26
+ *
27
+ * @example | Issue creds + return them with the room join payload
28
+ * const { TurnServer, issueTurnCredentials } = require('@zero-server/webrtc');
29
+ * const creds = issueTurnCredentials({
30
+ * secret: process.env.TURN_SECRET,
31
+ * userId: req.user.id,
32
+ * servers: ['turn:rtc.example.com:3478'],
33
+ * ttl: '20m',
34
+ * });
35
+ * res.json({ token, iceServers: [creds] });
36
+ *
37
+ * @example | Multi-listener (UDP + future TCP) with tight per-user quotas
38
+ * const turn = new TurnServer({
39
+ * secret: process.env.TURN_SECRET,
40
+ * realm: 'rtc.example.com',
41
+ * listeners: [{ proto: 'udp', port: 3478, host: '0.0.0.0' }],
42
+ * quotas: {
43
+ * maxAllocationsPerUser: 2,
44
+ * maxBytesPerMinute: 5_000_000, // 5 MB/min per user
45
+ * },
46
+ * defaultLifetime: 300,
47
+ * maxLifetime: 1800,
48
+ * });
49
+ */
50
+
51
+ 'use strict';
52
+
53
+ const dgram = require('node:dgram');
54
+ const crypto = require('node:crypto');
55
+ const { EventEmitter } = require('node:events');
56
+
57
+ const { TurnError } = require('../../errors');
58
+ const codec = require('./codec');
59
+
60
+ const {
61
+ STUN_CLASS, TURN_METHOD, ATTR, PROTO_UDP,
62
+ CHANNEL_MIN, CHANNEL_MAX,
63
+ encodeMessage, decodeMessage,
64
+ getAttr, getAttrs,
65
+ longTermKey, verifyIntegrity, findIntegrity,
66
+ encodeErrorCode, encodeUInt32, decodeUInt32,
67
+ encodeChannelNumber, decodeChannelNumber,
68
+ encodeChannelData, decodeChannelData, looksLikeChannelData,
69
+ encodeXorAddress, decodeXorAddress,
70
+ endpointKey,
71
+ } = codec;
72
+
73
+ const DEFAULT_LIFETIME = 600; // 10 minutes
74
+ const MAX_LIFETIME = 3600;
75
+ const PERMISSION_LIFETIME = 300; // 5 minutes
76
+ const CHANNEL_LIFETIME = 600;
77
+ const NONCE_LIFETIME_MS = 60_000;
78
+ const SOFTWARE_NAME = 'zero-server-turn';
79
+
80
+ /**
81
+ * Embedded TURN server backed by `dgram` sockets.
82
+ *
83
+ * @class TurnServer
84
+ * @extends EventEmitter
85
+ */
86
+ class TurnServer extends EventEmitter
87
+ {
88
+ /**
89
+ * @param {object} opts
90
+ * @param {string} opts.secret
91
+ * Shared HMAC secret for ephemeral credentials produced by
92
+ * {@link module:webrtc/turn/credentials.issueTurnCredentials}.
93
+ * @param {string} [opts.realm='zero-server']
94
+ * @param {Array<{proto:'udp'|'tcp'|'tls',port:number,host?:string,tls?:object}>} opts.listeners
95
+ * @param {object} [opts.quotas]
96
+ * @param {number} [opts.quotas.maxAllocationsPerUser=Infinity]
97
+ * @param {number} [opts.quotas.maxBytesPerMinute=Infinity]
98
+ * @param {number} [opts.defaultLifetime=600]
99
+ * @param {number} [opts.maxLifetime=3600]
100
+ * @param {string} [opts.relayHost='127.0.0.1']
101
+ * Address bound by each per-allocation relay socket. Production
102
+ * deployments should pass the server's public IP.
103
+ */
104
+ constructor(opts)
105
+ {
106
+ super();
107
+ const o = opts || {};
108
+ if (typeof o.secret !== 'string' || o.secret.length === 0)
109
+ throw new TurnError('TurnServer: opts.secret is required', { code: 'TURN_CONFIG' });
110
+ if (!Array.isArray(o.listeners) || o.listeners.length === 0)
111
+ throw new TurnError('TurnServer: opts.listeners must be a non-empty array', { code: 'TURN_CONFIG' });
112
+
113
+ this._secret = o.secret;
114
+ this.realm = typeof o.realm === 'string' && o.realm.length ? o.realm : 'zero-server';
115
+ this._listeners = o.listeners.map((l) => Object.assign({}, l));
116
+ this._quotas = Object.assign(
117
+ { maxAllocationsPerUser: Infinity, maxBytesPerMinute: Infinity },
118
+ o.quotas || {},
119
+ );
120
+ this._defaultLifetime = Number.isFinite(o.defaultLifetime) ? o.defaultLifetime : DEFAULT_LIFETIME;
121
+ this._maxLifetime = Number.isFinite(o.maxLifetime) ? o.maxLifetime : MAX_LIFETIME;
122
+ this._relayHost = typeof o.relayHost === 'string' ? o.relayHost : '127.0.0.1';
123
+
124
+ /** @type {Map<string, _Allocation>} */
125
+ this._allocations = new Map();
126
+ /** @type {Map<string, Set<string>>} */
127
+ this._userAllocs = new Map();
128
+ /** @type {Map<string, {windowStart:number, bytes:number}>} */
129
+ this._userBytes = new Map();
130
+ /** @type {Map<string, {value:string, expiresAt:number}>} */
131
+ this._nonces = new Map();
132
+ /** @type {Array<{proto:string, socket:any, address:string, port:number}>} */
133
+ this._bound = [];
134
+
135
+ this._closed = false;
136
+ }
137
+
138
+ // ------------------------------------------------------------------
139
+ // Lifecycle
140
+ // ------------------------------------------------------------------
141
+
142
+ /**
143
+ * Bind all configured listeners. Resolves once every listener is
144
+ * listening; rejects on the first bind error.
145
+ *
146
+ * @returns {Promise<void>}
147
+ */
148
+ async start()
149
+ {
150
+ if (this._closed) throw new TurnError('TurnServer: already stopped', { code: 'TURN_STOPPED' });
151
+ for (const l of this._listeners)
152
+ {
153
+ if (l.proto !== 'udp')
154
+ throw new TurnError(
155
+ `TurnServer: ${l.proto} listeners are not implemented yet`,
156
+ { code: 'TURN_TRANSPORT_UNSUPPORTED' },
157
+ );
158
+ const sock = dgram.createSocket(l.family === 6 ? 'udp6' : 'udp4');
159
+ await new Promise((resolve, reject) =>
160
+ {
161
+ sock.once('error', reject);
162
+ sock.bind(l.port, l.host || '127.0.0.1', () =>
163
+ {
164
+ sock.removeListener('error', reject);
165
+ resolve();
166
+ });
167
+ });
168
+ const addr = sock.address();
169
+ sock.on('message', (msg, rinfo) => this._onClientMessage(sock, msg, rinfo));
170
+ sock.on('error', (err) => this.emit('error', err));
171
+ this._bound.push({ proto: 'udp', socket: sock, address: addr.address, port: addr.port });
172
+ }
173
+ this._sweepInterval = setInterval(() => this._sweep(), 1000);
174
+ if (this._sweepInterval.unref) this._sweepInterval.unref();
175
+ }
176
+
177
+ /**
178
+ * Close all listeners and free every allocation's relay socket.
179
+ *
180
+ * @returns {Promise<void>}
181
+ */
182
+ async stop()
183
+ {
184
+ this._closed = true;
185
+ if (this._sweepInterval) { clearInterval(this._sweepInterval); this._sweepInterval = null; }
186
+ for (const alloc of this._allocations.values()) this._freeAllocation(alloc);
187
+ this._allocations.clear();
188
+ this._userAllocs.clear();
189
+ const closes = this._bound.map((b) => new Promise((r) => b.socket.close(r)));
190
+ this._bound = [];
191
+ await Promise.all(closes);
192
+ }
193
+
194
+ /**
195
+ * The bound address of the first listener (handy for tests that ask
196
+ * the kernel for an ephemeral port via `port: 0`).
197
+ *
198
+ * @returns {{address:string, port:number}|null}
199
+ */
200
+ address()
201
+ {
202
+ return this._bound.length ? { address: this._bound[0].address, port: this._bound[0].port } : null;
203
+ }
204
+
205
+ // ------------------------------------------------------------------
206
+ // Dispatch
207
+ // ------------------------------------------------------------------
208
+
209
+ /** @private */
210
+ _onClientMessage(sock, raw, rinfo)
211
+ {
212
+ try
213
+ {
214
+ if (looksLikeChannelData(raw))
215
+ {
216
+ this._handleChannelData(sock, raw, rinfo);
217
+ return;
218
+ }
219
+ const msg = decodeMessage(raw);
220
+ switch (msg.method)
221
+ {
222
+ case TURN_METHOD.ALLOCATE: this._handleAllocate(sock, raw, msg, rinfo); break;
223
+ case TURN_METHOD.REFRESH: this._handleRefresh(sock, raw, msg, rinfo); break;
224
+ case TURN_METHOD.CREATE_PERMISSION: this._handleCreatePermission(sock, raw, msg, rinfo); break;
225
+ case TURN_METHOD.CHANNEL_BIND: this._handleChannelBind(sock, raw, msg, rinfo); break;
226
+ case TURN_METHOD.SEND: this._handleSend(sock, raw, msg, rinfo); break;
227
+ default:
228
+ this._sendError(sock, msg.method, msg.transactionId, rinfo, 400, 'Bad Request');
229
+ }
230
+ }
231
+ catch (err)
232
+ {
233
+ this.emit('error', err);
234
+ }
235
+ }
236
+
237
+ // ------------------------------------------------------------------
238
+ // Auth
239
+ // ------------------------------------------------------------------
240
+
241
+ /**
242
+ * Validate a request that carries a USERNAME / REALM / NONCE / MI set.
243
+ * Returns `{ ok:true, key, username, userId }` on success or
244
+ * `{ ok:false, code, reason, withNonce }` on failure.
245
+ *
246
+ * @private
247
+ */
248
+ _checkAuth(raw, msg, rinfo)
249
+ {
250
+ const usernameBuf = getAttr(msg, ATTR.USERNAME);
251
+ const realmBuf = getAttr(msg, ATTR.REALM);
252
+ const nonceBuf = getAttr(msg, ATTR.NONCE);
253
+ const miSpec = findIntegrity(raw);
254
+ if (!usernameBuf || !realmBuf || !nonceBuf || !miSpec)
255
+ return { ok: false, code: 401, reason: 'Unauthorized', withNonce: true };
256
+
257
+ const username = usernameBuf.toString('utf8');
258
+ const realm = realmBuf.toString('utf8');
259
+ const nonce = nonceBuf.toString('utf8');
260
+
261
+ if (realm !== this.realm)
262
+ return { ok: false, code: 441, reason: 'Wrong Credentials' };
263
+
264
+ const stored = this._nonces.get(`${rinfo.address}:${rinfo.port}`);
265
+ if (!stored || stored.value !== nonce || stored.expiresAt <= Date.now())
266
+ return { ok: false, code: 438, reason: 'Stale Nonce', withNonce: true };
267
+
268
+ // ephemeral credentials: username = "<expiry>:<userId>"
269
+ const colon = username.indexOf(':');
270
+ if (colon <= 0)
271
+ return { ok: false, code: 441, reason: 'Wrong Credentials' };
272
+ const expiry = Number(username.slice(0, colon));
273
+ const userId = username.slice(colon + 1);
274
+ if (!Number.isFinite(expiry) || expiry <= Math.floor(Date.now() / 1000))
275
+ return { ok: false, code: 401, reason: 'Expired Credentials', withNonce: true };
276
+
277
+ const password = crypto.createHmac('sha1', this._secret).update(username).digest('base64');
278
+ const key = longTermKey(username, this.realm, password);
279
+
280
+ if (!verifyIntegrity(raw, key, miSpec.value, miSpec.offset))
281
+ return { ok: false, code: 401, reason: 'Bad MAC', withNonce: true };
282
+
283
+ return { ok: true, key, username, userId };
284
+ }
285
+
286
+ /** @private */
287
+ _issueNonce(rinfo)
288
+ {
289
+ const value = crypto.randomBytes(12).toString('hex');
290
+ this._nonces.set(`${rinfo.address}:${rinfo.port}`, {
291
+ value, expiresAt: Date.now() + NONCE_LIFETIME_MS,
292
+ });
293
+ return value;
294
+ }
295
+
296
+ /** @private */
297
+ _sendError(sock, method, txid, rinfo, code, reason, opts)
298
+ {
299
+ const attrs = [
300
+ { type: ATTR.ERROR_CODE, value: encodeErrorCode(code, reason) },
301
+ ];
302
+ if (opts && opts.withNonce)
303
+ {
304
+ attrs.push({ type: ATTR.NONCE, value: Buffer.from(this._issueNonce(rinfo), 'utf8') });
305
+ attrs.push({ type: ATTR.REALM, value: Buffer.from(this.realm, 'utf8') });
306
+ }
307
+ attrs.push({ type: ATTR.SOFTWARE, value: Buffer.from(SOFTWARE_NAME, 'utf8') });
308
+ const buf = encodeMessage(method, STUN_CLASS.ERROR, txid, attrs);
309
+ sock.send(buf, rinfo.port, rinfo.address);
310
+ }
311
+
312
+ // ------------------------------------------------------------------
313
+ // ALLOCATE
314
+ // ------------------------------------------------------------------
315
+
316
+ /** @private */
317
+ _handleAllocate(sock, raw, msg, rinfo)
318
+ {
319
+ const clientKey = endpointKey(rinfo.address, rinfo.port);
320
+ if (this._allocations.has(clientKey))
321
+ {
322
+ this._sendError(sock, msg.method, msg.transactionId, rinfo, 437, 'Allocation Mismatch');
323
+ return;
324
+ }
325
+
326
+ const auth = this._checkAuth(raw, msg, rinfo);
327
+ if (!auth.ok)
328
+ {
329
+ this._sendError(sock, msg.method, msg.transactionId, rinfo,
330
+ auth.code, auth.reason, { withNonce: auth.withNonce });
331
+ return;
332
+ }
333
+
334
+ const rt = getAttr(msg, ATTR.REQUESTED_TRANSPORT);
335
+ if (!rt || rt.length < 1 || rt[0] !== PROTO_UDP)
336
+ {
337
+ this._sendError(sock, msg.method, msg.transactionId, rinfo, 442, 'Unsupported Transport Protocol');
338
+ return;
339
+ }
340
+
341
+ const userSet = this._userAllocs.get(auth.userId) || new Set();
342
+ if (userSet.size >= this._quotas.maxAllocationsPerUser)
343
+ {
344
+ this._sendError(sock, msg.method, msg.transactionId, rinfo, 486, 'Allocation Quota Reached');
345
+ return;
346
+ }
347
+
348
+ const lifetimeAttr = getAttr(msg, ATTR.LIFETIME);
349
+ const requested = lifetimeAttr ? decodeUInt32(lifetimeAttr) : this._defaultLifetime;
350
+ const lifetime = Math.max(60, Math.min(this._maxLifetime, requested || this._defaultLifetime));
351
+
352
+ const relay = dgram.createSocket('udp4');
353
+ relay.bind({ address: this._relayHost, port: 0 }, () =>
354
+ {
355
+ const ra = relay.address();
356
+ const alloc = {
357
+ clientKey, sock, rinfo: { address: rinfo.address, port: rinfo.port },
358
+ userId: auth.userId, key: auth.key,
359
+ relay, relayAddress: ra.address, relayPort: ra.port,
360
+ permissions: new Map(), // peerIp -> expiresAt
361
+ channels: new Map(), // channel# -> { peerIp, peerPort, expiresAt }
362
+ channelByPeer: new Map(), // "ip:port" -> channel#
363
+ expiresAt: Date.now() + lifetime * 1000,
364
+ };
365
+ this._allocations.set(clientKey, alloc);
366
+ userSet.add(clientKey);
367
+ this._userAllocs.set(auth.userId, userSet);
368
+
369
+ relay.on('message', (data, peerRinfo) => this._onRelayMessage(alloc, data, peerRinfo));
370
+ relay.on('error', (err) => this.emit('error', err));
371
+
372
+ const attrs = [
373
+ { type: ATTR.XOR_RELAYED_ADDRESS, value: encodeXorAddress(ra.address, ra.port, msg.transactionId) },
374
+ { type: ATTR.XOR_MAPPED_ADDRESS, value: encodeXorAddress(rinfo.address, rinfo.port, msg.transactionId) },
375
+ { type: ATTR.LIFETIME, value: encodeUInt32(lifetime) },
376
+ { type: ATTR.SOFTWARE, value: Buffer.from(SOFTWARE_NAME, 'utf8') },
377
+ ];
378
+ const reply = encodeMessage(msg.method, STUN_CLASS.SUCCESS, msg.transactionId, attrs, auth.key);
379
+ sock.send(reply, rinfo.port, rinfo.address);
380
+ this.emit('allocation', { userId: auth.userId, relay: ra, client: rinfo });
381
+ });
382
+ relay.on('error', (err) => this.emit('error', err));
383
+ }
384
+
385
+ // ------------------------------------------------------------------
386
+ // REFRESH
387
+ // ------------------------------------------------------------------
388
+
389
+ /** @private */
390
+ _handleRefresh(sock, raw, msg, rinfo)
391
+ {
392
+ const alloc = this._allocations.get(endpointKey(rinfo.address, rinfo.port));
393
+ if (!alloc)
394
+ {
395
+ this._sendError(sock, msg.method, msg.transactionId, rinfo, 437, 'Allocation Mismatch');
396
+ return;
397
+ }
398
+ const auth = this._checkAuth(raw, msg, rinfo);
399
+ if (!auth.ok)
400
+ {
401
+ this._sendError(sock, msg.method, msg.transactionId, rinfo,
402
+ auth.code, auth.reason, { withNonce: auth.withNonce });
403
+ return;
404
+ }
405
+
406
+ const lifetimeAttr = getAttr(msg, ATTR.LIFETIME);
407
+ const requested = lifetimeAttr ? decodeUInt32(lifetimeAttr) : this._defaultLifetime;
408
+ let lifetime;
409
+ if (requested === 0)
410
+ {
411
+ this._freeAllocation(alloc);
412
+ this._allocations.delete(alloc.clientKey);
413
+ const set = this._userAllocs.get(alloc.userId);
414
+ if (set) { set.delete(alloc.clientKey); if (!set.size) this._userAllocs.delete(alloc.userId); }
415
+ lifetime = 0;
416
+ this.emit('deallocation', { userId: alloc.userId, client: rinfo });
417
+ }
418
+ else
419
+ {
420
+ lifetime = Math.max(60, Math.min(this._maxLifetime, requested));
421
+ alloc.expiresAt = Date.now() + lifetime * 1000;
422
+ }
423
+ const reply = encodeMessage(msg.method, STUN_CLASS.SUCCESS, msg.transactionId, [
424
+ { type: ATTR.LIFETIME, value: encodeUInt32(lifetime) },
425
+ { type: ATTR.SOFTWARE, value: Buffer.from(SOFTWARE_NAME, 'utf8') },
426
+ ], auth.key);
427
+ sock.send(reply, rinfo.port, rinfo.address);
428
+ }
429
+
430
+ // ------------------------------------------------------------------
431
+ // CREATE-PERMISSION
432
+ // ------------------------------------------------------------------
433
+
434
+ /** @private */
435
+ _handleCreatePermission(sock, raw, msg, rinfo)
436
+ {
437
+ const alloc = this._allocations.get(endpointKey(rinfo.address, rinfo.port));
438
+ if (!alloc)
439
+ {
440
+ this._sendError(sock, msg.method, msg.transactionId, rinfo, 437, 'Allocation Mismatch');
441
+ return;
442
+ }
443
+ const auth = this._checkAuth(raw, msg, rinfo);
444
+ if (!auth.ok)
445
+ {
446
+ this._sendError(sock, msg.method, msg.transactionId, rinfo,
447
+ auth.code, auth.reason, { withNonce: auth.withNonce });
448
+ return;
449
+ }
450
+ const peerAttrs = getAttrs(msg, ATTR.XOR_PEER_ADDRESS);
451
+ if (peerAttrs.length === 0)
452
+ {
453
+ this._sendError(sock, msg.method, msg.transactionId, rinfo, 400, 'Bad Request');
454
+ return;
455
+ }
456
+ const now = Date.now();
457
+ for (const av of peerAttrs)
458
+ {
459
+ const peer = decodeXorAddress(av, msg.transactionId);
460
+ alloc.permissions.set(peer.address, now + PERMISSION_LIFETIME * 1000);
461
+ }
462
+ const reply = encodeMessage(msg.method, STUN_CLASS.SUCCESS, msg.transactionId, [
463
+ { type: ATTR.SOFTWARE, value: Buffer.from(SOFTWARE_NAME, 'utf8') },
464
+ ], auth.key);
465
+ sock.send(reply, rinfo.port, rinfo.address);
466
+ }
467
+
468
+ // ------------------------------------------------------------------
469
+ // CHANNEL-BIND
470
+ // ------------------------------------------------------------------
471
+
472
+ /** @private */
473
+ _handleChannelBind(sock, raw, msg, rinfo)
474
+ {
475
+ const alloc = this._allocations.get(endpointKey(rinfo.address, rinfo.port));
476
+ if (!alloc)
477
+ {
478
+ this._sendError(sock, msg.method, msg.transactionId, rinfo, 437, 'Allocation Mismatch');
479
+ return;
480
+ }
481
+ const auth = this._checkAuth(raw, msg, rinfo);
482
+ if (!auth.ok)
483
+ {
484
+ this._sendError(sock, msg.method, msg.transactionId, rinfo,
485
+ auth.code, auth.reason, { withNonce: auth.withNonce });
486
+ return;
487
+ }
488
+ const chAttr = getAttr(msg, ATTR.CHANNEL_NUMBER);
489
+ const peerAttr = getAttr(msg, ATTR.XOR_PEER_ADDRESS);
490
+ if (!chAttr || !peerAttr)
491
+ {
492
+ this._sendError(sock, msg.method, msg.transactionId, rinfo, 400, 'Bad Request');
493
+ return;
494
+ }
495
+ const channel = decodeChannelNumber(chAttr);
496
+ if (channel < CHANNEL_MIN || channel > CHANNEL_MAX)
497
+ {
498
+ this._sendError(sock, msg.method, msg.transactionId, rinfo, 400, 'Bad Channel Number');
499
+ return;
500
+ }
501
+ const peer = decodeXorAddress(peerAttr, msg.transactionId);
502
+ const peerKey = endpointKey(peer.address, peer.port);
503
+ const existing = alloc.channels.get(channel);
504
+ if (existing && (existing.peerIp !== peer.address || existing.peerPort !== peer.port))
505
+ {
506
+ this._sendError(sock, msg.method, msg.transactionId, rinfo, 400, 'Channel In Use');
507
+ return;
508
+ }
509
+ const otherChan = alloc.channelByPeer.get(peerKey);
510
+ if (otherChan && otherChan !== channel)
511
+ {
512
+ this._sendError(sock, msg.method, msg.transactionId, rinfo, 400, 'Peer Already Bound');
513
+ return;
514
+ }
515
+ alloc.channels.set(channel, {
516
+ peerIp: peer.address, peerPort: peer.port,
517
+ expiresAt: Date.now() + CHANNEL_LIFETIME * 1000,
518
+ });
519
+ alloc.channelByPeer.set(peerKey, channel);
520
+ alloc.permissions.set(peer.address, Date.now() + PERMISSION_LIFETIME * 1000);
521
+
522
+ const reply = encodeMessage(msg.method, STUN_CLASS.SUCCESS, msg.transactionId, [
523
+ { type: ATTR.SOFTWARE, value: Buffer.from(SOFTWARE_NAME, 'utf8') },
524
+ ], auth.key);
525
+ sock.send(reply, rinfo.port, rinfo.address);
526
+ }
527
+
528
+ // ------------------------------------------------------------------
529
+ // SEND (indication)
530
+ // ------------------------------------------------------------------
531
+
532
+ /** @private */
533
+ _handleSend(sock, raw, msg, rinfo)
534
+ {
535
+ const alloc = this._allocations.get(endpointKey(rinfo.address, rinfo.port));
536
+ if (!alloc) return; // indications are unauthenticated, silent drop
537
+ const peerAttr = getAttr(msg, ATTR.XOR_PEER_ADDRESS);
538
+ const data = getAttr(msg, ATTR.DATA);
539
+ if (!peerAttr || !data) return;
540
+ const peer = decodeXorAddress(peerAttr, msg.transactionId);
541
+ const exp = alloc.permissions.get(peer.address);
542
+ if (!exp || exp <= Date.now()) return;
543
+ if (!this._chargeBytes(alloc.userId, data.length)) return;
544
+ alloc.relay.send(data, peer.port, peer.address);
545
+ }
546
+
547
+ // ------------------------------------------------------------------
548
+ // ChannelData (no STUN header)
549
+ // ------------------------------------------------------------------
550
+
551
+ /** @private */
552
+ _handleChannelData(sock, raw, rinfo)
553
+ {
554
+ const alloc = this._allocations.get(endpointKey(rinfo.address, rinfo.port));
555
+ if (!alloc) return;
556
+ const cd = decodeChannelData(raw);
557
+ if (!cd) return;
558
+ const bind = alloc.channels.get(cd.channel);
559
+ if (!bind || bind.expiresAt <= Date.now()) return;
560
+ if (!this._chargeBytes(alloc.userId, cd.payload.length)) return;
561
+ alloc.relay.send(cd.payload, bind.peerPort, bind.peerIp);
562
+ }
563
+
564
+ // ------------------------------------------------------------------
565
+ // Relay -> client
566
+ // ------------------------------------------------------------------
567
+
568
+ /** @private */
569
+ _onRelayMessage(alloc, data, peerRinfo)
570
+ {
571
+ const exp = alloc.permissions.get(peerRinfo.address);
572
+ if (!exp || exp <= Date.now()) return;
573
+ if (!this._chargeBytes(alloc.userId, data.length)) return;
574
+
575
+ const channel = alloc.channelByPeer.get(endpointKey(peerRinfo.address, peerRinfo.port));
576
+ if (channel)
577
+ {
578
+ const frame = encodeChannelData(channel, data);
579
+ alloc.sock.send(frame, alloc.rinfo.port, alloc.rinfo.address);
580
+ return;
581
+ }
582
+ const txid = crypto.randomBytes(12);
583
+ const attrs = [
584
+ { type: ATTR.XOR_PEER_ADDRESS, value: encodeXorAddress(peerRinfo.address, peerRinfo.port, txid) },
585
+ { type: ATTR.DATA, value: data },
586
+ ];
587
+ const ind = encodeMessage(TURN_METHOD.DATA, STUN_CLASS.INDICATION, txid, attrs);
588
+ alloc.sock.send(ind, alloc.rinfo.port, alloc.rinfo.address);
589
+ }
590
+
591
+ // ------------------------------------------------------------------
592
+ // Quotas + sweep
593
+ // ------------------------------------------------------------------
594
+
595
+ /** @private */
596
+ _chargeBytes(userId, n)
597
+ {
598
+ const cap = this._quotas.maxBytesPerMinute;
599
+ if (!Number.isFinite(cap)) return true;
600
+ const now = Date.now();
601
+ let q = this._userBytes.get(userId);
602
+ if (!q || now - q.windowStart >= 60_000)
603
+ {
604
+ q = { windowStart: now, bytes: 0 };
605
+ this._userBytes.set(userId, q);
606
+ }
607
+ if (q.bytes + n > cap) return false;
608
+ q.bytes += n;
609
+ return true;
610
+ }
611
+
612
+ /** @private */
613
+ _freeAllocation(alloc)
614
+ {
615
+ try { alloc.relay.close(); } catch (_) { /* ignore */ }
616
+ }
617
+
618
+ /** @private */
619
+ _sweep()
620
+ {
621
+ const now = Date.now();
622
+ for (const [k, alloc] of this._allocations)
623
+ {
624
+ if (alloc.expiresAt <= now)
625
+ {
626
+ this._freeAllocation(alloc);
627
+ this._allocations.delete(k);
628
+ const set = this._userAllocs.get(alloc.userId);
629
+ if (set) { set.delete(k); if (!set.size) this._userAllocs.delete(alloc.userId); }
630
+ this.emit('deallocation', { userId: alloc.userId, client: alloc.rinfo, reason: 'expired' });
631
+ continue;
632
+ }
633
+ for (const [ip, exp] of alloc.permissions)
634
+ if (exp <= now) alloc.permissions.delete(ip);
635
+ for (const [chan, b] of alloc.channels)
636
+ {
637
+ if (b.expiresAt <= now)
638
+ {
639
+ alloc.channels.delete(chan);
640
+ alloc.channelByPeer.delete(endpointKey(b.peerIp, b.peerPort));
641
+ }
642
+ }
643
+ }
644
+ for (const [k, n] of this._nonces) if (n.expiresAt <= now) this._nonces.delete(k);
645
+ }
646
+ }
647
+
648
+ module.exports = { TurnServer };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zero-server/sdk",
3
- "version": "0.9.6",
4
- "description": "Zero-dependency backend framework for Node.js routing, ORM, auth, WebSocket, SSE, gRPC, observability, and 20+ middleware. Distributed as a single SDK and as scoped @zero-server/* packages.",
3
+ "version": "0.9.8",
4
+ "description": "Zero-dependency backend framework for Node.js - routing, ORM, auth, WebSocket, SSE, gRPC, observability, and 20+ middleware. Distributed as a single SDK and as scoped @zero-server/* packages.",
5
5
  "main": "index.js",
6
6
  "bin": {
7
7
  "zh": "lib/cli.js",