zero-query 1.1.1 → 1.2.0
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/LICENSE +21 -21
- package/README.md +2 -0
- package/cli/args.js +33 -33
- package/cli/commands/build-api.js +443 -442
- package/cli/commands/build.js +254 -247
- package/cli/commands/bundle.js +1228 -1224
- package/cli/commands/create.js +137 -121
- package/cli/commands/dev/devtools/index.js +56 -56
- package/cli/commands/dev/devtools/js/components.js +49 -49
- package/cli/commands/dev/devtools/js/core.js +423 -423
- package/cli/commands/dev/devtools/js/elements.js +421 -421
- package/cli/commands/dev/devtools/js/network.js +166 -166
- package/cli/commands/dev/devtools/js/performance.js +73 -73
- package/cli/commands/dev/devtools/js/router.js +105 -105
- package/cli/commands/dev/devtools/js/source.js +132 -132
- package/cli/commands/dev/devtools/js/stats.js +35 -35
- package/cli/commands/dev/devtools/js/tabs.js +79 -79
- package/cli/commands/dev/devtools/panel.html +95 -95
- package/cli/commands/dev/devtools/styles.css +244 -244
- package/cli/commands/dev/index.js +107 -107
- package/cli/commands/dev/logger.js +75 -75
- package/cli/commands/dev/overlay.js +858 -858
- package/cli/commands/dev/server.js +220 -220
- package/cli/commands/dev/validator.js +94 -94
- package/cli/commands/dev/watcher.js +172 -172
- package/cli/help.js +114 -112
- package/cli/index.js +52 -52
- package/cli/scaffold/default/LICENSE +21 -21
- package/cli/scaffold/default/app/app.js +207 -207
- package/cli/scaffold/default/app/components/about.js +201 -201
- package/cli/scaffold/default/app/components/api-demo.js +143 -143
- package/cli/scaffold/default/app/components/contact-card.js +231 -231
- package/cli/scaffold/default/app/components/contacts/contacts.css +706 -706
- package/cli/scaffold/default/app/components/contacts/contacts.html +200 -200
- package/cli/scaffold/default/app/components/contacts/contacts.js +196 -196
- package/cli/scaffold/default/app/components/counter.js +127 -127
- package/cli/scaffold/default/app/components/home.js +249 -249
- package/cli/scaffold/default/app/components/not-found.js +16 -16
- package/cli/scaffold/default/app/components/playground/playground.css +115 -115
- package/cli/scaffold/default/app/components/playground/playground.html +161 -161
- package/cli/scaffold/default/app/components/playground/playground.js +116 -116
- package/cli/scaffold/default/app/components/todos.js +225 -225
- package/cli/scaffold/default/app/components/toolkit/toolkit.css +97 -97
- package/cli/scaffold/default/app/components/toolkit/toolkit.html +146 -146
- package/cli/scaffold/default/app/components/toolkit/toolkit.js +280 -280
- package/cli/scaffold/default/app/routes.js +15 -15
- package/cli/scaffold/default/app/store.js +101 -101
- package/cli/scaffold/default/global.css +552 -552
- package/cli/scaffold/default/index.html +99 -99
- package/cli/scaffold/minimal/app/app.js +85 -85
- package/cli/scaffold/minimal/app/components/about.js +68 -68
- package/cli/scaffold/minimal/app/components/counter.js +122 -122
- package/cli/scaffold/minimal/app/components/home.js +68 -68
- package/cli/scaffold/minimal/app/components/not-found.js +16 -16
- package/cli/scaffold/minimal/app/routes.js +9 -9
- package/cli/scaffold/minimal/app/store.js +36 -36
- package/cli/scaffold/minimal/global.css +300 -300
- package/cli/scaffold/minimal/index.html +44 -44
- package/cli/scaffold/ssr/app/app.js +41 -41
- package/cli/scaffold/ssr/app/components/about.js +55 -55
- package/cli/scaffold/ssr/app/components/blog/index.js +65 -65
- package/cli/scaffold/ssr/app/components/blog/post.js +86 -86
- package/cli/scaffold/ssr/app/components/home.js +37 -37
- package/cli/scaffold/ssr/app/components/not-found.js +15 -15
- package/cli/scaffold/ssr/app/routes.js +8 -8
- package/cli/scaffold/ssr/global.css +228 -228
- package/cli/scaffold/ssr/index.html +37 -37
- package/cli/scaffold/ssr/package.json +8 -8
- package/cli/scaffold/ssr/server/data/posts.js +144 -144
- package/cli/scaffold/ssr/server/index.js +213 -213
- package/cli/scaffold/webrtc/app/app.js +11 -0
- package/cli/scaffold/webrtc/app/components/video-room.js +295 -0
- package/cli/scaffold/webrtc/app/lib/room.js +252 -0
- package/cli/scaffold/webrtc/assets/.gitkeep +0 -0
- package/cli/scaffold/webrtc/global.css +250 -0
- package/cli/scaffold/webrtc/index.html +21 -0
- package/cli/utils.js +305 -287
- package/dist/API.md +661 -0
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +10313 -6614
- package/dist/zquery.min.js +8 -631
- package/index.d.ts +570 -371
- package/index.js +311 -240
- package/package.json +76 -70
- package/src/component.js +1709 -1691
- package/src/core.js +921 -921
- package/src/diff.js +497 -497
- package/src/errors.js +209 -209
- package/src/expression.js +922 -922
- package/src/http.js +242 -242
- package/src/package.json +1 -1
- package/src/reactive.js +255 -255
- package/src/router.js +843 -843
- package/src/ssr.js +418 -418
- package/src/store.js +318 -318
- package/src/utils.js +515 -515
- package/src/webrtc/e2ee.js +351 -0
- package/src/webrtc/errors.js +116 -0
- package/src/webrtc/ice.js +301 -0
- package/src/webrtc/index.js +131 -0
- package/src/webrtc/joinToken.js +119 -0
- package/src/webrtc/observe.js +172 -0
- package/src/webrtc/peer.js +351 -0
- package/src/webrtc/reactive.js +268 -0
- package/src/webrtc/room.js +625 -0
- package/src/webrtc/sdp.js +302 -0
- package/src/webrtc/sfu/index.js +43 -0
- package/src/webrtc/sfu/livekit.js +131 -0
- package/src/webrtc/sfu/mediasoup.js +150 -0
- package/src/webrtc/signaling.js +373 -0
- package/src/webrtc/turn.js +237 -0
- package/tests/_helpers/webrtcFakes.js +289 -0
- package/tests/audit.test.js +4158 -4158
- package/tests/cli.test.js +1136 -1103
- package/tests/compare.test.js +497 -486
- package/tests/component.test.js +3969 -3938
- package/tests/core.test.js +1910 -1910
- package/tests/dev-server.test.js +489 -489
- package/tests/diff.test.js +1416 -1416
- package/tests/docs.test.js +1664 -1650
- package/tests/electron-features.test.js +864 -864
- package/tests/errors.test.js +619 -619
- package/tests/expression.test.js +1056 -1056
- package/tests/http.test.js +648 -648
- package/tests/reactive.test.js +819 -819
- package/tests/router.test.js +2327 -2327
- package/tests/ssr.test.js +870 -870
- package/tests/store.test.js +830 -830
- package/tests/test-minifier.js +153 -153
- package/tests/test-ssr.js +27 -27
- package/tests/utils.test.js +1377 -1377
- package/tests/webrtc/e2ee.test.js +283 -0
- package/tests/webrtc/ice.test.js +202 -0
- package/tests/webrtc/joinToken.test.js +89 -0
- package/tests/webrtc/observe.test.js +111 -0
- package/tests/webrtc/peer.test.js +373 -0
- package/tests/webrtc/reactive.test.js +235 -0
- package/tests/webrtc/room.test.js +406 -0
- package/tests/webrtc/sdp.test.js +151 -0
- package/tests/webrtc/sfu-livekit.test.js +119 -0
- package/tests/webrtc/sfu.test.js +160 -0
- package/tests/webrtc/signaling.test.js +251 -0
- package/tests/webrtc/turn.test.js +256 -0
- package/types/collection.d.ts +383 -383
- package/types/component.d.ts +186 -186
- package/types/errors.d.ts +135 -135
- package/types/http.d.ts +92 -92
- package/types/misc.d.ts +201 -201
- package/types/reactive.d.ts +98 -98
- package/types/router.d.ts +190 -190
- package/types/ssr.d.ts +102 -102
- package/types/store.d.ts +146 -146
- package/types/utils.d.ts +245 -245
- package/types/webrtc.d.ts +653 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/webrtc/e2ee.js - SFrame-style end-to-end encryption
|
|
3
|
+
*
|
|
4
|
+
* Provides a small AES-GCM SFrame implementation suitable for
|
|
5
|
+
* `RTCRtpScriptTransform` / Encoded Transforms wiring. Frames are wrapped
|
|
6
|
+
* as `[1-byte epoch][12-byte IV][N-byte ciphertext+tag]` so receivers can
|
|
7
|
+
* route a frame to the correct key without an out-of-band signal.
|
|
8
|
+
*
|
|
9
|
+
* Key derivation: PBKDF2(passphrase, salt) -> HKDF -> AES-GCM-128. The
|
|
10
|
+
* salt is intended to be a room id so two clients of the same room with
|
|
11
|
+
* the same passphrase derive the same key.
|
|
12
|
+
*
|
|
13
|
+
* The actual `RTCRtpScriptTransform` wiring lives in `attachE2ee()`; the
|
|
14
|
+
* core encryptFrame / decryptFrame helpers are pure and run anywhere
|
|
15
|
+
* WebCrypto is available (browsers, jsdom, Node 18+).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { E2eeError } from './errors.js';
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
const AES_GCM_KEY_BITS = 128;
|
|
22
|
+
const IV_BYTES = 12;
|
|
23
|
+
const HEADER_BYTES = 1 + IV_BYTES; // 1-byte epoch + 12-byte IV
|
|
24
|
+
const PBKDF2_ITERATIONS = 100_000;
|
|
25
|
+
const HKDF_INFO = new TextEncoder().encode('zquery-sframe-v1');
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @returns {SubtleCrypto}
|
|
30
|
+
*/
|
|
31
|
+
function _subtle() {
|
|
32
|
+
const subtle = typeof crypto !== 'undefined' && crypto.subtle ? crypto.subtle : null;
|
|
33
|
+
if (!subtle) {
|
|
34
|
+
throw new E2eeError('WebCrypto SubtleCrypto is not available in this environment', {
|
|
35
|
+
code: 'ZQ_WEBRTC_E2EE_NO_WEBCRYPTO',
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return subtle;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function _randomBytes(n) {
|
|
42
|
+
const buf = new Uint8Array(n);
|
|
43
|
+
if (typeof crypto === 'undefined' || typeof crypto.getRandomValues !== 'function') {
|
|
44
|
+
throw new E2eeError('crypto.getRandomValues is not available in this environment', {
|
|
45
|
+
code: 'ZQ_WEBRTC_E2EE_NO_RANDOM',
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
crypto.getRandomValues(buf);
|
|
49
|
+
return buf;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function _asUint8(input) {
|
|
53
|
+
if (input instanceof Uint8Array) return input;
|
|
54
|
+
if (input instanceof ArrayBuffer) return new Uint8Array(input);
|
|
55
|
+
if (ArrayBuffer.isView(input)) return new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
|
|
56
|
+
// Handle cross-realm ArrayBuffer (jsdom / VM contexts).
|
|
57
|
+
if (input && typeof input === 'object' && typeof input.byteLength === 'number'
|
|
58
|
+
&& Object.prototype.toString.call(input) === '[object ArrayBuffer]') {
|
|
59
|
+
return new Uint8Array(input);
|
|
60
|
+
}
|
|
61
|
+
throw new E2eeError('expected a BufferSource (Uint8Array | ArrayBuffer | typed array)', {
|
|
62
|
+
code: 'ZQ_WEBRTC_E2EE_BAD_INPUT',
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Derive an AES-GCM-128 SFrame key from a passphrase + salt (typically
|
|
69
|
+
* the room id). Two clients calling this with the same inputs produce
|
|
70
|
+
* the same key.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} passphrase
|
|
73
|
+
* @param {string} salt
|
|
74
|
+
* @returns {Promise<CryptoKey>}
|
|
75
|
+
*/
|
|
76
|
+
export async function deriveSFrameKey(passphrase, salt) {
|
|
77
|
+
if (typeof passphrase !== 'string' || !passphrase) {
|
|
78
|
+
throw new E2eeError('deriveSFrameKey: passphrase must be a non-empty string', {
|
|
79
|
+
code: 'ZQ_WEBRTC_E2EE_BAD_PASSPHRASE',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (typeof salt !== 'string' || !salt) {
|
|
83
|
+
throw new E2eeError('deriveSFrameKey: salt must be a non-empty string', {
|
|
84
|
+
code: 'ZQ_WEBRTC_E2EE_BAD_SALT',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const subtle = _subtle();
|
|
88
|
+
const enc = new TextEncoder();
|
|
89
|
+
const baseKey = await subtle.importKey(
|
|
90
|
+
'raw', enc.encode(passphrase), { name: 'PBKDF2' }, false, ['deriveBits']
|
|
91
|
+
);
|
|
92
|
+
const pbkdfBits = await subtle.deriveBits(
|
|
93
|
+
{ name: 'PBKDF2', hash: 'SHA-256', salt: enc.encode(salt), iterations: PBKDF2_ITERATIONS },
|
|
94
|
+
baseKey,
|
|
95
|
+
256
|
|
96
|
+
);
|
|
97
|
+
const hkdfKey = await subtle.importKey(
|
|
98
|
+
'raw', pbkdfBits, { name: 'HKDF' }, false, ['deriveKey']
|
|
99
|
+
);
|
|
100
|
+
return subtle.deriveKey(
|
|
101
|
+
{ name: 'HKDF', hash: 'SHA-256', salt: enc.encode(salt), info: HKDF_INFO },
|
|
102
|
+
hkdfKey,
|
|
103
|
+
{ name: 'AES-GCM', length: AES_GCM_KEY_BITS },
|
|
104
|
+
false,
|
|
105
|
+
['encrypt', 'decrypt']
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Generate a fresh random AES-GCM-128 SFrame key.
|
|
112
|
+
*
|
|
113
|
+
* @returns {Promise<CryptoKey>}
|
|
114
|
+
*/
|
|
115
|
+
export async function generateSFrameKey() {
|
|
116
|
+
return _subtle().generateKey(
|
|
117
|
+
{ name: 'AES-GCM', length: AES_GCM_KEY_BITS },
|
|
118
|
+
true,
|
|
119
|
+
['encrypt', 'decrypt']
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Holds the current key + epoch for an SFrame transform pair. Receivers
|
|
126
|
+
* keep a sliding window of accepted epochs (old keys retained briefly so
|
|
127
|
+
* in-flight frames decode after a rotation; oldest evicted on each
|
|
128
|
+
* `setKey`).
|
|
129
|
+
*/
|
|
130
|
+
export class SFrameContext {
|
|
131
|
+
/**
|
|
132
|
+
* @param {{maxEpochs?: number}} [opts]
|
|
133
|
+
*/
|
|
134
|
+
constructor(opts) {
|
|
135
|
+
const max = opts && Number.isFinite(opts.maxEpochs) ? opts.maxEpochs : 4;
|
|
136
|
+
/** @private */ this._keys = new Map(); // epoch -> CryptoKey
|
|
137
|
+
/** @private */ this._maxEpochs = Math.max(1, max);
|
|
138
|
+
/** @public */ this.currentEpoch = 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Install `key` for `epoch` and mark it as the encrypt key. Evicts the
|
|
143
|
+
* oldest epoch when more than `maxEpochs` are tracked.
|
|
144
|
+
*
|
|
145
|
+
* @param {number} epoch
|
|
146
|
+
* @param {CryptoKey} key
|
|
147
|
+
*/
|
|
148
|
+
setKey(epoch, key) {
|
|
149
|
+
if (!Number.isInteger(epoch) || epoch < 0 || epoch > 255) {
|
|
150
|
+
throw new E2eeError('SFrameContext.setKey: epoch must be an integer in [0, 255]', {
|
|
151
|
+
code: 'ZQ_WEBRTC_E2EE_BAD_EPOCH',
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (!key) {
|
|
155
|
+
throw new E2eeError('SFrameContext.setKey: key required', {
|
|
156
|
+
code: 'ZQ_WEBRTC_E2EE_BAD_KEY',
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
this._keys.set(epoch, key);
|
|
160
|
+
this.currentEpoch = epoch;
|
|
161
|
+
while (this._keys.size > this._maxEpochs) {
|
|
162
|
+
const oldest = this._keys.keys().next().value;
|
|
163
|
+
this._keys.delete(oldest);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Drop a previously installed epoch (e.g. forward-secret evict on peer-leave). */
|
|
168
|
+
removeEpoch(epoch) {
|
|
169
|
+
this._keys.delete(epoch);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Return the key for `epoch`, or `null` if unknown / evicted. */
|
|
173
|
+
getKey(epoch) {
|
|
174
|
+
return this._keys.get(epoch) || null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Number of epochs currently tracked. */
|
|
178
|
+
get epochCount() {
|
|
179
|
+
return this._keys.size;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Encrypt one frame using the context's current epoch key.
|
|
186
|
+
*
|
|
187
|
+
* Output layout: `[1-byte epoch][12-byte IV][ciphertext + 16-byte tag]`.
|
|
188
|
+
*
|
|
189
|
+
* @param {SFrameContext} ctx
|
|
190
|
+
* @param {BufferSource} payload
|
|
191
|
+
* @returns {Promise<Uint8Array>}
|
|
192
|
+
*/
|
|
193
|
+
export async function encryptFrame(ctx, payload) {
|
|
194
|
+
if (!(ctx instanceof SFrameContext)) {
|
|
195
|
+
throw new E2eeError('encryptFrame: ctx must be an SFrameContext', {
|
|
196
|
+
code: 'ZQ_WEBRTC_E2EE_BAD_CTX',
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
const key = ctx.getKey(ctx.currentEpoch);
|
|
200
|
+
if (!key) {
|
|
201
|
+
throw new E2eeError(`encryptFrame: no key installed for epoch ${ctx.currentEpoch}`, {
|
|
202
|
+
code: 'ZQ_WEBRTC_E2EE_NO_KEY',
|
|
203
|
+
context: { epoch: ctx.currentEpoch },
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
const plain = _asUint8(payload);
|
|
207
|
+
const iv = _randomBytes(IV_BYTES);
|
|
208
|
+
const cipher = new Uint8Array(await _subtle().encrypt({ name: 'AES-GCM', iv }, key, plain));
|
|
209
|
+
const out = new Uint8Array(HEADER_BYTES + cipher.byteLength);
|
|
210
|
+
out[0] = ctx.currentEpoch & 0xff;
|
|
211
|
+
out.set(iv, 1);
|
|
212
|
+
out.set(cipher, HEADER_BYTES);
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Decrypt one frame produced by `encryptFrame`. Returns the plaintext as a
|
|
219
|
+
* `Uint8Array`. Throws `E2eeError` if the epoch is unknown or AES-GCM
|
|
220
|
+
* authentication fails.
|
|
221
|
+
*
|
|
222
|
+
* @param {SFrameContext} ctx
|
|
223
|
+
* @param {BufferSource} frame
|
|
224
|
+
* @returns {Promise<Uint8Array>}
|
|
225
|
+
*/
|
|
226
|
+
export async function decryptFrame(ctx, frame) {
|
|
227
|
+
if (!(ctx instanceof SFrameContext)) {
|
|
228
|
+
throw new E2eeError('decryptFrame: ctx must be an SFrameContext', {
|
|
229
|
+
code: 'ZQ_WEBRTC_E2EE_BAD_CTX',
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
const bytes = _asUint8(frame);
|
|
233
|
+
if (bytes.byteLength <= HEADER_BYTES) {
|
|
234
|
+
throw new E2eeError('decryptFrame: frame too short for SFrame header', {
|
|
235
|
+
code: 'ZQ_WEBRTC_E2EE_SHORT_FRAME',
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
const epoch = bytes[0];
|
|
239
|
+
const key = ctx.getKey(epoch);
|
|
240
|
+
if (!key) {
|
|
241
|
+
throw new E2eeError(`decryptFrame: no key for epoch ${epoch}`, {
|
|
242
|
+
code: 'ZQ_WEBRTC_E2EE_UNKNOWN_EPOCH',
|
|
243
|
+
context: { epoch },
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
const iv = bytes.subarray(1, HEADER_BYTES);
|
|
247
|
+
const cipher = bytes.subarray(HEADER_BYTES);
|
|
248
|
+
let plain;
|
|
249
|
+
try {
|
|
250
|
+
plain = new Uint8Array(await _subtle().decrypt({ name: 'AES-GCM', iv }, key, cipher));
|
|
251
|
+
} catch (err) {
|
|
252
|
+
throw new E2eeError('decryptFrame: AES-GCM authentication failed', {
|
|
253
|
+
code: 'ZQ_WEBRTC_E2EE_AUTH_FAILED',
|
|
254
|
+
cause: err instanceof Error ? err : undefined,
|
|
255
|
+
context: { epoch },
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
return plain;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Attach SFrame encrypt/decrypt transforms to every existing and future
|
|
264
|
+
* RTP sender/receiver on `pc`. Uses `RTCRtpScriptTransform` where
|
|
265
|
+
* available, falls back to the legacy `createEncodedStreams()` API on
|
|
266
|
+
* older engines.
|
|
267
|
+
*
|
|
268
|
+
* @param {RTCPeerConnection} pc
|
|
269
|
+
* @param {SFrameContext} ctx
|
|
270
|
+
* @returns {{refresh(): void, detach(): void}}
|
|
271
|
+
*/
|
|
272
|
+
export function attachE2ee(pc, ctx) {
|
|
273
|
+
if (!pc || typeof pc.getSenders !== 'function' || typeof pc.getReceivers !== 'function') {
|
|
274
|
+
throw new E2eeError('attachE2ee: pc must look like an RTCPeerConnection', {
|
|
275
|
+
code: 'ZQ_WEBRTC_E2EE_BAD_PC',
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
if (!(ctx instanceof SFrameContext)) {
|
|
279
|
+
throw new E2eeError('attachE2ee: ctx must be an SFrameContext', {
|
|
280
|
+
code: 'ZQ_WEBRTC_E2EE_BAD_CTX',
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const wired = new WeakSet();
|
|
285
|
+
let detached = false;
|
|
286
|
+
|
|
287
|
+
function wireSender(sender) {
|
|
288
|
+
if (detached || wired.has(sender)) return;
|
|
289
|
+
wired.add(sender);
|
|
290
|
+
const stream = _maybeEncodedStreams(sender);
|
|
291
|
+
if (!stream) return;
|
|
292
|
+
const transformer = new TransformStream({
|
|
293
|
+
async transform(chunk, controller) {
|
|
294
|
+
try {
|
|
295
|
+
const payload = _asUint8(chunk.data);
|
|
296
|
+
const out = await encryptFrame(ctx, payload);
|
|
297
|
+
chunk.data = out.buffer;
|
|
298
|
+
controller.enqueue(chunk);
|
|
299
|
+
} catch (_) {
|
|
300
|
+
// drop frame on encrypt failure (no key yet, etc.)
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
stream.readable.pipeThrough(transformer).pipeTo(stream.writable).catch(() => {});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function wireReceiver(receiver) {
|
|
308
|
+
if (detached || wired.has(receiver)) return;
|
|
309
|
+
wired.add(receiver);
|
|
310
|
+
const stream = _maybeEncodedStreams(receiver);
|
|
311
|
+
if (!stream) return;
|
|
312
|
+
const transformer = new TransformStream({
|
|
313
|
+
async transform(chunk, controller) {
|
|
314
|
+
try {
|
|
315
|
+
const payload = _asUint8(chunk.data);
|
|
316
|
+
const out = await decryptFrame(ctx, payload);
|
|
317
|
+
chunk.data = out.buffer;
|
|
318
|
+
controller.enqueue(chunk);
|
|
319
|
+
} catch (_) {
|
|
320
|
+
// drop undecryptable frame
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
stream.readable.pipeThrough(transformer).pipeTo(stream.writable).catch(() => {});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function refresh() {
|
|
328
|
+
if (detached) return;
|
|
329
|
+
for (const s of pc.getSenders()) wireSender(s);
|
|
330
|
+
for (const r of pc.getReceivers()) wireReceiver(r);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
refresh();
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
refresh,
|
|
337
|
+
detach() { detached = true; },
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
function _maybeEncodedStreams(senderOrReceiver) {
|
|
343
|
+
if (typeof senderOrReceiver.createEncodedStreams === 'function') {
|
|
344
|
+
try {
|
|
345
|
+
return senderOrReceiver.createEncodedStreams();
|
|
346
|
+
} catch (_) {
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/webrtc/errors.js - WebRTC error family
|
|
3
|
+
*
|
|
4
|
+
* All WebRTC-specific errors derive from `WebRtcError`, which itself
|
|
5
|
+
* derives from `ZQueryError` so they participate in the same
|
|
6
|
+
* `$.onError(handler)` reporting pipeline as the rest of the library.
|
|
7
|
+
*
|
|
8
|
+
* Each subclass has a sensible default `code` string; callers may override
|
|
9
|
+
* via the constructor's options bag (`{ code, context, cause }`). The codes
|
|
10
|
+
* intentionally mirror the families used by the matching `@zero-server/webrtc`
|
|
11
|
+
* package so cross-stack error reporting stays consistent.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { ZQueryError } from '../errors.js';
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Base class for every WebRTC client error. Extends `ZQueryError` so it
|
|
19
|
+
* shows up in `$.onError(handler)` like any other library error.
|
|
20
|
+
*
|
|
21
|
+
* throw new WebRtcError('peer connection failed');
|
|
22
|
+
* throw new WebRtcError('peer connection failed', { code: 'PC_FAILED', context: { peerId } });
|
|
23
|
+
*/
|
|
24
|
+
export class WebRtcError extends ZQueryError {
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} message - human-readable description.
|
|
27
|
+
* @param {object} [options]
|
|
28
|
+
* @param {string} [options.code] - stable error code (defaults per subclass).
|
|
29
|
+
* @param {object} [options.context] - extra structured context.
|
|
30
|
+
* @param {Error} [options.cause] - original error, if any.
|
|
31
|
+
*/
|
|
32
|
+
constructor(message, options = {}) {
|
|
33
|
+
const code = options.code || 'ZQ_WEBRTC';
|
|
34
|
+
const context = options.context || {};
|
|
35
|
+
super(code, message, context, options.cause);
|
|
36
|
+
this.name = 'WebRtcError';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
/** Signaling-channel error (WebSocket transport, protocol framing, etc.). */
|
|
42
|
+
export class SignalingError extends WebRtcError {
|
|
43
|
+
/**
|
|
44
|
+
* @param {string} message
|
|
45
|
+
* @param {object} [options] - same shape as `WebRtcError`.
|
|
46
|
+
*/
|
|
47
|
+
constructor(message, options = {}) {
|
|
48
|
+
super(message, { code: options.code || 'ZQ_WEBRTC_SIGNALING', context: options.context, cause: options.cause });
|
|
49
|
+
this.name = 'SignalingError';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
/** ICE candidate / gathering / connectivity error. */
|
|
55
|
+
export class IceError extends WebRtcError {
|
|
56
|
+
/**
|
|
57
|
+
* @param {string} message
|
|
58
|
+
* @param {object} [options]
|
|
59
|
+
*/
|
|
60
|
+
constructor(message, options = {}) {
|
|
61
|
+
super(message, { code: options.code || 'ZQ_WEBRTC_ICE', context: options.context, cause: options.cause });
|
|
62
|
+
this.name = 'IceError';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
/** SDP parse / validate / mangle error. */
|
|
68
|
+
export class SdpError extends WebRtcError {
|
|
69
|
+
/**
|
|
70
|
+
* @param {string} message
|
|
71
|
+
* @param {object} [options]
|
|
72
|
+
*/
|
|
73
|
+
constructor(message, options = {}) {
|
|
74
|
+
super(message, { code: options.code || 'ZQ_WEBRTC_SDP', context: options.context, cause: options.cause });
|
|
75
|
+
this.name = 'SdpError';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
/** TURN credential fetch / refresh error. */
|
|
81
|
+
export class TurnError extends WebRtcError {
|
|
82
|
+
/**
|
|
83
|
+
* @param {string} message
|
|
84
|
+
* @param {object} [options]
|
|
85
|
+
*/
|
|
86
|
+
constructor(message, options = {}) {
|
|
87
|
+
super(message, { code: options.code || 'ZQ_WEBRTC_TURN', context: options.context, cause: options.cause });
|
|
88
|
+
this.name = 'TurnError';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
/** End-to-end encryption (SFrame / key exchange) error. */
|
|
94
|
+
export class E2eeError extends WebRtcError {
|
|
95
|
+
/**
|
|
96
|
+
* @param {string} message
|
|
97
|
+
* @param {object} [options]
|
|
98
|
+
*/
|
|
99
|
+
constructor(message, options = {}) {
|
|
100
|
+
super(message, { code: options.code || 'ZQ_WEBRTC_E2EE', context: options.context, cause: options.cause });
|
|
101
|
+
this.name = 'E2eeError';
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
/** SFU adapter (mediasoup / LiveKit) error. */
|
|
107
|
+
export class SfuError extends WebRtcError {
|
|
108
|
+
/**
|
|
109
|
+
* @param {string} message
|
|
110
|
+
* @param {object} [options]
|
|
111
|
+
*/
|
|
112
|
+
constructor(message, options = {}) {
|
|
113
|
+
super(message, { code: options.code || 'ZQ_WEBRTC_SFU', context: options.context, cause: options.cause });
|
|
114
|
+
this.name = 'SfuError';
|
|
115
|
+
}
|
|
116
|
+
}
|