@zero-server/sdk 0.9.5 → 0.9.7

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