@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/package.json CHANGED
@@ -1,6 +1,46 @@
1
1
  {
2
2
  "name": "@encarnage7/local-meet",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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;