@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,132 @@
1
+ /**
2
+ * @module @zero-server/webrtc
3
+ * @description First-class WebRTC support for Zero Server.
4
+ *
5
+ * Signaling hub, room / peer orchestration, RFC 8489 STUN client,
6
+ * RFC 7635 TURN credential issuance, optional embedded TURN server,
7
+ * SFrame E2EE key relay, and a pluggable SFU adapter interface.
8
+ *
9
+ * Implementation is landing PR-by-PR per `.myshit/WEBRTC-ROADMAP.md`.
10
+ * Real exports already live in this barrel; the rest throw
11
+ * `WEBRTC_NOT_IMPLEMENTED` so accidental production use fails loud.
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ const {
17
+ WebRTCError, SignalingError, IceError, TurnError, SdpError,
18
+ } = require('../errors');
19
+
20
+ const { parseSdp, stringifySdp } = require('./sdp');
21
+ const {
22
+ parseCandidate, stringifyCandidate, filterCandidates,
23
+ isPrivateIp, isLoopbackIp, isLinkLocalIp, isMdnsHostname,
24
+ CANDIDATE_TYPES, TCP_TYPES,
25
+ } = require('./ice');
26
+ const {
27
+ stunBinding, encodeBindingRequest, decodeMessage,
28
+ encodeXorMappedAddress, decodeXorMappedAddress,
29
+ STUN_MAGIC_COOKIE, STUN_METHOD, STUN_CLASS, STUN_ATTR,
30
+ } = require('./stun');
31
+ const { issueTurnCredentials } = require('./turn/credentials');
32
+ const { TurnServer } = require('./turn/server');
33
+ const { SignalingHub, Room, Peer, PEER_STATE } = require('./signaling');
34
+ const { signJoinToken, verifyJoinToken } = require('./joinToken');
35
+ const { bindObservability } = require('./observe');
36
+ const {
37
+ E2eeChannel, attachE2ee,
38
+ generateE2eeKeyPair, sealKey, openSealedKey,
39
+ } = require('./e2ee');
40
+ const {
41
+ useCluster, ClusterCoordinator, MemoryClusterAdapter,
42
+ } = require('./cluster');
43
+ const { runWebRTCCommand } = require('./cli');
44
+ const { SfuAdapter, loadSfuAdapter } = require('./sfu');
45
+ const { MemorySfuAdapter } = require('./sfu/memory');
46
+ const { MediasoupSfuAdapter } = require('./sfu/mediasoup');
47
+ const { LiveKitSfuAdapter } = require('./sfu/livekit');
48
+ const { spawnBotPeer } = require('./bot');
49
+
50
+ /**
51
+ * @private
52
+ * Sentinel for surfaces that have not yet been implemented. Each PR in the
53
+ * roadmap replaces one of these with a real function/class export.
54
+ */
55
+ const notImplemented = (name) =>
56
+ {
57
+ throw new WebRTCError(
58
+ `${name} is not implemented yet - see .myshit/WEBRTC-ROADMAP.md for the implementation plan.`,
59
+ { code: 'WEBRTC_NOT_IMPLEMENTED' },
60
+ );
61
+ };
62
+
63
+ module.exports = {
64
+ // Signaling - landing in a later PR
65
+ createWebRTC: () => notImplemented('createWebRTC'),
66
+ SignalingHub,
67
+ Room,
68
+ Peer,
69
+ PEER_STATE,
70
+
71
+ // SDP - PR 1
72
+ parseSdp,
73
+ stringifySdp,
74
+
75
+ // ICE - PR 1
76
+ parseCandidate,
77
+ stringifyCandidate,
78
+ filterCandidates,
79
+ isPrivateIp,
80
+ isLoopbackIp,
81
+ isLinkLocalIp,
82
+ isMdnsHostname,
83
+ CANDIDATE_TYPES,
84
+ TCP_TYPES,
85
+
86
+ // NAT traversal - later PRs
87
+ stunBinding,
88
+ encodeBindingRequest,
89
+ decodeMessage,
90
+ encodeXorMappedAddress,
91
+ decodeXorMappedAddress,
92
+ STUN_MAGIC_COOKIE,
93
+ STUN_METHOD,
94
+ STUN_CLASS,
95
+ STUN_ATTR,
96
+ issueTurnCredentials,
97
+ TurnServer,
98
+
99
+ // SFU + tokens - later PRs
100
+ SfuAdapter,
101
+ MemorySfuAdapter,
102
+ MediasoupSfuAdapter,
103
+ LiveKitSfuAdapter,
104
+ loadSfuAdapter,
105
+ signJoinToken,
106
+ verifyJoinToken,
107
+
108
+ // Server-side WebRTC peer (wrtc bot)
109
+ spawnBotPeer,
110
+
111
+ // Observability - PR 6
112
+ bindObservability,
113
+
114
+ // E2EE key relay - PR 7
115
+ E2eeChannel,
116
+ attachE2ee,
117
+ generateE2eeKeyPair,
118
+ sealKey,
119
+ openSealedKey,
120
+
121
+ // Cluster - PR 8
122
+ useCluster,
123
+ ClusterCoordinator,
124
+ MemoryClusterAdapter,
125
+
126
+ // CLI
127
+ runWebRTCCommand,
128
+
129
+ // Errors (re-exported from lib/errors.js so consumers can `instanceof` them
130
+ // through @zero-server/webrtc without also requiring @zero-server/errors).
131
+ WebRTCError, SignalingError, IceError, TurnError, SdpError,
132
+ };
@@ -0,0 +1,116 @@
1
+ /**
2
+ * @module webrtc/joinToken
3
+ * @description Signed, short-TTL join tokens that authenticate a peer's
4
+ * right to join a specific room. JWT-shaped (HS256 by default)
5
+ * and audience-scoped to `room:<name>` so a token leaked from
6
+ * one channel cannot be replayed against another.
7
+ *
8
+ * Reuses the canonical sign/verify primitives from `lib/auth/jwt.js`.
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const { sign, verify } = require('../auth');
14
+ const { SignalingError, WebRTCError } = require('../errors');
15
+
16
+ /**
17
+ * Issue a join token for `user` to enter `room`.
18
+ *
19
+ * @param {object} opts
20
+ * @param {string|Buffer} opts.secret - HMAC secret (HS256) or PEM key (RS256).
21
+ * @param {string|object} opts.user - User identifier (string) or object containing `id`.
22
+ * @param {string} opts.room - Target room name.
23
+ * @param {number} [opts.ttl=300] - Seconds until expiry. Negative values are accepted
24
+ * (used by tests to mint already-expired tokens).
25
+ * @param {string} [opts.algorithm='HS256']
26
+ * @param {string|string[]} [opts.audience] - Override the default `room:<name>` audience.
27
+ * @param {object} [opts.claims] - Additional claims merged into the payload.
28
+ * @returns {string} Compact JWT.
29
+ *
30
+ * @example
31
+ * const token = signJoinToken({
32
+ * secret: process.env.JOIN_SECRET,
33
+ * user: req.user,
34
+ * room: 'boardroom',
35
+ * ttl: 300,
36
+ * });
37
+ * res.json({ wsUrl: '/rtc', token });
38
+ *
39
+ * @section Signaling
40
+ */
41
+ function signJoinToken(opts = {})
42
+ {
43
+ if (!opts || typeof opts !== 'object')
44
+ throw new SignalingError('signJoinToken: opts must be an object');
45
+ if (!opts.secret) throw new SignalingError('signJoinToken: secret is required');
46
+ if (opts.user === undefined || opts.user === null)
47
+ throw new SignalingError('signJoinToken: user is required');
48
+ if (typeof opts.room !== 'string' || opts.room.length === 0)
49
+ throw new SignalingError('signJoinToken: room is required');
50
+
51
+ const ttl = Number.isFinite(opts.ttl) ? opts.ttl : 300;
52
+ const sub = typeof opts.user === 'string' ? opts.user
53
+ : (opts.user && (opts.user.id || opts.user.userId || opts.user.sub));
54
+ if (!sub) throw new SignalingError('signJoinToken: user.id is required');
55
+
56
+ const payload = Object.assign({}, opts.claims || {}, {
57
+ room: opts.room,
58
+ user: typeof opts.user === 'object' ? opts.user : { id: sub },
59
+ });
60
+
61
+ return sign(payload, opts.secret, {
62
+ algorithm: opts.algorithm || 'HS256',
63
+ expiresIn: ttl,
64
+ subject: String(sub),
65
+ audience: opts.audience || ('room:' + opts.room),
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Verify a join token and return its payload. Throws a `WebRTCError` with
71
+ * `code: 'INVALID_TOKEN'` on any failure - bad signature, expired, audience
72
+ * mismatch, malformed, etc.
73
+ *
74
+ * @param {string} token
75
+ * @param {object} opts
76
+ * @param {string|Buffer} opts.secret
77
+ * @param {string} [opts.room] - If supplied, audience must be `room:<room>`.
78
+ * @param {string|string[]} [opts.audience] - Explicit audience override.
79
+ * @param {string|string[]} [opts.algorithms=['HS256']]
80
+ * @param {number} [opts.clockTolerance=0]
81
+ * @returns {object} Verified payload.
82
+ *
83
+ * @section Signaling
84
+ */
85
+ function verifyJoinToken(token, opts = {})
86
+ {
87
+ if (!opts || typeof opts !== 'object')
88
+ throw new WebRTCError('verifyJoinToken: opts must be an object', { code: 'INVALID_TOKEN' });
89
+ if (!opts.secret)
90
+ throw new WebRTCError('verifyJoinToken: secret is required', { code: 'INVALID_TOKEN' });
91
+ if (typeof token !== 'string' || token.length === 0)
92
+ throw new WebRTCError('verifyJoinToken: token must be a non-empty string', { code: 'INVALID_TOKEN' });
93
+
94
+ const audience = opts.audience || (opts.room ? 'room:' + opts.room : undefined);
95
+ try
96
+ {
97
+ const { payload } = verify(token, opts.secret, {
98
+ algorithms: opts.algorithms || ['HS256'],
99
+ audience,
100
+ clockTolerance: opts.clockTolerance || 0,
101
+ });
102
+ if (opts.room && payload.room && payload.room !== opts.room)
103
+ throw new WebRTCError('verifyJoinToken: room claim mismatch', { code: 'INVALID_TOKEN' });
104
+ return payload;
105
+ }
106
+ catch (err)
107
+ {
108
+ if (err instanceof WebRTCError) throw err;
109
+ throw new WebRTCError(
110
+ 'verifyJoinToken: ' + (err && err.message ? err.message : 'invalid token'),
111
+ { code: 'INVALID_TOKEN', cause: err && err.code ? err.code : undefined },
112
+ );
113
+ }
114
+ }
115
+
116
+ module.exports = { signJoinToken, verifyJoinToken };
@@ -0,0 +1,229 @@
1
+ /**
2
+ * @module webrtc/observe
3
+ * @description Optional metrics + tracing wiring for a {@link SignalingHub}.
4
+ * Pass a `MetricsRegistry`, a `Tracer`, or both; the binder
5
+ * subscribes to the hub's lifecycle events and exports the six
6
+ * standard `zs_webrtc_*` Prometheus series plus per-operation
7
+ * OpenTelemetry-compatible spans.
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ // --- Helpers ---
13
+
14
+ /** Extract the first `a=ice-ufrag:` value from an SDP blob. */
15
+ function _extractUfrag(sdp)
16
+ {
17
+ const m = /^a=ice-ufrag:([^\r\n]+)/m.exec(sdp);
18
+ return m ? m[1].trim() : null;
19
+ }
20
+
21
+ // --- Metrics binder ---
22
+
23
+ function _registerMetrics(registry)
24
+ {
25
+ return {
26
+ peersActive: registry.gauge({
27
+ name: 'zs_webrtc_peers_active',
28
+ help: 'Number of WebRTC peers currently joined per room.',
29
+ labels: ['room'],
30
+ }),
31
+ roomsActive: registry.gauge({
32
+ name: 'zs_webrtc_rooms_active',
33
+ help: 'Number of WebRTC rooms with at least one peer.',
34
+ }),
35
+ signalingMessages: registry.counter({
36
+ name: 'zs_webrtc_signaling_messages_total',
37
+ help: 'WebRTC signaling messages by type, direction, and outcome.',
38
+ labels: ['type', 'direction', 'result'],
39
+ }),
40
+ offerDuration: registry.histogram({
41
+ name: 'zs_webrtc_offer_duration_ms',
42
+ help: 'End-to-end latency between an offer and its matching answer, in ms.',
43
+ labels: ['room'],
44
+ buckets: [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000],
45
+ }),
46
+ joinFailures: registry.counter({
47
+ name: 'zs_webrtc_join_failures_total',
48
+ help: 'WebRTC room joins rejected by the hub.',
49
+ labels: ['reason'],
50
+ }),
51
+ iceRestart: registry.counter({
52
+ name: 'zs_webrtc_ice_restart_total',
53
+ help: 'Detected ICE restarts (ufrag rotation) per room.',
54
+ labels: ['room'],
55
+ }),
56
+ };
57
+ }
58
+
59
+ function _bindMetrics(hub, m)
60
+ {
61
+ /** Outstanding offer timestamps: `offererPeerId` -> ms epoch. */
62
+ const offerStart = new Map();
63
+ /** Last seen ufrag per peer.id (for ICE-restart detection). */
64
+ const lastUfrag = new Map();
65
+
66
+ hub.on('signal', ({ peer, type }) =>
67
+ {
68
+ m.signalingMessages.inc({ type, direction: 'in', result: 'ok' });
69
+ });
70
+
71
+ hub.on('join', ({ peer, room }) =>
72
+ {
73
+ const before = m.peersActive.get({ room: room.name });
74
+ m.peersActive.inc({ room: room.name });
75
+ // Room newly non-empty?
76
+ if (before === 0) m.roomsActive.inc();
77
+ });
78
+
79
+ hub.on('leave', ({ peer, room }) =>
80
+ {
81
+ m.peersActive.dec({ room: room.name });
82
+ if (m.peersActive.get({ room: room.name }) <= 0)
83
+ m.roomsActive.dec();
84
+ });
85
+
86
+ hub.on('joinFailed', ({ reason }) =>
87
+ {
88
+ m.joinFailures.inc({ reason: reason || 'UNKNOWN' });
89
+ });
90
+
91
+ hub.on('offer', ({ peer, sdp, room }) =>
92
+ {
93
+ offerStart.set(peer.id, Date.now());
94
+
95
+ // ICE-restart detection: compare ufrag against last-known for this peer.
96
+ const ufrag = _extractUfrag(sdp);
97
+ if (ufrag)
98
+ {
99
+ const prev = lastUfrag.get(peer.id);
100
+ if (prev && prev !== ufrag)
101
+ m.iceRestart.inc({ room: room.name });
102
+ lastUfrag.set(peer.id, ufrag);
103
+ }
104
+ });
105
+
106
+ hub.on('answer', ({ peer, target, room }) =>
107
+ {
108
+ // The offer was sent by the original target (now answering back to it).
109
+ const startedAt = offerStart.get(target.id);
110
+ if (startedAt !== undefined)
111
+ {
112
+ m.offerDuration.observe({ room: room.name }, Date.now() - startedAt);
113
+ offerStart.delete(target.id);
114
+ }
115
+ });
116
+ }
117
+
118
+ // --- Tracing binder ---
119
+
120
+ function _bindTracing(hub, tracer)
121
+ {
122
+ hub.on('signal', ({ peer, type }) =>
123
+ {
124
+ const span = tracer.startSpan('webrtc.signal', {
125
+ kind: 'server',
126
+ attributes: { 'peer.id': peer.id, 'rtc.type': type },
127
+ });
128
+ span.setOk();
129
+ span.end();
130
+ });
131
+
132
+ hub.on('join', ({ peer, room }) =>
133
+ {
134
+ const span = tracer.startSpan('webrtc.join', {
135
+ kind: 'server',
136
+ attributes: { 'peer.id': peer.id, 'room.id': room.name },
137
+ });
138
+ span.setOk();
139
+ span.end();
140
+ });
141
+
142
+ hub.on('joinFailed', ({ peer, reason, room }) =>
143
+ {
144
+ const span = tracer.startSpan('webrtc.join', {
145
+ kind: 'server',
146
+ attributes: { 'peer.id': peer.id, 'room.id': room || '', 'rtc.error': reason },
147
+ });
148
+ span.setError(reason);
149
+ span.end();
150
+ });
151
+
152
+ hub.on('offer', ({ peer, target, room }) =>
153
+ {
154
+ const span = tracer.startSpan('webrtc.publish', {
155
+ kind: 'producer',
156
+ attributes: { 'peer.id': peer.id, 'room.id': room.name, 'rtc.target': target.id },
157
+ });
158
+ span.setOk();
159
+ span.end();
160
+ });
161
+
162
+ hub.on('publishFailed', ({ peer, reason, room }) =>
163
+ {
164
+ const span = tracer.startSpan('webrtc.publish', {
165
+ kind: 'producer',
166
+ attributes: { 'peer.id': peer.id, 'room.id': room || '', 'rtc.error': reason },
167
+ });
168
+ span.setError(reason);
169
+ span.end();
170
+ });
171
+
172
+ hub.on('answer', ({ peer, target, room }) =>
173
+ {
174
+ const span = tracer.startSpan('webrtc.subscribe', {
175
+ kind: 'consumer',
176
+ attributes: { 'peer.id': peer.id, 'room.id': room.name, 'rtc.target': target.id },
177
+ });
178
+ span.setOk();
179
+ span.end();
180
+ });
181
+
182
+ hub.on('subscribeFailed', ({ peer, reason, room }) =>
183
+ {
184
+ const span = tracer.startSpan('webrtc.subscribe', {
185
+ kind: 'consumer',
186
+ attributes: { 'peer.id': peer.id, 'room.id': room || '', 'rtc.error': reason },
187
+ });
188
+ span.setError(reason);
189
+ span.end();
190
+ });
191
+ }
192
+
193
+ // --- Public API ---
194
+
195
+ /**
196
+ * Wire a {@link SignalingHub} to a metrics registry and / or a tracer.
197
+ *
198
+ * Registers six standard Prometheus series under the `zs_webrtc_` prefix:
199
+ *
200
+ * - `zs_webrtc_peers_active{room}` (gauge)
201
+ * - `zs_webrtc_rooms_active` (gauge)
202
+ * - `zs_webrtc_signaling_messages_total{type,direction,result}` (counter)
203
+ * - `zs_webrtc_offer_duration_ms{room}` (histogram)
204
+ * - `zs_webrtc_join_failures_total{reason}` (counter)
205
+ * - `zs_webrtc_ice_restart_total{room}` (counter)
206
+ *
207
+ * Emits spans named `webrtc.join`, `webrtc.signal`, `webrtc.publish`, and
208
+ * `webrtc.subscribe`, each annotated with `peer.id` and `room.id`.
209
+ *
210
+ * @section Observability
211
+ *
212
+ * @param {SignalingHub} hub - The hub to instrument.
213
+ * @param {object} opts
214
+ * @param {MetricsRegistry} [opts.metrics] - Prometheus-compatible registry.
215
+ * @param {Tracer} [opts.tracer] - Tracer for span emission.
216
+ * @returns {SignalingHub} The same hub, for chaining.
217
+ *
218
+ * @example | Plug into an existing app
219
+ * const hub = new SignalingHub();
220
+ * bindObservability(hub, { metrics: app.metrics() });
221
+ */
222
+ function bindObservability(hub, opts = {})
223
+ {
224
+ if (opts.metrics) _bindMetrics(hub, _registerMetrics(opts.metrics));
225
+ if (opts.tracer) _bindTracing(hub, opts.tracer);
226
+ return hub;
227
+ }
228
+
229
+ module.exports = { bindObservability };
@@ -0,0 +1,116 @@
1
+ /**
2
+ * @module webrtc/peer
3
+ * @description Per-connection state machine for a WebRTC signaling peer.
4
+ *
5
+ * A `Peer` wraps a transport object (typically a `WebSocketConnection`
6
+ * but any duck-typed `{ send, on, close }` works - see the unit tests
7
+ * for a minimal in-memory transport) and exposes the JSEP message
8
+ * types as ordinary events. The hub is responsible for routing.
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ let _peerCounter = 0;
14
+
15
+ /**
16
+ * JSEP signaling state strings, matching RFC 8829 §4.1.
17
+ * @enum {string}
18
+ */
19
+ const PEER_STATE = Object.freeze({
20
+ STABLE: 'stable',
21
+ HAVE_LOCAL_OFFER: 'have-local-offer',
22
+ HAVE_REMOTE_OFFER: 'have-remote-offer',
23
+ });
24
+
25
+ /**
26
+ * One signaling peer attached to a `SignalingHub` via some transport.
27
+ *
28
+ * @class
29
+ * @section Peers
30
+ *
31
+ * @example
32
+ * hub.on('join', ({ peer, room }) => {
33
+ * peer.send('welcome', { roomSize: room.size });
34
+ * peer.on('mute', ev => audit('mute', peer.id, ev.kind));
35
+ * });
36
+ */
37
+ class Peer
38
+ {
39
+ /**
40
+ * @constructor
41
+ * @param {object} transport - Anything with `send(string)`, `on('message'|'close', cb)`, `close(code?, reason?)`.
42
+ * @param {object} [info] - Connection metadata.
43
+ * @param {*} [info.user] - Authenticated user object (if any).
44
+ * @param {string} [info.ip] - Remote IP for audit / rate limits.
45
+ */
46
+ constructor(transport, info = {})
47
+ {
48
+ /** @type {string} Globally-unique peer id within a hub. */
49
+ this.id = 'peer_' + (++_peerCounter) + '_' + Date.now().toString(36);
50
+
51
+ /** @type {*} Authenticated user object (if any). */
52
+ this.user = info.user || null;
53
+
54
+ /** @type {string|null} Remote IP for audit / rate limits. */
55
+ this.ip = info.ip || null;
56
+
57
+ /** @type {object} Underlying transport (WS connection or mock). */
58
+ this.transport = transport;
59
+
60
+ /** @type {string} Current JSEP state. */
61
+ this.state = PEER_STATE.STABLE;
62
+
63
+ /** @type {import('./room')|null} Current room membership. */
64
+ this.room = null;
65
+
66
+ /** @type {number} Count of malformed frames received - rate-limit material. */
67
+ this.errors = 0;
68
+
69
+ /** @type {number} ms timestamp the peer was created. */
70
+ this.connectedAt = Date.now();
71
+
72
+ /** @type {boolean} */
73
+ this.closed = false;
74
+ }
75
+
76
+ /**
77
+ * Send a JSON envelope to this peer. `type` is added to `payload` and
78
+ * the result is serialised once. Silently drops sends after close.
79
+ * @param {string} type
80
+ * @param {object} [payload]
81
+ */
82
+ send(type, payload)
83
+ {
84
+ if (this.closed) return;
85
+ const frame = Object.assign({ type }, payload || {});
86
+ try { this.transport.send(JSON.stringify(frame)); }
87
+ catch { /* transport may already be closed; ignore */ }
88
+ }
89
+
90
+ /**
91
+ * Send a typed error frame. Callers SHOULD use this rather than throwing,
92
+ * because exceptions escape the message handler and tear down the process.
93
+ * @param {string} code
94
+ * @param {string} message
95
+ */
96
+ sendError(code, message)
97
+ {
98
+ this.send('error', { code, message });
99
+ }
100
+
101
+ /**
102
+ * Close the underlying transport with a WebSocket-style code and reason.
103
+ * Defaults to 1000 (normal closure).
104
+ * @param {number} [code=1000]
105
+ * @param {string} [reason='']
106
+ */
107
+ close(code = 1000, reason = '')
108
+ {
109
+ if (this.closed) return;
110
+ this.closed = true;
111
+ try { this.transport.close(code, reason); }
112
+ catch { /* already closed */ }
113
+ }
114
+ }
115
+
116
+ module.exports = { Peer, PEER_STATE };