peer-client 1.0.0 → 1.0.1

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.
@@ -286,18 +286,22 @@ export class PeerClient extends Emitter {
286
286
  break;
287
287
  }
288
288
  case 'peer_joined': {
289
- const info = msg.payload ?? { fingerprint: msg.from ?? '', alias: '' };
290
- this.emit('peer_joined', info);
289
+ const ns = msg.namespace ?? '';
290
+ const info = typeof msg.payload === 'string'
291
+ ? JSON.parse(msg.payload)
292
+ : msg.payload ?? { fingerprint: msg.from ?? '', alias: '' };
293
+ this.emit('peer_joined', info, ns);
291
294
  break;
292
295
  }
293
296
  case 'peer_left': {
297
+ const ns = msg.namespace ?? '';
294
298
  const fp = msg.from ?? msg.payload?.fingerprint ?? '';
295
299
  const peer = this.peers.get(fp);
296
300
  if (peer) {
297
301
  peer.close();
298
302
  this.peers.delete(fp);
299
303
  }
300
- this.emit('peer_left', fp);
304
+ this.emit('peer_left', fp, ns);
301
305
  break;
302
306
  }
303
307
  case 'signal': {
@@ -0,0 +1,37 @@
1
+ import { Emitter } from './core/emitter';
2
+ import type { PeerClient } from './core/client';
3
+ import type { PeerInfo } from './core/types';
4
+ import { DirectRoom } from './room';
5
+ import { E2E } from './crypto';
6
+ type E2ERoomEvent = 'ready' | 'data' | 'decrypt_error' | 'peer_joined' | 'peer_left' | 'state_changed' | 'closed' | 'error';
7
+ type E2ERoomState = 'connecting' | 'exchanging' | 'ready' | 'closed';
8
+ export declare class E2EDirectRoom extends Emitter<E2ERoomEvent> {
9
+ private client;
10
+ private roomId;
11
+ private room;
12
+ private e2e;
13
+ private remoteFingerprint;
14
+ private _state;
15
+ private _closed;
16
+ private exchangeTimer;
17
+ private cleanups;
18
+ constructor(client: PeerClient, roomId: string, e2e?: E2E);
19
+ get state(): E2ERoomState;
20
+ get fingerprint(): string;
21
+ hasEncryption(): boolean;
22
+ getRoom(): DirectRoom | null;
23
+ getE2E(): E2E;
24
+ create(): Promise<void>;
25
+ join(): Promise<PeerInfo[]>;
26
+ send(data: any): void;
27
+ close(): void;
28
+ private initE2E;
29
+ private setState;
30
+ private attachListeners;
31
+ private startKeyExchange;
32
+ private handleKeyExchange;
33
+ private handleData;
34
+ private encryptAndSend;
35
+ private clearExchangeTimer;
36
+ }
37
+ export {};
@@ -0,0 +1,221 @@
1
+ import { Emitter } from './core/emitter';
2
+ import { DirectRoom } from './room';
3
+ import { E2E } from './crypto';
4
+ const KEY_EXCHANGE_TIMEOUT = 10000;
5
+ export class E2EDirectRoom extends Emitter {
6
+ client;
7
+ roomId;
8
+ room = null;
9
+ e2e;
10
+ remoteFingerprint = '';
11
+ _state = 'connecting';
12
+ _closed = false;
13
+ exchangeTimer = null;
14
+ cleanups = [];
15
+ constructor(client, roomId, e2e) {
16
+ super();
17
+ this.client = client;
18
+ this.roomId = roomId;
19
+ this.e2e = e2e ?? new E2E();
20
+ }
21
+ get state() {
22
+ return this._state;
23
+ }
24
+ get fingerprint() {
25
+ return this.remoteFingerprint;
26
+ }
27
+ hasEncryption() {
28
+ return this._state === 'ready' && this.e2e.hasKey(this.remoteFingerprint);
29
+ }
30
+ getRoom() {
31
+ return this.room;
32
+ }
33
+ getE2E() {
34
+ return this.e2e;
35
+ }
36
+ async create() {
37
+ if (this._closed)
38
+ throw new Error('Room is closed');
39
+ await this.initE2E();
40
+ this.room = new DirectRoom(this.client, this.roomId);
41
+ this.attachListeners();
42
+ await this.room.create();
43
+ }
44
+ async join() {
45
+ if (this._closed)
46
+ throw new Error('Room is closed');
47
+ await this.initE2E();
48
+ this.room = new DirectRoom(this.client, this.roomId);
49
+ this.attachListeners();
50
+ return this.room.join();
51
+ }
52
+ send(data) {
53
+ if (this._closed)
54
+ return;
55
+ if (!this.room)
56
+ return;
57
+ if (this._state === 'ready' && this.e2e.hasKey(this.remoteFingerprint)) {
58
+ this.encryptAndSend(data);
59
+ }
60
+ else {
61
+ this.room.send({ _plain: true, data });
62
+ }
63
+ }
64
+ close() {
65
+ if (this._closed)
66
+ return;
67
+ this._closed = true;
68
+ this.setState('closed');
69
+ this.clearExchangeTimer();
70
+ this.cleanups.forEach((fn) => fn());
71
+ this.cleanups = [];
72
+ if (this.room) {
73
+ this.room.close();
74
+ this.room = null;
75
+ }
76
+ this.emit('closed');
77
+ this.removeAllListeners();
78
+ }
79
+ async initE2E() {
80
+ if (!this.e2e.getPublicKeyRaw()) {
81
+ await this.e2e.init();
82
+ }
83
+ }
84
+ setState(s) {
85
+ if (this._state === s)
86
+ return;
87
+ this._state = s;
88
+ this.emit('state_changed', s);
89
+ }
90
+ attachListeners() {
91
+ if (!this.room)
92
+ return;
93
+ const offData = this.room.on('data', (raw, from) => {
94
+ this.handleData(raw, from);
95
+ });
96
+ const offJoined = this.room.on('peer_joined', (info) => {
97
+ this.remoteFingerprint = info.fingerprint;
98
+ this.emit('peer_joined', info);
99
+ });
100
+ const offConnected = this.room.on('peer_connected', (fp) => {
101
+ this.remoteFingerprint = fp;
102
+ this.startKeyExchange(fp);
103
+ });
104
+ const offLeft = this.room.on('peer_left', (fp) => {
105
+ if (fp === this.remoteFingerprint) {
106
+ this.e2e.removeKey(fp);
107
+ this.remoteFingerprint = '';
108
+ if (this._state !== 'closed') {
109
+ this.setState('connecting');
110
+ }
111
+ }
112
+ this.emit('peer_left', fp);
113
+ });
114
+ const offClosed = this.room.on('closed', () => {
115
+ this.close();
116
+ });
117
+ const offError = this.room.on('error', (err) => {
118
+ this.emit('error', err);
119
+ });
120
+ this.cleanups.push(offData, offJoined, offConnected, offLeft, offClosed, offError);
121
+ }
122
+ startKeyExchange(fingerprint) {
123
+ if (this._closed)
124
+ return;
125
+ this.setState('exchanging');
126
+ this.clearExchangeTimer();
127
+ this.exchangeTimer = setTimeout(() => {
128
+ if (this._state === 'exchanging') {
129
+ this.emit('error', new Error('Key exchange timeout'));
130
+ this.setState('connecting');
131
+ }
132
+ }, KEY_EXCHANGE_TIMEOUT);
133
+ try {
134
+ const pubKey = this.e2e.getPublicKeyB64();
135
+ this.room.send({
136
+ _e2e_exchange: true,
137
+ type: 'key_offer',
138
+ publicKey: pubKey,
139
+ fingerprint: this.client.fingerprint,
140
+ });
141
+ }
142
+ catch (err) {
143
+ this.clearExchangeTimer();
144
+ this.emit('error', err instanceof Error ? err : new Error(String(err)));
145
+ }
146
+ }
147
+ async handleKeyExchange(payload, from) {
148
+ if (this._closed)
149
+ return;
150
+ if (payload.type === 'key_offer') {
151
+ try {
152
+ await this.e2e.deriveKey(from, payload.publicKey);
153
+ const pubKey = this.e2e.getPublicKeyB64();
154
+ this.room.send({
155
+ _e2e_exchange: true,
156
+ type: 'key_ack',
157
+ publicKey: pubKey,
158
+ fingerprint: this.client.fingerprint,
159
+ });
160
+ this.clearExchangeTimer();
161
+ this.remoteFingerprint = from;
162
+ this.setState('ready');
163
+ this.emit('ready', from);
164
+ }
165
+ catch (err) {
166
+ this.emit('error', err instanceof Error ? err : new Error(String(err)));
167
+ }
168
+ }
169
+ if (payload.type === 'key_ack') {
170
+ try {
171
+ await this.e2e.deriveKey(from, payload.publicKey);
172
+ this.clearExchangeTimer();
173
+ this.remoteFingerprint = from;
174
+ this.setState('ready');
175
+ this.emit('ready', from);
176
+ }
177
+ catch (err) {
178
+ this.emit('error', err instanceof Error ? err : new Error(String(err)));
179
+ }
180
+ }
181
+ }
182
+ async handleData(raw, from) {
183
+ if (raw?._e2e_exchange) {
184
+ await this.handleKeyExchange(raw, from);
185
+ return;
186
+ }
187
+ if (raw?._encrypted && this.e2e.hasKey(from)) {
188
+ try {
189
+ const decrypted = await this.e2e.decrypt(from, raw.data);
190
+ const parsed = JSON.parse(decrypted);
191
+ this.emit('data', parsed, from);
192
+ }
193
+ catch {
194
+ this.emit('decrypt_error', from, raw);
195
+ this.startKeyExchange(from);
196
+ }
197
+ return;
198
+ }
199
+ if (raw?._plain) {
200
+ this.emit('data', raw.data, from);
201
+ return;
202
+ }
203
+ this.emit('data', raw, from);
204
+ }
205
+ async encryptAndSend(data) {
206
+ try {
207
+ const encrypted = await this.e2e.encrypt(this.remoteFingerprint, JSON.stringify(data));
208
+ this.room.send({ _encrypted: true, data: encrypted });
209
+ }
210
+ catch (err) {
211
+ this.emit('error', err instanceof Error ? err : new Error(String(err)));
212
+ this.room.send({ _plain: true, data });
213
+ }
214
+ }
215
+ clearExchangeTimer() {
216
+ if (this.exchangeTimer) {
217
+ clearTimeout(this.exchangeTimer);
218
+ this.exchangeTimer = null;
219
+ }
220
+ }
221
+ }
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export { Identity } from './core/identity';
4
4
  export { Transport } from './core/transport';
5
5
  export { Emitter, setEmitterErrorHandler } from './core/emitter';
6
6
  export { DirectRoom, GroupRoom } from './room';
7
+ export { E2EDirectRoom } from './e2e.room';
7
8
  export { DirectMedia, GroupMedia } from './media';
8
9
  export { JSONTransfer, FileTransfer, ImageTransfer } from './transfer';
9
10
  export { StateSync, CRDTSync } from './sync';
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ export { Identity } from './core/identity';
4
4
  export { Transport } from './core/transport';
5
5
  export { Emitter, setEmitterErrorHandler } from './core/emitter';
6
6
  export { DirectRoom, GroupRoom } from './room';
7
+ export { E2EDirectRoom } from './e2e.room';
7
8
  export { DirectMedia, GroupMedia } from './media';
8
9
  export { JSONTransfer, FileTransfer, ImageTransfer } from './transfer';
9
10
  export { StateSync, CRDTSync } from './sync';
package/dist/room.js CHANGED
@@ -26,13 +26,17 @@ export class DirectRoom extends Emitter {
26
26
  return peers;
27
27
  }
28
28
  listen() {
29
- const offJoined = this.client.on('peer_joined', (info) => {
29
+ const offJoined = this.client.on('peer_joined', (info, ns) => {
30
+ if (ns && ns !== this.roomId)
31
+ return;
30
32
  this.emit('peer_joined', info);
31
33
  if (!this.remotePeer) {
32
34
  this.connectTo(info.fingerprint, info.alias);
33
35
  }
34
36
  });
35
- const offLeft = this.client.on('peer_left', (fp) => {
37
+ const offLeft = this.client.on('peer_left', (fp, ns) => {
38
+ if (ns && ns !== this.roomId)
39
+ return;
36
40
  if (fp === this.remoteFingerprint) {
37
41
  this.remotePeer = null;
38
42
  this.remoteFingerprint = '';
@@ -123,11 +127,15 @@ export class GroupRoom extends Emitter {
123
127
  return peers;
124
128
  }
125
129
  listen() {
126
- const offJoined = this.client.on('peer_joined', (info) => {
130
+ const offJoined = this.client.on('peer_joined', (info, ns) => {
131
+ if (ns && ns !== this.roomId)
132
+ return;
127
133
  this.emit('peer_joined', info);
128
134
  this.connectTo(info.fingerprint, info.alias);
129
135
  });
130
- const offLeft = this.client.on('peer_left', (fp) => {
136
+ const offLeft = this.client.on('peer_left', (fp, ns) => {
137
+ if (ns && ns !== this.roomId)
138
+ return;
131
139
  const hadP2P = this.connectedPeers.has(fp);
132
140
  this.connectedPeers.delete(fp);
133
141
  this.relayPeers.delete(fp);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peer-client",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Universal WebRTC peer-to-peer library with signaling, rooms, file transfer, state sync, and E2E encryption",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",