@encarnage7/local-meet 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +403 -2
- package/dist/index.cjs +2558 -0
- package/dist/index.d.ts +223 -0
- package/dist/index.mjs +2560 -0
- package/dist/local-meet.min.js +2574 -0
- package/package.json +44 -4
- package/src/crypto.js +92 -0
- package/src/events.js +38 -0
- package/src/index.js +150 -0
- package/src/room.js +2272 -0
- package/src/types.d.ts +223 -0
package/package.json
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@encarnage7/local-meet",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Zero-trust WebRTC P2P Mesh & Cloudflare Calls SFU client library with E2EE and resilient HTTPS signaling.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.mjs",
|
|
8
|
+
"browser": "./dist/local-meet.min.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.mjs",
|
|
14
|
+
"require": "./dist/index.cjs",
|
|
15
|
+
"default": "./dist/index.mjs"
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"src",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "node build.js",
|
|
27
|
+
"prepublishOnly": "node build.js"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"webrtc",
|
|
31
|
+
"video-call",
|
|
32
|
+
"cloudflare-calls",
|
|
33
|
+
"p2p",
|
|
34
|
+
"mesh",
|
|
35
|
+
"e2ee",
|
|
36
|
+
"audio-call",
|
|
37
|
+
"screensharing",
|
|
38
|
+
"sdk"
|
|
39
|
+
],
|
|
40
|
+
"author": "Local Meet Contributors",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/gambaru/local-meet.git"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/crypto.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cifrado de paquetes de datos de sala con AES-GCM-256 usando Web Crypto API.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const PBKDF2_ITERATIONS = 100000;
|
|
6
|
+
const cachedAesKeys = new Map();
|
|
7
|
+
|
|
8
|
+
function bytesToBase64(bytes) {
|
|
9
|
+
let binary = "";
|
|
10
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
11
|
+
binary += String.fromCharCode(bytes[i]);
|
|
12
|
+
}
|
|
13
|
+
return btoa(binary);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function base64ToBytes(base64) {
|
|
17
|
+
const binary = atob(base64);
|
|
18
|
+
const out = new Uint8Array(binary.length);
|
|
19
|
+
for (let i = 0; i < binary.length; i++) {
|
|
20
|
+
out[i] = binary.charCodeAt(i);
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function deriveRoomAesKey(roomHash, extraSecret = "") {
|
|
26
|
+
const cleanHash = (roomHash || "").toLowerCase().trim();
|
|
27
|
+
const cacheKey = `${cleanHash}::${extraSecret}`;
|
|
28
|
+
if (cachedAesKeys.has(cacheKey)) {
|
|
29
|
+
return cachedAesKeys.get(cacheKey);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const salt = new TextEncoder().encode(`local-meet-salt-v1:${cleanHash}`);
|
|
33
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
34
|
+
"raw",
|
|
35
|
+
new TextEncoder().encode(`${cleanHash}:${extraSecret}`),
|
|
36
|
+
"PBKDF2",
|
|
37
|
+
false,
|
|
38
|
+
["deriveKey"]
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const aesKey = await crypto.subtle.deriveKey(
|
|
42
|
+
{
|
|
43
|
+
name: "PBKDF2",
|
|
44
|
+
salt,
|
|
45
|
+
iterations: PBKDF2_ITERATIONS,
|
|
46
|
+
hash: "SHA-256",
|
|
47
|
+
},
|
|
48
|
+
keyMaterial,
|
|
49
|
+
{ name: "AES-GCM", length: 256 },
|
|
50
|
+
false,
|
|
51
|
+
["encrypt", "decrypt"]
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
cachedAesKeys.set(cacheKey, aesKey);
|
|
55
|
+
return aesKey;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function encryptDataPacket(roomHash, plaintext, extraSecret = "") {
|
|
59
|
+
const aesKey = await deriveRoomAesKey(roomHash, extraSecret);
|
|
60
|
+
const iv = new Uint8Array(12);
|
|
61
|
+
crypto.getRandomValues(iv);
|
|
62
|
+
const encoded = new TextEncoder().encode(plaintext);
|
|
63
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
64
|
+
{ name: "AES-GCM", iv },
|
|
65
|
+
aesKey,
|
|
66
|
+
encoded
|
|
67
|
+
);
|
|
68
|
+
return {
|
|
69
|
+
e2ee: "aes-gcm-256",
|
|
70
|
+
iv: bytesToBase64(iv),
|
|
71
|
+
ct: bytesToBase64(new Uint8Array(ciphertext)),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function decryptDataPacket(roomHash, packet, extraSecret = "") {
|
|
76
|
+
if (!packet || packet.e2ee !== "aes-gcm-256" || !packet.iv || !packet.ct) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const aesKey = await deriveRoomAesKey(roomHash, extraSecret);
|
|
81
|
+
const iv = base64ToBytes(packet.iv);
|
|
82
|
+
const ct = base64ToBytes(packet.ct);
|
|
83
|
+
const decryptedBuffer = await crypto.subtle.decrypt(
|
|
84
|
+
{ name: "AES-GCM", iv },
|
|
85
|
+
aesKey,
|
|
86
|
+
ct
|
|
87
|
+
);
|
|
88
|
+
return new TextDecoder().decode(decryptedBuffer);
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
package/src/events.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Eventos emitidos por LocalMeetRoom
|
|
3
|
+
*/
|
|
4
|
+
export const RoomEvents = Object.freeze({
|
|
5
|
+
ParticipantConnected: "participantConnected",
|
|
6
|
+
ParticipantDisconnected: "participantDisconnected",
|
|
7
|
+
ParticipantMetadataChanged: "participantMetadataChanged",
|
|
8
|
+
ParticipantPermissionsChanged: "participantPermissionsChanged",
|
|
9
|
+
DataReceived: "dataReceived",
|
|
10
|
+
TrackSubscribed: "trackSubscribed",
|
|
11
|
+
TrackUnsubscribed: "trackUnsubscribed",
|
|
12
|
+
TrackUnpublished: "trackUnpublished",
|
|
13
|
+
LocalTrackPublished: "localTrackPublished",
|
|
14
|
+
LocalTrackUnpublished: "localTrackUnpublished",
|
|
15
|
+
TrackMuted: "trackMuted",
|
|
16
|
+
TrackUnmuted: "trackUnmuted",
|
|
17
|
+
ActiveSpeakersChanged: "activeSpeakersChanged",
|
|
18
|
+
Disconnected: "disconnected",
|
|
19
|
+
Error: "error",
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const TrackSources = Object.freeze({
|
|
23
|
+
Camera: "camera",
|
|
24
|
+
Microphone: "microphone",
|
|
25
|
+
ScreenShare: "screen_share",
|
|
26
|
+
ScreenShareAudio: "screen_share_audio",
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const TrackKinds = Object.freeze({
|
|
30
|
+
Audio: "audio",
|
|
31
|
+
Video: "video",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export const EngineModes = Object.freeze({
|
|
35
|
+
CloudflareCalls: "cf-calls",
|
|
36
|
+
P2PMesh: "p2p",
|
|
37
|
+
LiveKit: "livekit",
|
|
38
|
+
});
|
package/src/index.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LocalMeetRoom,
|
|
3
|
+
HybridTrack,
|
|
4
|
+
HybridPublication,
|
|
5
|
+
HybridParticipant,
|
|
6
|
+
} from "./room.js";
|
|
7
|
+
import { RoomEvents, TrackSources, TrackKinds, EngineModes } from "./events.js";
|
|
8
|
+
import { encryptDataPacket, decryptDataPacket } from "./crypto.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Cliente principal estilo Axios para inicializar llamadas con 1 sola línea de código.
|
|
12
|
+
*/
|
|
13
|
+
export const LocalMeet = {
|
|
14
|
+
RoomEvents,
|
|
15
|
+
TrackSources,
|
|
16
|
+
TrackKinds,
|
|
17
|
+
EngineModes,
|
|
18
|
+
LocalMeetRoom,
|
|
19
|
+
HybridTrack,
|
|
20
|
+
HybridPublication,
|
|
21
|
+
HybridParticipant,
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Crea una sala nueva como anfitrión en el backend y devuelve una instancia lista de LocalMeetRoom.
|
|
25
|
+
*
|
|
26
|
+
* @param {Object} options
|
|
27
|
+
* @param {string} options.serverUrl - URL del Worker o servidor de señalización (ej: "https://mi-meet.workers.dev")
|
|
28
|
+
* @param {string} [options.hostName="Anfitrión"] - Nombre a mostrar del anfitrión
|
|
29
|
+
* @param {string} [options.title=""] - Título de la reunión
|
|
30
|
+
* @param {string} [options.engine="cf-calls"] - "cf-calls" | "p2p" | "livekit"
|
|
31
|
+
* @param {string} [options.accessKey=""] - Clave HOST_ACCESS_KEY si el servidor la requiere
|
|
32
|
+
* @param {string} [options.customHash=""] - Hash de sala a reutilizar si ya eres el anfitrión
|
|
33
|
+
* @param {string} [options.hostSecret=""] - Secreto de anfitrión previo
|
|
34
|
+
* @returns {Promise<LocalMeetRoom>}
|
|
35
|
+
*/
|
|
36
|
+
async createRoom(options = {}) {
|
|
37
|
+
const {
|
|
38
|
+
serverUrl = typeof window !== "undefined" ? window.location.origin : "",
|
|
39
|
+
hostName = "Anfitrión",
|
|
40
|
+
title = "",
|
|
41
|
+
engine = EngineModes.CloudflareCalls,
|
|
42
|
+
accessKey = "",
|
|
43
|
+
customHash = "",
|
|
44
|
+
hostSecret = "",
|
|
45
|
+
debug = false,
|
|
46
|
+
} = options;
|
|
47
|
+
|
|
48
|
+
const cleanServer = (serverUrl || "").replace(/\/+$/, "");
|
|
49
|
+
const res = await fetch(`${cleanServer}/api/host-join`, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: { "Content-Type": "application/json" },
|
|
52
|
+
body: JSON.stringify({
|
|
53
|
+
host_name: hostName,
|
|
54
|
+
title,
|
|
55
|
+
engine,
|
|
56
|
+
access_key: accessKey,
|
|
57
|
+
room_hash: customHash,
|
|
58
|
+
host_secret: hostSecret,
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const data = await res.json();
|
|
63
|
+
if (!res.ok) {
|
|
64
|
+
throw new Error(data?.detail || `Error al crear sala (${res.status})`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const room = new LocalMeetRoom({
|
|
68
|
+
serverUrl: cleanServer,
|
|
69
|
+
roomHash: data.room_hash,
|
|
70
|
+
title: data.title || title,
|
|
71
|
+
identity: data.identity,
|
|
72
|
+
displayName: data.host_name || hostName,
|
|
73
|
+
isHost: true,
|
|
74
|
+
hostSecret: data.host_secret,
|
|
75
|
+
engine: data.engine || engine,
|
|
76
|
+
cfCallsReady: Boolean(data.cf_calls_ready),
|
|
77
|
+
iceServers: data.ice_servers,
|
|
78
|
+
startInWaitingRoom: false,
|
|
79
|
+
debug,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return room;
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Se une a una sala existente como invitado.
|
|
87
|
+
*
|
|
88
|
+
* @param {Object} options
|
|
89
|
+
* @param {string} options.serverUrl - URL del Worker o servidor
|
|
90
|
+
* @param {string} options.roomHash - Hash de la sala (ej: "c4f2-9k81-m3n2")
|
|
91
|
+
* @param {string} [options.guestName="Invitado"] - Nombre del participante
|
|
92
|
+
* @returns {Promise<LocalMeetRoom>}
|
|
93
|
+
*/
|
|
94
|
+
async join(options = {}) {
|
|
95
|
+
const {
|
|
96
|
+
serverUrl = typeof window !== "undefined" ? window.location.origin : "",
|
|
97
|
+
roomHash = "",
|
|
98
|
+
guestName = "Invitado",
|
|
99
|
+
debug = false,
|
|
100
|
+
} = options;
|
|
101
|
+
|
|
102
|
+
const cleanServer = (serverUrl || "").replace(/\/+$/, "");
|
|
103
|
+
const cleanHash = (roomHash || "").toLowerCase().trim();
|
|
104
|
+
|
|
105
|
+
const res = await fetch(`${cleanServer}/api/guest-join`, {
|
|
106
|
+
method: "POST",
|
|
107
|
+
headers: { "Content-Type": "application/json" },
|
|
108
|
+
body: JSON.stringify({
|
|
109
|
+
room_hash: cleanHash,
|
|
110
|
+
guest_name: guestName,
|
|
111
|
+
}),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const data = await res.json();
|
|
115
|
+
if (!res.ok) {
|
|
116
|
+
throw new Error(data?.detail || `Error al unirse a sala (${res.status})`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const room = new LocalMeetRoom({
|
|
120
|
+
serverUrl: cleanServer,
|
|
121
|
+
roomHash: data.room_hash,
|
|
122
|
+
title: `Sala ${data.room_hash}`,
|
|
123
|
+
identity: data.identity,
|
|
124
|
+
displayName: data.guest_name || guestName,
|
|
125
|
+
isHost: false,
|
|
126
|
+
engine: data.engine || EngineModes.CloudflareCalls,
|
|
127
|
+
cfCallsReady: Boolean(data.cf_calls_ready),
|
|
128
|
+
iceServers: data.ice_servers,
|
|
129
|
+
startInWaitingRoom: true,
|
|
130
|
+
debug,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return room;
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export {
|
|
138
|
+
LocalMeetRoom,
|
|
139
|
+
HybridTrack,
|
|
140
|
+
HybridPublication,
|
|
141
|
+
HybridParticipant,
|
|
142
|
+
RoomEvents,
|
|
143
|
+
TrackSources,
|
|
144
|
+
TrackKinds,
|
|
145
|
+
EngineModes,
|
|
146
|
+
encryptDataPacket,
|
|
147
|
+
decryptDataPacket,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export default LocalMeet;
|