@decentnetwork/lan 0.1.159 → 0.1.160
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/bin/tun-helper-darwin-amd64 +0 -0
- package/bin/tun-helper-darwin-arm64 +0 -0
- package/bin/tun-helper-linux-amd64 +0 -0
- package/bin/tun-helper-linux-arm64 +0 -0
- package/dist/carrier/peer-manager.d.ts +8 -0
- package/dist/carrier/peer-manager.js +21 -0
- package/dist/daemon/ipc.d.ts +12 -1
- package/dist/daemon/ipc.js +10 -0
- package/dist/daemon/server.d.ts +6 -0
- package/dist/daemon/server.js +49 -0
- package/dist/ui/desktop/app.js +211 -6
- package/dist/ui/desktop/index.html +2 -0
- package/dist/ui/desktop/vendor/peer-webrtc.js +626 -0
- package/dist/ui/server.js +22 -0
- package/package.json +3 -2
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -66,6 +66,14 @@ export declare class PeerManager extends EventEmitter {
|
|
|
66
66
|
* bulkmsg-split) — the only file path native iOS/C Carrier clients can
|
|
67
67
|
* receive online. Use for native friends; JS friends keep sendFile(). */
|
|
68
68
|
sendInlineFile(userid: string, data: Uint8Array, name: string): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Send a WebRTC call-signaling payload to a friend over the Carrier
|
|
71
|
+
* friend-invite channel (the "carrier" extension), the exact transport the
|
|
72
|
+
* iOS/Android Beagle apps listen on for calls. `data` is an RtcSignal JSON
|
|
73
|
+
* string/bytes (see @decentnetwork/peer-webrtc). Realtime — requires a live
|
|
74
|
+
* session, no offline fallback.
|
|
75
|
+
*/
|
|
76
|
+
sendCallSignal(userid: string, data: Uint8Array | string): Promise<void>;
|
|
69
77
|
/** True when the friend is a NATIVE Carrier client (iOS/Android/C): its
|
|
70
78
|
* relay/DHT key differs from its identity key. JS peers announce their
|
|
71
79
|
* identity key as their DHT key, so the two match. */
|
|
@@ -129,6 +129,18 @@ export class PeerManager extends EventEmitter {
|
|
|
129
129
|
throw new Error("Peer not created. Call create() first.");
|
|
130
130
|
await this.peer.sendInlineFile(userid, { name, data });
|
|
131
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Send a WebRTC call-signaling payload to a friend over the Carrier
|
|
134
|
+
* friend-invite channel (the "carrier" extension), the exact transport the
|
|
135
|
+
* iOS/Android Beagle apps listen on for calls. `data` is an RtcSignal JSON
|
|
136
|
+
* string/bytes (see @decentnetwork/peer-webrtc). Realtime — requires a live
|
|
137
|
+
* session, no offline fallback.
|
|
138
|
+
*/
|
|
139
|
+
async sendCallSignal(userid, data) {
|
|
140
|
+
if (!this.peer)
|
|
141
|
+
throw new Error("Peer not created. Call create() first.");
|
|
142
|
+
await this.peer.sendInvite(userid, data, { ext: "carrier" });
|
|
143
|
+
}
|
|
132
144
|
/** True when the friend is a NATIVE Carrier client (iOS/Android/C): its
|
|
133
145
|
* relay/DHT key differs from its identity key. JS peers announce their
|
|
134
146
|
* identity key as their DHT key, so the two match. */
|
|
@@ -534,6 +546,15 @@ export class PeerManager extends EventEmitter {
|
|
|
534
546
|
// Inline files (iOS/C Carrier style: FileModel JSON over bulkmsg).
|
|
535
547
|
// The SDK reassembles + decodes; forward to the daemon to save + log.
|
|
536
548
|
this.peer.onInlineFile((f) => this.emit("inline-file", f));
|
|
549
|
+
// WebRTC call signaling (RtcSignal JSON) over the Carrier "carrier"
|
|
550
|
+
// friend-invite extension — the channel iOS/Android Beagle use for calls.
|
|
551
|
+
// Forward the raw payload to the daemon, which relays it to the browser UI
|
|
552
|
+
// (peer-webrtc CallEngine) over the low-latency call-signal poll.
|
|
553
|
+
this.peer.onInvite((evt) => {
|
|
554
|
+
if (evt.ext !== undefined && evt.ext !== "carrier")
|
|
555
|
+
return;
|
|
556
|
+
this.emit("call-signal", { pubkey: evt.pubkey, data: evt.data });
|
|
557
|
+
});
|
|
537
558
|
// Toxcore-standard file transfer (native-compatible). Forward offers,
|
|
538
559
|
// progress, and completions to the daemon, which auto-accepts and saves.
|
|
539
560
|
this.peer.onFile((o) => this.emit("file-offer", o));
|
package/dist/daemon/ipc.d.ts
CHANGED
|
@@ -82,6 +82,15 @@ export interface IpcHandlers {
|
|
|
82
82
|
fileCancel: (peer: string, ids: string[]) => Promise<Record<string, unknown>>;
|
|
83
83
|
/** Mark a conversation read up to `ts` (defaults to now) — clears unread. */
|
|
84
84
|
chatMarkRead: (userid: string, ts?: number) => Promise<void>;
|
|
85
|
+
/** Send a WebRTC call-signaling payload (RtcSignal JSON) to a friend over the
|
|
86
|
+
* Carrier "carrier" friend-invite extension — the channel iOS/Android Beagle
|
|
87
|
+
* listen on for calls. Backs the desktop UI's audio/video calls. */
|
|
88
|
+
callSignal: (userid: string, data: string) => Promise<void>;
|
|
89
|
+
/** Long-poll for inbound call-signaling payloads. Resolves with any queued
|
|
90
|
+
* signals immediately, otherwise holds the connection until one arrives or a
|
|
91
|
+
* ~20s timeout elapses (then resolves empty). The UI re-polls to get
|
|
92
|
+
* near-instant offer/answer/candidate delivery without a WebSocket. */
|
|
93
|
+
callPoll: () => Promise<Record<string, unknown>>;
|
|
85
94
|
/** Re-read proxy allowlist from config and apply it to the running
|
|
86
95
|
* proxy WITHOUT restarting the daemon. Lets `agentnet proxy
|
|
87
96
|
* allow-host` take effect instantly instead of forcing a daemon
|
|
@@ -104,7 +113,7 @@ export interface IpcHandlers {
|
|
|
104
113
|
selfRestart: () => Promise<Record<string, unknown>>;
|
|
105
114
|
}
|
|
106
115
|
export interface IpcRequest {
|
|
107
|
-
op: "friend-request" | "ping" | "diag" | "friends-pending" | "friends-accept" | "friends-reject" | "chat-send" | "chat-history" | "friends-list" | "friend-remove" | "friend-set-alias" | "friends-autoaccept" | "set-profile" | "file-send" | "file-delete" | "file-cancel" | "chat-mark-read" | "subscribe" | "sign" | "proxy-reload" | "proxy-access" | "self-restart";
|
|
116
|
+
op: "friend-request" | "ping" | "diag" | "friends-pending" | "friends-accept" | "friends-reject" | "chat-send" | "chat-history" | "friends-list" | "friend-remove" | "friend-set-alias" | "friends-autoaccept" | "set-profile" | "file-send" | "file-delete" | "file-cancel" | "chat-mark-read" | "subscribe" | "sign" | "call-signal" | "call-poll" | "proxy-reload" | "proxy-access" | "self-restart";
|
|
108
117
|
address?: string;
|
|
109
118
|
hello?: string;
|
|
110
119
|
userid?: string;
|
|
@@ -118,6 +127,8 @@ export interface IpcRequest {
|
|
|
118
127
|
ts?: number;
|
|
119
128
|
enabled?: boolean;
|
|
120
129
|
ids?: string[];
|
|
130
|
+
/** RtcSignal JSON payload for the "call-signal" op. */
|
|
131
|
+
data?: string;
|
|
121
132
|
}
|
|
122
133
|
export interface IpcResponseOk {
|
|
123
134
|
ok: true;
|
package/dist/daemon/ipc.js
CHANGED
|
@@ -241,6 +241,16 @@ export class IpcServer {
|
|
|
241
241
|
throw new Error("text is required");
|
|
242
242
|
return await this.handlers.sign(req.text);
|
|
243
243
|
}
|
|
244
|
+
case "call-signal": {
|
|
245
|
+
if (!req.userid)
|
|
246
|
+
throw new Error("userid is required");
|
|
247
|
+
if (typeof req.data !== "string")
|
|
248
|
+
throw new Error("data is required");
|
|
249
|
+
await this.handlers.callSignal(req.userid, req.data);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
case "call-poll":
|
|
253
|
+
return await this.handlers.callPoll();
|
|
244
254
|
case "proxy-reload":
|
|
245
255
|
return await this.handlers.proxyReload();
|
|
246
256
|
case "proxy-access":
|
package/dist/daemon/server.d.ts
CHANGED
|
@@ -44,6 +44,12 @@ export declare class DaemonServer {
|
|
|
44
44
|
/** Outgoing file transfers in flight: fileId → the chat message tracking it,
|
|
45
45
|
* so progress/complete/cancel events can patch its status + sent bytes. */
|
|
46
46
|
private readonly activeSends;
|
|
47
|
+
/** Inbound WebRTC call-signaling payloads ({userid, data}) waiting to be
|
|
48
|
+
* drained by the UI's /api/call-poll long-poll. Bounded so a UI that never
|
|
49
|
+
* polls can't grow it unboundedly. */
|
|
50
|
+
private readonly callSignalQueue;
|
|
51
|
+
/** Resolvers for in-flight call-poll long-polls, woken when a signal lands. */
|
|
52
|
+
private callSignalWaiters;
|
|
47
53
|
/** Directory holding queued (offline) outgoing file bytes: <configDir>/outbox.
|
|
48
54
|
* One file per queued message, named by its msgId. Set in start(). */
|
|
49
55
|
private outboxDir;
|
package/dist/daemon/server.js
CHANGED
|
@@ -89,6 +89,12 @@ export class DaemonServer {
|
|
|
89
89
|
/** Outgoing file transfers in flight: fileId → the chat message tracking it,
|
|
90
90
|
* so progress/complete/cancel events can patch its status + sent bytes. */
|
|
91
91
|
activeSends = new Map();
|
|
92
|
+
/** Inbound WebRTC call-signaling payloads ({userid, data}) waiting to be
|
|
93
|
+
* drained by the UI's /api/call-poll long-poll. Bounded so a UI that never
|
|
94
|
+
* polls can't grow it unboundedly. */
|
|
95
|
+
callSignalQueue = [];
|
|
96
|
+
/** Resolvers for in-flight call-poll long-polls, woken when a signal lands. */
|
|
97
|
+
callSignalWaiters = [];
|
|
92
98
|
/** Directory holding queued (offline) outgoing file bytes: <configDir>/outbox.
|
|
93
99
|
* One file per queued message, named by its msgId. Set in start(). */
|
|
94
100
|
outboxDir = "";
|
|
@@ -529,6 +535,32 @@ export class DaemonServer {
|
|
|
529
535
|
chatMarkRead: async (userid, ts) => {
|
|
530
536
|
this.friendMeta?.markRead(userid, ts);
|
|
531
537
|
},
|
|
538
|
+
callSignal: async (userid, data) => {
|
|
539
|
+
// Relay a WebRTC RtcSignal from the browser UI out to the friend over
|
|
540
|
+
// the Carrier "carrier" invite extension. Realtime — requires a live
|
|
541
|
+
// session (throws if the peer is unreachable, so the UI can surface it).
|
|
542
|
+
await this.peerManager.sendCallSignal(userid, data);
|
|
543
|
+
},
|
|
544
|
+
callPoll: async () => {
|
|
545
|
+
// Drain immediately if signals are queued; otherwise hold up to ~20s
|
|
546
|
+
// for one to arrive (near-instant delivery without a WebSocket). The
|
|
547
|
+
// IPC client's timeout is 30s, so a 20s hold is safe.
|
|
548
|
+
if (this.callSignalQueue.length === 0) {
|
|
549
|
+
await new Promise((resolve) => {
|
|
550
|
+
const timer = setTimeout(() => {
|
|
551
|
+
this.callSignalWaiters = this.callSignalWaiters.filter((w) => w !== wake);
|
|
552
|
+
resolve();
|
|
553
|
+
}, 20_000);
|
|
554
|
+
const wake = () => {
|
|
555
|
+
clearTimeout(timer);
|
|
556
|
+
resolve();
|
|
557
|
+
};
|
|
558
|
+
this.callSignalWaiters.push(wake);
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
const signals = this.callSignalQueue.splice(0, this.callSignalQueue.length);
|
|
562
|
+
return { signals };
|
|
563
|
+
},
|
|
532
564
|
subscribe: (emit) => {
|
|
533
565
|
const onEvent = (event) => emit(event);
|
|
534
566
|
this.ipcEvents.on("event", onEvent);
|
|
@@ -931,6 +963,23 @@ export class DaemonServer {
|
|
|
931
963
|
this.ipcEvents.emit("event", { type: "chat", userid: f.pubkey, dir: "in" });
|
|
932
964
|
})().catch((e) => this.logger.warn(`Failed to save inline file: ${e.message}`));
|
|
933
965
|
});
|
|
966
|
+
// WebRTC call signaling (RtcSignal JSON) inbound over the Carrier
|
|
967
|
+
// "carrier" invite extension. Queue it for the UI's /api/call-poll and
|
|
968
|
+
// wake any pending long-poll for near-instant delivery. The payload is
|
|
969
|
+
// JSON text (native C senders NUL-terminate it — strip that here).
|
|
970
|
+
this.peerManager.on("call-signal", (evt) => {
|
|
971
|
+
const text = Buffer.from(evt.data).toString("utf-8").replace(/\0+$/u, "").trim();
|
|
972
|
+
if (!text)
|
|
973
|
+
return;
|
|
974
|
+
// Cap the backlog so a UI that stopped polling can't grow it forever.
|
|
975
|
+
if (this.callSignalQueue.length > 256)
|
|
976
|
+
this.callSignalQueue.shift();
|
|
977
|
+
this.callSignalQueue.push({ userid: evt.pubkey, data: text });
|
|
978
|
+
const waiters = this.callSignalWaiters;
|
|
979
|
+
this.callSignalWaiters = [];
|
|
980
|
+
for (const wake of waiters)
|
|
981
|
+
wake();
|
|
982
|
+
});
|
|
934
983
|
this.peerManager.on("friend-request", (req) => {
|
|
935
984
|
void pubkeyHexToUserid(req.pubkey).then((userid) => {
|
|
936
985
|
const who = `${req.name || "(unnamed)"} ${userid}`;
|
package/dist/ui/desktop/app.js
CHANGED
|
@@ -1241,7 +1241,7 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, selMode, selected, onT
|
|
|
1241
1241
|
), mine && m.status === "queued" ? /* @__PURE__ */ React.createElement("span", { style: { display: "inline-flex", alignItems: "center", gap: 2 }, title: "waiting for peer \u2014 will send when they're online" }, /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 11, stroke: 2.2, color: "var(--faint)" }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10, color: "var(--faint)" } }, T.queued || "queued")) : mine && m.status && /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 12, stroke: 2.2, color: m.status === "read" ? "var(--accent)" : "var(--faint)" })))
|
|
1242
1242
|
);
|
|
1243
1243
|
}
|
|
1244
|
-
function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, onAlias, onRemove, onOpenNet }) {
|
|
1244
|
+
function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, onAlias, onRemove, onOpenNet, onCall }) {
|
|
1245
1245
|
const scrollRef = React.useRef(null);
|
|
1246
1246
|
const fileRef = React.useRef(null);
|
|
1247
1247
|
const thread = threadProp && threadProp.length ? threadProp : [{ day: "Today" }, { from: "them", time: "\u2014", text: lang === "zh" ? "\u6682\u65E0\u6D88\u606F\u8BB0\u5F55\uFF0C\u53D1\u4E2A\u6D88\u606F\u6253\u4E2A\u62DB\u547C\u5427\u3002" : "No messages yet. Say hi." }];
|
|
@@ -1331,7 +1331,7 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
|
|
|
1331
1331
|
onDrop
|
|
1332
1332
|
},
|
|
1333
1333
|
dragOver && /* @__PURE__ */ React.createElement("div", { style: { position: "absolute", inset: 0, zIndex: 30, background: "rgba(91,140,255,0.10)", border: "2px dashed var(--accent)", borderRadius: 12, display: "flex", alignItems: "center", justifyContent: "center", pointerEvents: "none", fontFamily: "var(--mono)", fontSize: 14, color: "var(--accent)" } }, lang === "zh" ? "\u677E\u5F00\u53D1\u9001\u6587\u4EF6" : "Drop to send file"),
|
|
1334
|
-
/* @__PURE__ */ React.createElement("div", { style: { height: 60, flexShrink: 0, borderBottom: "1px solid var(--line)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px", background: "var(--panel)" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer, size: 34, radius: 8 }), /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: peer.alias ? "var(--ui)" : "var(--mono)", fontSize: 15, fontWeight: 700, color: "var(--text)" } }, peer.alias || shortKey(peer.userId, 10, 6)), peer.agent && /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, "agent"), /* @__PURE__ */ React.createElement(RouteTag, { peer })), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 2 } }, /* @__PURE__ */ React.createElement(Mono, { size: 11.5, dim: true, copy: peer.userId, title: peer.userId }, shortKey(peer.userId, 10, 6)), /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)" } }, "\xB7"), /* @__PURE__ */ React.createElement("button", { onClick: () => onOpenNet(peer), style: { background: "none", border: "none", cursor: "pointer", padding: 0, fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--accent)", display: "inline-flex", alignItems: "center", gap: 4 } }, /* @__PURE__ */ React.createElement(Icon, { name: "network", size: 12, stroke: 2 }), " ", peer.ip))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(Btn, { icon: "more", onClick: () => setMenu((v) => !v) }), menu && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { onClick: () => setMenu(false), style: { position: "fixed", inset: 0, zIndex: 40 } }), /* @__PURE__ */ React.createElement("div", { style: { position: "absolute", right: 0, top: 36, zIndex: 50, width: 180, background: "var(--panel-2)", border: "1px solid var(--line)", borderRadius: 9, padding: 6, boxShadow: "0 14px 40px rgba(0,0,0,0.4)" } }, /* @__PURE__ */ React.createElement(MenuItem, { icon: "hash", label: T.alias, onClick: () => {
|
|
1334
|
+
/* @__PURE__ */ React.createElement("div", { style: { height: 60, flexShrink: 0, borderBottom: "1px solid var(--line)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px", background: "var(--panel)" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer, size: 34, radius: 8 }), /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: peer.alias ? "var(--ui)" : "var(--mono)", fontSize: 15, fontWeight: 700, color: "var(--text)" } }, peer.alias || shortKey(peer.userId, 10, 6)), peer.agent && /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, "agent"), /* @__PURE__ */ React.createElement(RouteTag, { peer })), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 2 } }, /* @__PURE__ */ React.createElement(Mono, { size: 11.5, dim: true, copy: peer.userId, title: peer.userId }, shortKey(peer.userId, 10, 6)), /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)" } }, "\xB7"), /* @__PURE__ */ React.createElement("button", { onClick: () => onOpenNet(peer), style: { background: "none", border: "none", cursor: "pointer", padding: 0, fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--accent)", display: "inline-flex", alignItems: "center", gap: 4 } }, /* @__PURE__ */ React.createElement(Icon, { name: "network", size: 12, stroke: 2 }), " ", peer.ip))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), onCall && /* @__PURE__ */ React.createElement(Btn, { icon: "phone", title: T && T.audioCall || "Audio call", onClick: () => onCall(peer.userId, false) }), onCall && /* @__PURE__ */ React.createElement(Btn, { icon: "video", title: T && T.videoCall || "Video call", onClick: () => onCall(peer.userId, true) }), /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(Btn, { icon: "more", onClick: () => setMenu((v) => !v) }), menu && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { onClick: () => setMenu(false), style: { position: "fixed", inset: 0, zIndex: 40 } }), /* @__PURE__ */ React.createElement("div", { style: { position: "absolute", right: 0, top: 36, zIndex: 50, width: 180, background: "var(--panel-2)", border: "1px solid var(--line)", borderRadius: 9, padding: 6, boxShadow: "0 14px 40px rgba(0,0,0,0.4)" } }, /* @__PURE__ */ React.createElement(MenuItem, { icon: "hash", label: T.alias, onClick: () => {
|
|
1335
1335
|
setMenu(false);
|
|
1336
1336
|
onAlias(peer);
|
|
1337
1337
|
} }), /* @__PURE__ */ React.createElement(MenuItem, { icon: "check", label: T.selectDelete || "Select & delete", onClick: () => {
|
|
@@ -1455,9 +1455,9 @@ function MenuItem({ icon, label, onClick, danger }) {
|
|
|
1455
1455
|
function ChatEmpty({ T }) {
|
|
1456
1456
|
return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 12, color: "var(--faint)", background: "var(--bg)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "message", size: 40, stroke: 1.4, color: "var(--line)" }), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13 } }, T.pickPeer));
|
|
1457
1457
|
}
|
|
1458
|
-
function ChatTab({ T, lang, peers, requests, activeId, thread, onSelect, onAct, onAdd, onSend, onSendFile, onAlias, onRemove, onOpenNet }) {
|
|
1458
|
+
function ChatTab({ T, lang, peers, requests, activeId, thread, onSelect, onAct, onAdd, onSend, onSendFile, onAlias, onRemove, onOpenNet, onCall }) {
|
|
1459
1459
|
const peer = peers.find((p) => p.id === activeId);
|
|
1460
|
-
return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", minWidth: 0, minHeight: 0 } }, /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct, onAdd }), peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, thread, onSend, onSendFile, onAlias, onRemove, onOpenNet }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
|
|
1460
|
+
return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", minWidth: 0, minHeight: 0 } }, /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct, onAdd }), peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, thread, onSend, onSendFile, onAlias, onRemove, onOpenNet, onCall }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
|
|
1461
1461
|
}
|
|
1462
1462
|
Object.assign(window, { ChatTab });
|
|
1463
1463
|
function StatTile({ label, value, sub, tone }) {
|
|
@@ -1540,6 +1540,209 @@ function ProfileTab({ T, me, onEdit }) {
|
|
|
1540
1540
|
} }));
|
|
1541
1541
|
}
|
|
1542
1542
|
Object.assign(window, { ProfileTab });
|
|
1543
|
+
function makeDaemonSignaling() {
|
|
1544
|
+
const PW = window.PeerWebRTC;
|
|
1545
|
+
let handler = null;
|
|
1546
|
+
let stopped = false;
|
|
1547
|
+
async function pollLoop() {
|
|
1548
|
+
while (!stopped) {
|
|
1549
|
+
let signals = [];
|
|
1550
|
+
try {
|
|
1551
|
+
const r = await fetch("/api/call-poll", { headers: { "cache-control": "no-cache" } });
|
|
1552
|
+
const d = await r.json();
|
|
1553
|
+
signals = d && d.signals || [];
|
|
1554
|
+
} catch (e) {
|
|
1555
|
+
await new Promise((res) => setTimeout(res, 1e3));
|
|
1556
|
+
continue;
|
|
1557
|
+
}
|
|
1558
|
+
for (const s of signals) {
|
|
1559
|
+
try {
|
|
1560
|
+
const signal = PW.decodeSignal(s.data);
|
|
1561
|
+
handler && handler(s.userid, signal);
|
|
1562
|
+
} catch (e) {
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
return {
|
|
1568
|
+
async send(peerId, signal) {
|
|
1569
|
+
const body = JSON.stringify({ userid: peerId, data: PW.encodeSignal(signal) });
|
|
1570
|
+
const r = await fetch("/api/call-signal", {
|
|
1571
|
+
method: "POST",
|
|
1572
|
+
headers: { "content-type": "application/json" },
|
|
1573
|
+
body
|
|
1574
|
+
});
|
|
1575
|
+
const d = await r.json().catch(() => ({ ok: r.ok }));
|
|
1576
|
+
if (!d.ok) throw new Error(d.error || "call-signal send failed");
|
|
1577
|
+
},
|
|
1578
|
+
onSignal(cb) {
|
|
1579
|
+
handler = cb;
|
|
1580
|
+
if (stopped === false && !this._started) {
|
|
1581
|
+
this._started = true;
|
|
1582
|
+
pollLoop();
|
|
1583
|
+
}
|
|
1584
|
+
},
|
|
1585
|
+
stop() {
|
|
1586
|
+
stopped = true;
|
|
1587
|
+
}
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
const CALL_ICE_SERVERS = [
|
|
1591
|
+
{ urls: "stun:stun.l.google.com:19302" },
|
|
1592
|
+
{ urls: "turn:tokyo.fi.chat:3478", username: "allcom", credential: "allcompass" }
|
|
1593
|
+
];
|
|
1594
|
+
function useCallController(selfId) {
|
|
1595
|
+
const [incoming, setIncoming] = React.useState(null);
|
|
1596
|
+
const [active, setActive] = React.useState(null);
|
|
1597
|
+
const [localStream, setLocalStream] = React.useState(null);
|
|
1598
|
+
const [remoteStream, setRemoteStream] = React.useState(null);
|
|
1599
|
+
const engineRef = React.useRef(null);
|
|
1600
|
+
React.useEffect(() => {
|
|
1601
|
+
if (!selfId || engineRef.current) return;
|
|
1602
|
+
if (!window.PeerWebRTC || !window.RTCPeerConnection) {
|
|
1603
|
+
console.warn("[call] WebRTC or peer-webrtc unavailable \u2014 calls disabled");
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
const { CallEngine } = window.PeerWebRTC;
|
|
1607
|
+
const signaling = makeDaemonSignaling();
|
|
1608
|
+
const engine = new CallEngine({
|
|
1609
|
+
selfId,
|
|
1610
|
+
signaling,
|
|
1611
|
+
createPeerConnection: (c) => new RTCPeerConnection(c),
|
|
1612
|
+
getLocalMedia: (k) => navigator.mediaDevices.getUserMedia({ audio: k.audio, video: k.video }),
|
|
1613
|
+
iceServers: CALL_ICE_SERVERS,
|
|
1614
|
+
logger: (m) => console.log(m)
|
|
1615
|
+
});
|
|
1616
|
+
engine.on("incomingCall", (info) => setIncoming(info));
|
|
1617
|
+
engine.on("stateChanged", (info, state) => setActive((a) => a && a.callId === info.callId ? { ...a, state } : a));
|
|
1618
|
+
engine.on("localStream", (id, s) => setLocalStream(s));
|
|
1619
|
+
engine.on("remoteStream", (id, s) => setRemoteStream(s));
|
|
1620
|
+
engine.on("ended", (id) => {
|
|
1621
|
+
setActive((a) => a && a.callId === id ? null : a);
|
|
1622
|
+
setIncoming((i) => i && i.callId === id ? null : i);
|
|
1623
|
+
setLocalStream(null);
|
|
1624
|
+
setRemoteStream(null);
|
|
1625
|
+
});
|
|
1626
|
+
engineRef.current = engine;
|
|
1627
|
+
return () => {
|
|
1628
|
+
try {
|
|
1629
|
+
signaling.stop();
|
|
1630
|
+
engine.dispose();
|
|
1631
|
+
} catch (e) {
|
|
1632
|
+
}
|
|
1633
|
+
engineRef.current = null;
|
|
1634
|
+
};
|
|
1635
|
+
}, [selfId]);
|
|
1636
|
+
const start = React.useCallback(async (peerId, video) => {
|
|
1637
|
+
const eng = engineRef.current;
|
|
1638
|
+
if (!eng) {
|
|
1639
|
+
alert("Calls unavailable (WebRTC not supported here)");
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
try {
|
|
1643
|
+
const callId = await eng.call(peerId, { audio: true, video: !!video });
|
|
1644
|
+
setActive({ callId, peerId, video: !!video, direction: "outgoing", state: "ringing" });
|
|
1645
|
+
} catch (e) {
|
|
1646
|
+
alert("Could not start call: " + (e && e.message || e));
|
|
1647
|
+
}
|
|
1648
|
+
}, []);
|
|
1649
|
+
const accept = React.useCallback(async () => {
|
|
1650
|
+
const eng = engineRef.current;
|
|
1651
|
+
setIncoming((inc) => {
|
|
1652
|
+
if (eng && inc) {
|
|
1653
|
+
setActive({ callId: inc.callId, peerId: inc.peerId, video: inc.video, direction: "incoming", state: "connecting" });
|
|
1654
|
+
eng.accept(inc.callId).catch((e) => alert("Accept failed: " + (e && e.message || e)));
|
|
1655
|
+
}
|
|
1656
|
+
return null;
|
|
1657
|
+
});
|
|
1658
|
+
}, []);
|
|
1659
|
+
const reject = React.useCallback(() => {
|
|
1660
|
+
const eng = engineRef.current;
|
|
1661
|
+
setIncoming((inc) => {
|
|
1662
|
+
if (eng && inc) eng.reject(inc.callId);
|
|
1663
|
+
return null;
|
|
1664
|
+
});
|
|
1665
|
+
}, []);
|
|
1666
|
+
const hangup = React.useCallback(() => {
|
|
1667
|
+
const eng = engineRef.current;
|
|
1668
|
+
setActive((a) => {
|
|
1669
|
+
if (eng && a) eng.hangup(a.callId);
|
|
1670
|
+
return null;
|
|
1671
|
+
});
|
|
1672
|
+
setLocalStream(null);
|
|
1673
|
+
setRemoteStream(null);
|
|
1674
|
+
}, []);
|
|
1675
|
+
const setMuted = React.useCallback((m) => {
|
|
1676
|
+
const eng = engineRef.current, a = active;
|
|
1677
|
+
if (eng && a) eng.setLocalTrackEnabled(a.callId, "audio", !m);
|
|
1678
|
+
}, [active]);
|
|
1679
|
+
const setVideoOn = React.useCallback((v) => {
|
|
1680
|
+
const eng = engineRef.current, a = active;
|
|
1681
|
+
if (eng && a) eng.setLocalTrackEnabled(a.callId, "video", v);
|
|
1682
|
+
}, [active]);
|
|
1683
|
+
return { incoming, active, localStream, remoteStream, start, accept, reject, hangup, setMuted, setVideoOn };
|
|
1684
|
+
}
|
|
1685
|
+
function VideoSurface({ stream, muted, style }) {
|
|
1686
|
+
const ref = React.useCallback((el) => {
|
|
1687
|
+
if (el && el.srcObject !== (stream || null)) el.srcObject = stream || null;
|
|
1688
|
+
}, [stream]);
|
|
1689
|
+
return /* @__PURE__ */ React.createElement("video", { ref, autoPlay: true, playsInline: true, muted: !!muted, style });
|
|
1690
|
+
}
|
|
1691
|
+
function IncomingCallModal({ T, ctl, peers }) {
|
|
1692
|
+
const inc = ctl.incoming;
|
|
1693
|
+
if (!inc) return null;
|
|
1694
|
+
const peer = (peers || []).find((p) => p.id === inc.peerId) || { id: inc.peerId };
|
|
1695
|
+
const name = peer.alias || peer.name || shortKey(inc.peerId, 10, 6);
|
|
1696
|
+
return /* @__PURE__ */ React.createElement("div", { style: { position: "fixed", inset: 0, zIndex: 80, background: "color-mix(in oklab, var(--bg), transparent 15%)", display: "flex", alignItems: "center", justifyContent: "center" } }, /* @__PURE__ */ React.createElement("div", { style: { width: 320, padding: 26, borderRadius: 18, background: "var(--panel)", border: "1px solid var(--line)", textAlign: "center", boxShadow: "0 20px 60px rgba(0,0,0,0.4)" } }, /* @__PURE__ */ React.createElement("div", { style: { position: "relative", display: "inline-flex", alignItems: "center", justifyContent: "center", marginBottom: 14 } }, /* @__PURE__ */ React.createElement("span", { style: { position: "absolute", width: 96, height: 96, borderRadius: 999, border: "2px solid var(--accent)", animation: "dkpulse 2s ease-out infinite" } }), /* @__PURE__ */ React.createElement(DkIdenticon, { seed: inc.peerId, size: 72, radius: 18 })), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 17, fontWeight: 700, color: "var(--text)" } }, name), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 12.5, color: "var(--faint)", marginTop: 4, display: "flex", gap: 6, alignItems: "center", justifyContent: "center" } }, /* @__PURE__ */ React.createElement(Icon, { name: inc.video ? "video" : "phone", size: 14, stroke: 2 }), T && (inc.video ? T.incomingVideo : T.incomingAudio) || "Incoming " + (inc.video ? "video" : "audio") + " call"), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 14, justifyContent: "center", marginTop: 22 } }, /* @__PURE__ */ React.createElement(CallBtn, { icon: "phone", label: T && T.decline || "Decline", danger: true, onClick: ctl.reject }), /* @__PURE__ */ React.createElement(CallBtn, { icon: inc.video ? "video" : "phone", label: T && T.accept || "Accept", active: true, onClick: ctl.accept }))));
|
|
1697
|
+
}
|
|
1698
|
+
function CallBtn({ icon, label, onClick, active, danger, wide }) {
|
|
1699
|
+
const bg = danger ? "var(--danger)" : active ? "var(--accent)" : "var(--chip)";
|
|
1700
|
+
const fg = danger || active ? "#fff" : "var(--text)";
|
|
1701
|
+
return /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement("button", { onClick, style: {
|
|
1702
|
+
width: wide ? 76 : 56,
|
|
1703
|
+
height: 56,
|
|
1704
|
+
borderRadius: wide ? 18 : 999,
|
|
1705
|
+
cursor: "pointer",
|
|
1706
|
+
background: bg,
|
|
1707
|
+
color: fg,
|
|
1708
|
+
border: "1px solid " + (active || danger ? "transparent" : "var(--line)"),
|
|
1709
|
+
display: "flex",
|
|
1710
|
+
alignItems: "center",
|
|
1711
|
+
justifyContent: "center"
|
|
1712
|
+
} }, /* @__PURE__ */ React.createElement(Icon, { name: icon, size: 22, stroke: 2 })), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10.5, color: "var(--faint)" } }, label));
|
|
1713
|
+
}
|
|
1714
|
+
function CallOverlay({ T, ctl, peers }) {
|
|
1715
|
+
const call = ctl.active;
|
|
1716
|
+
const [secs, setSecs] = React.useState(0);
|
|
1717
|
+
const [muted, setMuted] = React.useState(false);
|
|
1718
|
+
const connected = call && call.state === "connected";
|
|
1719
|
+
React.useEffect(() => {
|
|
1720
|
+
if (!connected) return;
|
|
1721
|
+
const id = setInterval(() => setSecs((s) => s + 1), 1e3);
|
|
1722
|
+
return () => clearInterval(id);
|
|
1723
|
+
}, [connected]);
|
|
1724
|
+
React.useEffect(() => {
|
|
1725
|
+
if (!call) {
|
|
1726
|
+
setSecs(0);
|
|
1727
|
+
setMuted(false);
|
|
1728
|
+
}
|
|
1729
|
+
}, [call]);
|
|
1730
|
+
if (!call) return null;
|
|
1731
|
+
const peer = (peers || []).find((p) => p.id === call.peerId) || { id: call.peerId };
|
|
1732
|
+
const name = peer.alias || peer.name || shortKey(call.peerId, 10, 6);
|
|
1733
|
+
const video = call.video;
|
|
1734
|
+
const mm = String(Math.floor(secs / 60)).padStart(2, "0");
|
|
1735
|
+
const ss = String(secs % 60).padStart(2, "0");
|
|
1736
|
+
const stateLabel = connected ? mm + ":" + ss : call.state === "ringing" ? T && T.ringing || "ringing\u2026" : call.state === "connecting" ? T && T.connecting || "connecting\u2026" : call.state;
|
|
1737
|
+
const toggleMute = () => {
|
|
1738
|
+
const n = !muted;
|
|
1739
|
+
setMuted(n);
|
|
1740
|
+
ctl.setMuted(n);
|
|
1741
|
+
};
|
|
1742
|
+
const controls = /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 18, alignItems: "flex-start" } }, /* @__PURE__ */ React.createElement(CallBtn, { icon: muted ? "micFill" : "mic", label: muted ? T && T.unmute || "unmute" : T && T.mute || "mute", active: muted, onClick: toggleMute }), /* @__PURE__ */ React.createElement(CallBtn, { icon: "phone", label: T && T.endCall || "end", danger: true, wide: true, onClick: ctl.hangup }));
|
|
1743
|
+
return /* @__PURE__ */ React.createElement("div", { style: { position: "fixed", inset: 0, zIndex: 70, background: "var(--bg)", display: "flex", flexDirection: "column" } }, /* @__PURE__ */ React.createElement("div", { style: { height: 52, flexShrink: 0, display: "flex", alignItems: "center", gap: 10, padding: "0 18px", borderBottom: "1px solid var(--line)" } }, /* @__PURE__ */ React.createElement(Icon, { name: video ? "video" : "phone", size: 16, color: "var(--accent)", stroke: 2 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 13, fontWeight: 700, color: "var(--text)" } }, name), /* @__PURE__ */ React.createElement(Tag, { tone: connected ? "ok" : "warn" }, stateLabel), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), connected && /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700, color: "var(--text)", letterSpacing: 1 } }, mm, ":", ss)), video ? /* @__PURE__ */ React.createElement("div", { style: { flex: 1, position: "relative", padding: 18 } }, /* @__PURE__ */ React.createElement("div", { style: { position: "absolute", inset: 18, borderRadius: 14, overflow: "hidden", background: "#000", border: "1px solid var(--line)" } }, ctl.remoteStream ? /* @__PURE__ */ React.createElement(VideoSurface, { stream: ctl.remoteStream, style: { width: "100%", height: "100%", objectFit: "cover" } }) : /* @__PURE__ */ React.createElement("div", { style: { width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center" } }, /* @__PURE__ */ React.createElement(DkIdenticon, { seed: call.peerId, size: 92, radius: 22 }))), /* @__PURE__ */ React.createElement("div", { style: { position: "absolute", right: 30, bottom: 30, width: 220, height: 148, borderRadius: 12, overflow: "hidden", background: "#000", border: "1px solid var(--line)" } }, ctl.localStream ? /* @__PURE__ */ React.createElement(VideoSurface, { stream: ctl.localStream, muted: true, style: { width: "100%", height: "100%", objectFit: "cover", transform: "scaleX(-1)" } }) : /* @__PURE__ */ React.createElement("div", { style: { width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center" } }, /* @__PURE__ */ React.createElement(Icon, { name: "userRound", size: 30, color: "var(--faint)", stroke: 1.4 }))), /* @__PURE__ */ React.createElement("div", { style: { position: "absolute", left: 0, right: 0, bottom: 30, display: "flex", justifyContent: "center" } }, /* @__PURE__ */ React.createElement("div", { style: { padding: "16px 26px", borderRadius: 20, background: "color-mix(in oklab, var(--panel), transparent 8%)", border: "1px solid var(--line)" } }, controls))) : /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 22 } }, ctl.remoteStream && /* @__PURE__ */ React.createElement(VideoSurface, { stream: ctl.remoteStream, style: { display: "none" } }), /* @__PURE__ */ React.createElement("div", { style: { position: "relative", display: "flex", alignItems: "center", justifyContent: "center" } }, !connected && /* @__PURE__ */ React.createElement("span", { style: { position: "absolute", width: 150, height: 150, borderRadius: 999, border: "2px solid var(--accent)", animation: "dkpulse 2s ease-out infinite" } }), /* @__PURE__ */ React.createElement(DkIdenticon, { seed: call.peerId, size: 108, radius: 26 })), /* @__PURE__ */ React.createElement("div", { style: { textAlign: "center" } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 22, fontWeight: 700, color: "var(--text)" } }, name), /* @__PURE__ */ React.createElement("div", { style: { marginTop: 8, fontFamily: "var(--mono)", fontSize: 12.5, color: "var(--faint)" } }, stateLabel)), /* @__PURE__ */ React.createElement("div", { style: { marginTop: 18 } }, controls)));
|
|
1744
|
+
}
|
|
1745
|
+
Object.assign(window, { useCallController, CallOverlay, IncomingCallModal });
|
|
1543
1746
|
const DK_DEFAULTS = (
|
|
1544
1747
|
/*EDITMODE-BEGIN*/
|
|
1545
1748
|
{
|
|
@@ -1781,6 +1984,8 @@ function DkApp() {
|
|
|
1781
1984
|
const T = STR[t.lang] || STR.en;
|
|
1782
1985
|
const vars = dkTheme(t.theme, t.accent);
|
|
1783
1986
|
const rowPad = t.density === "comfortable" ? "11px 12px" : "7px 10px";
|
|
1987
|
+
const callCtl = useCallController(me.userId);
|
|
1988
|
+
const onCall = (peerId, video) => callCtl.start(peerId, !!video);
|
|
1784
1989
|
React.useEffect(() => {
|
|
1785
1990
|
if (!activeId && peers.length) setActiveId(peers[0].id);
|
|
1786
1991
|
}, [peers, activeId]);
|
|
@@ -1840,7 +2045,7 @@ function DkApp() {
|
|
|
1840
2045
|
{ id: "network", icon: "network", label: T.network },
|
|
1841
2046
|
{ id: "profile", icon: "userRound", label: T.profile }
|
|
1842
2047
|
];
|
|
1843
|
-
return /* @__PURE__ */ React.createElement("div", { style: { ...vars, "--row-pad": rowPad, position: "fixed", inset: 0, display: "flex", background: "var(--bg)", color: "var(--text)", fontFamily: "var(--ui)" } }, /* @__PURE__ */ React.createElement("div", { style: { width: 68, flexShrink: 0, borderRight: "1px solid var(--line)", background: "var(--rail)", display: "flex", flexDirection: "column", alignItems: "center", padding: "14px 0", gap: 8 } }, /* @__PURE__ */ React.createElement("div", { style: { width: 38, height: 38, borderRadius: 10, background: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 8 } }, /* @__PURE__ */ React.createElement(Icon, { name: "terminal", size: 20, color: "#fff", stroke: 2.2 })), nav.map((n) => /* @__PURE__ */ React.createElement(RailBtn, { key: n.id, icon: n.icon, label: n.label, active: tab === n.id, soon: n.soon, onClick: () => setTab(n.id) })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 36, radius: 9 }))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, minHeight: 0, display: "flex", flexDirection: "column" } }, /* @__PURE__ */ React.createElement("div", { style: { height: 46, flexShrink: 0, borderBottom: "1px solid var(--line)", background: "var(--panel)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700, letterSpacing: -0.3, color: "var(--text)" } }, "beagle"), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, "\xB7 ", nav.find((n) => n.id === tab).label.toLowerCase()), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, me.channel, " \xB7 lan ", me.lanVer), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 7, padding: "0 4px" } }, /* @__PURE__ */ React.createElement(StatusDot, { online: me.online }), /* @__PURE__ */ React.createElement(Mono, { size: 12.5, copy: me.ip }, me.ip)), /* @__PURE__ */ React.createElement("span", { style: { width: 1, height: 22, background: "var(--line)" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 26, radius: 7 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: "var(--text)" } }, me.name))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, peers, requests, activeId, thread: data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onAlias, onRemove, onOpenNet }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, reqCount: requests.length, onSetExit, onOpenChat }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me, onEdit })), /* @__PURE__ */ React.createElement(TweaksPanel, null, /* @__PURE__ */ React.createElement(TweakSection, { label: t.lang === "zh" ? "\u5916\u89C2" : "Appearance" }), /* @__PURE__ */ React.createElement(
|
|
2048
|
+
return /* @__PURE__ */ React.createElement("div", { style: { ...vars, "--row-pad": rowPad, position: "fixed", inset: 0, display: "flex", background: "var(--bg)", color: "var(--text)", fontFamily: "var(--ui)" } }, /* @__PURE__ */ React.createElement("div", { style: { width: 68, flexShrink: 0, borderRight: "1px solid var(--line)", background: "var(--rail)", display: "flex", flexDirection: "column", alignItems: "center", padding: "14px 0", gap: 8 } }, /* @__PURE__ */ React.createElement("div", { style: { width: 38, height: 38, borderRadius: 10, background: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 8 } }, /* @__PURE__ */ React.createElement(Icon, { name: "terminal", size: 20, color: "#fff", stroke: 2.2 })), nav.map((n) => /* @__PURE__ */ React.createElement(RailBtn, { key: n.id, icon: n.icon, label: n.label, active: tab === n.id, soon: n.soon, onClick: () => setTab(n.id) })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 36, radius: 9 }))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, minHeight: 0, display: "flex", flexDirection: "column" } }, /* @__PURE__ */ React.createElement("div", { style: { height: 46, flexShrink: 0, borderBottom: "1px solid var(--line)", background: "var(--panel)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700, letterSpacing: -0.3, color: "var(--text)" } }, "beagle"), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, "\xB7 ", nav.find((n) => n.id === tab).label.toLowerCase()), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, me.channel, " \xB7 lan ", me.lanVer), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 7, padding: "0 4px" } }, /* @__PURE__ */ React.createElement(StatusDot, { online: me.online }), /* @__PURE__ */ React.createElement(Mono, { size: 12.5, copy: me.ip }, me.ip)), /* @__PURE__ */ React.createElement("span", { style: { width: 1, height: 22, background: "var(--line)" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 26, radius: 7 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: "var(--text)" } }, me.name))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, peers, requests, activeId, thread: data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onAlias, onRemove, onOpenNet, onCall }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, reqCount: requests.length, onSetExit, onOpenChat }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me, onEdit })), /* @__PURE__ */ React.createElement(TweaksPanel, null, /* @__PURE__ */ React.createElement(TweakSection, { label: t.lang === "zh" ? "\u5916\u89C2" : "Appearance" }), /* @__PURE__ */ React.createElement(
|
|
1844
2049
|
TweakRadio,
|
|
1845
2050
|
{
|
|
1846
2051
|
label: t.lang === "zh" ? "\u4E3B\u9898" : "Theme",
|
|
@@ -1883,6 +2088,6 @@ function DkApp() {
|
|
|
1883
2088
|
color: "#fff",
|
|
1884
2089
|
fontSize: 12,
|
|
1885
2090
|
fontWeight: 600
|
|
1886
|
-
} }, n.label)))));
|
|
2091
|
+
} }, n.label)))), /* @__PURE__ */ React.createElement(IncomingCallModal, { T, ctl: callCtl, peers }), /* @__PURE__ */ React.createElement(CallOverlay, { T, ctl: callCtl, peers }));
|
|
1887
2092
|
}
|
|
1888
2093
|
ReactDOM.createRoot(document.getElementById("root")).render(/* @__PURE__ */ React.createElement(DkApp, null));
|
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
<script src="vendor/react-dom.production.min.js"></script>
|
|
33
33
|
<!-- QR encoder (Kazuhiko Arase, MIT) — exposes window.qrcode for the QR-code button. -->
|
|
34
34
|
<script src="vendor/qrcode.js"></script>
|
|
35
|
+
<!-- peer-webrtc call engine (bundled IIFE) — exposes window.PeerWebRTC. -->
|
|
36
|
+
<script src="vendor/peer-webrtc.js"></script>
|
|
35
37
|
<!-- The whole desktop app, esbuild-transpiled from src/ui/desktop/*.jsx. -->
|
|
36
38
|
<script src="app.js"></script>
|
|
37
39
|
</body>
|
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
var PeerWebRTC = (() => {
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __typeError = (msg) => {
|
|
7
|
+
throw TypeError(msg);
|
|
8
|
+
};
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
22
|
+
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
23
|
+
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
24
|
+
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
|
25
|
+
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
26
|
+
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
|
27
|
+
|
|
28
|
+
// node_modules/@decentnetwork/peer-webrtc/dist/index.js
|
|
29
|
+
var dist_exports = {};
|
|
30
|
+
__export(dist_exports, {
|
|
31
|
+
BroadcastChannelSignaling: () => BroadcastChannelSignaling,
|
|
32
|
+
CallEngine: () => CallEngine,
|
|
33
|
+
CarrierSignaling: () => CarrierSignaling,
|
|
34
|
+
Emitter: () => Emitter,
|
|
35
|
+
decodeSignal: () => decodeSignal,
|
|
36
|
+
decodeSignalBytesGuard: () => decodeSignalBytesGuard,
|
|
37
|
+
encodeSignal: () => encodeSignal,
|
|
38
|
+
encodeSignalBytes: () => encodeSignalBytes,
|
|
39
|
+
looksLikeSignal: () => looksLikeSignal
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// node_modules/@decentnetwork/peer-webrtc/dist/emitter.js
|
|
43
|
+
var _handlers;
|
|
44
|
+
var Emitter = class {
|
|
45
|
+
constructor() {
|
|
46
|
+
__privateAdd(this, _handlers, {});
|
|
47
|
+
}
|
|
48
|
+
on(event, handler) {
|
|
49
|
+
var _a, _b;
|
|
50
|
+
((_b = (_a = __privateGet(this, _handlers))[event]) != null ? _b : _a[event] = /* @__PURE__ */ new Set()).add(handler);
|
|
51
|
+
return () => this.off(event, handler);
|
|
52
|
+
}
|
|
53
|
+
off(event, handler) {
|
|
54
|
+
var _a;
|
|
55
|
+
(_a = __privateGet(this, _handlers)[event]) == null ? void 0 : _a.delete(handler);
|
|
56
|
+
}
|
|
57
|
+
once(event, handler) {
|
|
58
|
+
const wrapped = (...args) => {
|
|
59
|
+
this.off(event, wrapped);
|
|
60
|
+
handler(...args);
|
|
61
|
+
};
|
|
62
|
+
return this.on(event, wrapped);
|
|
63
|
+
}
|
|
64
|
+
emit(event, ...args) {
|
|
65
|
+
const set = __privateGet(this, _handlers)[event];
|
|
66
|
+
if (!set)
|
|
67
|
+
return;
|
|
68
|
+
for (const handler of [...set]) {
|
|
69
|
+
handler(...args);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
removeAll() {
|
|
73
|
+
__privateSet(this, _handlers, {});
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
_handlers = new WeakMap();
|
|
77
|
+
|
|
78
|
+
// node_modules/@decentnetwork/peer-webrtc/dist/signal.js
|
|
79
|
+
var SDP_TYPES = /* @__PURE__ */ new Set([
|
|
80
|
+
"offer",
|
|
81
|
+
"answer",
|
|
82
|
+
"candidate",
|
|
83
|
+
"remove-candidates",
|
|
84
|
+
"prAnswer",
|
|
85
|
+
"bye",
|
|
86
|
+
"action",
|
|
87
|
+
"event"
|
|
88
|
+
]);
|
|
89
|
+
function encodeSignal(signal) {
|
|
90
|
+
const out = { type: signal.type };
|
|
91
|
+
if (signal.sdp !== void 0)
|
|
92
|
+
out.sdp = signal.sdp;
|
|
93
|
+
if (signal.candidates !== void 0) {
|
|
94
|
+
out.candidates = signal.candidates.map((c) => {
|
|
95
|
+
var _a;
|
|
96
|
+
const o = { sdp: c.sdp, sdpMLineIndex: c.sdpMLineIndex };
|
|
97
|
+
o.sdpMid = (_a = c.sdpMid) != null ? _a : null;
|
|
98
|
+
return o;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (signal.reason !== void 0)
|
|
102
|
+
out.reason = signal.reason;
|
|
103
|
+
if (signal.options !== void 0)
|
|
104
|
+
out.options = signal.options;
|
|
105
|
+
if (signal.action !== void 0)
|
|
106
|
+
out.action = signal.action;
|
|
107
|
+
out.callId = signal.callId;
|
|
108
|
+
if (signal.event !== void 0)
|
|
109
|
+
out.event = signal.event;
|
|
110
|
+
return JSON.stringify(out);
|
|
111
|
+
}
|
|
112
|
+
function encodeSignalBytes(signal) {
|
|
113
|
+
return new TextEncoder().encode(encodeSignal(signal));
|
|
114
|
+
}
|
|
115
|
+
function decodeSignal(input) {
|
|
116
|
+
const text = (typeof input === "string" ? input : new TextDecoder().decode(input)).replace(/\0+$/u, "").trim();
|
|
117
|
+
const obj = JSON.parse(text);
|
|
118
|
+
if (typeof obj.type !== "string" || !SDP_TYPES.has(obj.type)) {
|
|
119
|
+
throw new Error(`RtcSignal: invalid or missing "type" (${String(obj.type)})`);
|
|
120
|
+
}
|
|
121
|
+
if (typeof obj.callId !== "string" || obj.callId.length === 0) {
|
|
122
|
+
throw new Error('RtcSignal: missing "callId"');
|
|
123
|
+
}
|
|
124
|
+
const signal = { type: obj.type, callId: obj.callId };
|
|
125
|
+
if (typeof obj.sdp === "string")
|
|
126
|
+
signal.sdp = obj.sdp;
|
|
127
|
+
if (Array.isArray(obj.candidates)) {
|
|
128
|
+
signal.candidates = obj.candidates.filter((c) => c && typeof c.sdp === "string").map((c) => ({
|
|
129
|
+
sdp: c.sdp,
|
|
130
|
+
sdpMLineIndex: typeof c.sdpMLineIndex === "number" ? c.sdpMLineIndex : 0,
|
|
131
|
+
sdpMid: typeof c.sdpMid === "string" ? c.sdpMid : null
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
if (typeof obj.reason === "string")
|
|
135
|
+
signal.reason = obj.reason;
|
|
136
|
+
if (Array.isArray(obj.options)) {
|
|
137
|
+
signal.options = obj.options.filter((o) => o === "audio" || o === "video" || o === "data");
|
|
138
|
+
}
|
|
139
|
+
if (obj.action === "accept" || obj.action === "reject")
|
|
140
|
+
signal.action = obj.action;
|
|
141
|
+
if (typeof obj.event === "string")
|
|
142
|
+
signal.event = obj.event;
|
|
143
|
+
return signal;
|
|
144
|
+
}
|
|
145
|
+
function looksLikeSignal(input) {
|
|
146
|
+
const text = (typeof input === "string" ? input : new TextDecoder().decode(input)).replace(/\0+$/u, "").trimStart();
|
|
147
|
+
return text.startsWith("{") && text.includes('"callId"') && text.includes('"type"');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// node_modules/@decentnetwork/peer-webrtc/dist/call-engine.js
|
|
151
|
+
var DEFAULT_ICE_SERVERS = [
|
|
152
|
+
{ urls: "stun:stun.l.google.com:19302" }
|
|
153
|
+
];
|
|
154
|
+
var _opts, _iceServers, _sessions, _CallEngine_instances, handleSignal_fn, onOffer_fn, onAnswer_fn, onCandidate_fn, onBye_fn, onAuxiliary_fn, createSession_fn, wirePeerConnection_fn, ensureRemoteStream_fn, attachLocalMedia_fn, flushLocalCandidates_fn, drainRemoteCandidates_fn, send_fn, sendByeSafe_fn, cleanup_fn, setState_fn, _toInfo, newCallId_fn, log_fn;
|
|
155
|
+
var CallEngine = class extends Emitter {
|
|
156
|
+
constructor(opts) {
|
|
157
|
+
var _a;
|
|
158
|
+
super();
|
|
159
|
+
__privateAdd(this, _CallEngine_instances);
|
|
160
|
+
__privateAdd(this, _opts);
|
|
161
|
+
__privateAdd(this, _iceServers);
|
|
162
|
+
__privateAdd(this, _sessions, /* @__PURE__ */ new Map());
|
|
163
|
+
__privateAdd(this, _toInfo, (session) => ({
|
|
164
|
+
callId: session.callId,
|
|
165
|
+
peerId: session.peerId,
|
|
166
|
+
direction: session.direction,
|
|
167
|
+
audio: session.audio,
|
|
168
|
+
video: session.video,
|
|
169
|
+
data: session.data,
|
|
170
|
+
state: session.state
|
|
171
|
+
}));
|
|
172
|
+
__privateSet(this, _opts, opts);
|
|
173
|
+
__privateSet(this, _iceServers, (_a = opts.iceServers) != null ? _a : DEFAULT_ICE_SERVERS);
|
|
174
|
+
opts.signaling.onSignal((peerId, signal) => {
|
|
175
|
+
try {
|
|
176
|
+
__privateMethod(this, _CallEngine_instances, handleSignal_fn).call(this, peerId, signal);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
__privateMethod(this, _CallEngine_instances, log_fn).call(this, `handleSignal error: ${err.message}`);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/** Calls currently tracked by the engine. */
|
|
183
|
+
get calls() {
|
|
184
|
+
return [...__privateGet(this, _sessions).values()].map(__privateGet(this, _toInfo));
|
|
185
|
+
}
|
|
186
|
+
/** True while any call is active (useful for busy-signalling). */
|
|
187
|
+
get isBusy() {
|
|
188
|
+
for (const s of __privateGet(this, _sessions).values()) {
|
|
189
|
+
if (!s.closed && s.state !== "ended" && s.state !== "failed")
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Place an outgoing call. Acquires local media, sends an offer, and returns
|
|
196
|
+
* the new callId. Emits "localStream" once media is up, "stateChanged" and
|
|
197
|
+
* "remoteStream"/"ended" as the call progresses.
|
|
198
|
+
*/
|
|
199
|
+
async call(peerId, kinds = {}) {
|
|
200
|
+
var _a, _b, _c;
|
|
201
|
+
const audio = (_a = kinds.audio) != null ? _a : true;
|
|
202
|
+
const video = (_b = kinds.video) != null ? _b : false;
|
|
203
|
+
const data = (_c = kinds.data) != null ? _c : false;
|
|
204
|
+
const callId = __privateMethod(this, _CallEngine_instances, newCallId_fn).call(this);
|
|
205
|
+
const session = __privateMethod(this, _CallEngine_instances, createSession_fn).call(this, callId, peerId, "outgoing", { audio, video, data });
|
|
206
|
+
await __privateMethod(this, _CallEngine_instances, attachLocalMedia_fn).call(this, session);
|
|
207
|
+
const offer = await session.pc.createOffer({
|
|
208
|
+
offerToReceiveAudio: audio,
|
|
209
|
+
offerToReceiveVideo: video
|
|
210
|
+
});
|
|
211
|
+
await session.pc.setLocalDescription(offer);
|
|
212
|
+
const options = [];
|
|
213
|
+
if (audio)
|
|
214
|
+
options.push("audio");
|
|
215
|
+
if (video)
|
|
216
|
+
options.push("video");
|
|
217
|
+
if (data)
|
|
218
|
+
options.push("data");
|
|
219
|
+
await __privateMethod(this, _CallEngine_instances, send_fn).call(this, peerId, {
|
|
220
|
+
type: "offer",
|
|
221
|
+
sdp: offer.sdp,
|
|
222
|
+
options,
|
|
223
|
+
callId,
|
|
224
|
+
event: void 0
|
|
225
|
+
});
|
|
226
|
+
__privateMethod(this, _CallEngine_instances, setState_fn).call(this, session, "ringing");
|
|
227
|
+
__privateMethod(this, _CallEngine_instances, log_fn).call(this, `call \u2192 ${peerId} callId=${callId} audio=${audio} video=${video}`);
|
|
228
|
+
return callId;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Accept an incoming call (one that surfaced via "incomingCall"). Acquires
|
|
232
|
+
* local media, applies the stored offer, and sends the answer.
|
|
233
|
+
*/
|
|
234
|
+
async accept(callId) {
|
|
235
|
+
const session = __privateGet(this, _sessions).get(callId);
|
|
236
|
+
if (!session || session.direction !== "incoming" || !session.pendingOffer) {
|
|
237
|
+
throw new Error(`accept: no pending incoming call ${callId}`);
|
|
238
|
+
}
|
|
239
|
+
await __privateMethod(this, _CallEngine_instances, attachLocalMedia_fn).call(this, session);
|
|
240
|
+
await session.pc.setRemoteDescription({ type: "offer", sdp: session.pendingOffer });
|
|
241
|
+
session.hasReceivedSdp = true;
|
|
242
|
+
session.pendingOffer = void 0;
|
|
243
|
+
await __privateMethod(this, _CallEngine_instances, drainRemoteCandidates_fn).call(this, session);
|
|
244
|
+
const answer = await session.pc.createAnswer();
|
|
245
|
+
await session.pc.setLocalDescription(answer);
|
|
246
|
+
await __privateMethod(this, _CallEngine_instances, send_fn).call(this, session.peerId, { type: "answer", sdp: answer.sdp, callId });
|
|
247
|
+
__privateMethod(this, _CallEngine_instances, setState_fn).call(this, session, "connecting");
|
|
248
|
+
await __privateMethod(this, _CallEngine_instances, flushLocalCandidates_fn).call(this, session);
|
|
249
|
+
__privateMethod(this, _CallEngine_instances, log_fn).call(this, `accept callId=${callId}`);
|
|
250
|
+
}
|
|
251
|
+
/** Reject an incoming call (sends bye/declined). */
|
|
252
|
+
async reject(callId) {
|
|
253
|
+
const session = __privateGet(this, _sessions).get(callId);
|
|
254
|
+
if (!session)
|
|
255
|
+
return;
|
|
256
|
+
await __privateMethod(this, _CallEngine_instances, sendByeSafe_fn).call(this, session, "declined");
|
|
257
|
+
__privateMethod(this, _CallEngine_instances, setState_fn).call(this, session, "declined");
|
|
258
|
+
__privateMethod(this, _CallEngine_instances, cleanup_fn).call(this, session, "declined");
|
|
259
|
+
}
|
|
260
|
+
/** Hang up an active call (sends bye/normal). */
|
|
261
|
+
async hangup(callId, reason = "normal") {
|
|
262
|
+
const session = __privateGet(this, _sessions).get(callId);
|
|
263
|
+
if (!session)
|
|
264
|
+
return;
|
|
265
|
+
await __privateMethod(this, _CallEngine_instances, sendByeSafe_fn).call(this, session, reason);
|
|
266
|
+
__privateMethod(this, _CallEngine_instances, cleanup_fn).call(this, session, reason);
|
|
267
|
+
}
|
|
268
|
+
/** Enable/disable a local track kind on an active call (mute / camera off). */
|
|
269
|
+
setLocalTrackEnabled(callId, kind, enabled) {
|
|
270
|
+
const session = __privateGet(this, _sessions).get(callId);
|
|
271
|
+
if (!(session == null ? void 0 : session.localStream))
|
|
272
|
+
return;
|
|
273
|
+
for (const track of session.localStream.getTracks()) {
|
|
274
|
+
if (track.kind === kind)
|
|
275
|
+
track.enabled = enabled;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/** Tear down every call (e.g. on app shutdown). */
|
|
279
|
+
dispose() {
|
|
280
|
+
for (const session of [...__privateGet(this, _sessions).values()]) {
|
|
281
|
+
__privateMethod(this, _CallEngine_instances, cleanup_fn).call(this, session, "close");
|
|
282
|
+
}
|
|
283
|
+
this.removeAll();
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
_opts = new WeakMap();
|
|
287
|
+
_iceServers = new WeakMap();
|
|
288
|
+
_sessions = new WeakMap();
|
|
289
|
+
_CallEngine_instances = new WeakSet();
|
|
290
|
+
// ---- internals -------------------------------------------------------
|
|
291
|
+
handleSignal_fn = function(peerId, signal) {
|
|
292
|
+
switch (signal.type) {
|
|
293
|
+
case "offer":
|
|
294
|
+
__privateMethod(this, _CallEngine_instances, onOffer_fn).call(this, peerId, signal);
|
|
295
|
+
return;
|
|
296
|
+
case "answer":
|
|
297
|
+
__privateMethod(this, _CallEngine_instances, onAnswer_fn).call(this, peerId, signal);
|
|
298
|
+
return;
|
|
299
|
+
case "candidate":
|
|
300
|
+
__privateMethod(this, _CallEngine_instances, onCandidate_fn).call(this, peerId, signal);
|
|
301
|
+
return;
|
|
302
|
+
case "remove-candidates":
|
|
303
|
+
__privateMethod(this, _CallEngine_instances, log_fn).call(this, `remove-candidates from ${peerId} (ignored)`);
|
|
304
|
+
return;
|
|
305
|
+
case "bye":
|
|
306
|
+
__privateMethod(this, _CallEngine_instances, onBye_fn).call(this, peerId, signal);
|
|
307
|
+
return;
|
|
308
|
+
case "action":
|
|
309
|
+
case "event":
|
|
310
|
+
case "prAnswer":
|
|
311
|
+
__privateMethod(this, _CallEngine_instances, onAuxiliary_fn).call(this, peerId, signal);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
onOffer_fn = function(peerId, signal) {
|
|
316
|
+
if (!signal.sdp)
|
|
317
|
+
return;
|
|
318
|
+
const existing = __privateGet(this, _sessions).get(signal.callId);
|
|
319
|
+
if (existing) {
|
|
320
|
+
existing.pendingOffer = signal.sdp;
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const audio = signal.options ? signal.options.includes("audio") : true;
|
|
324
|
+
const video = signal.options ? signal.options.includes("video") : false;
|
|
325
|
+
const data = signal.options ? signal.options.includes("data") : false;
|
|
326
|
+
const session = __privateMethod(this, _CallEngine_instances, createSession_fn).call(this, signal.callId, peerId, "incoming", { audio, video, data });
|
|
327
|
+
session.pendingOffer = signal.sdp;
|
|
328
|
+
this.emit("incomingCall", __privateGet(this, _toInfo).call(this, session));
|
|
329
|
+
__privateMethod(this, _CallEngine_instances, log_fn).call(this, `incoming call from ${peerId} callId=${signal.callId} audio=${audio} video=${video}`);
|
|
330
|
+
};
|
|
331
|
+
onAnswer_fn = function(peerId, signal) {
|
|
332
|
+
const session = __privateGet(this, _sessions).get(signal.callId);
|
|
333
|
+
if (!session || session.peerId !== peerId || !signal.sdp)
|
|
334
|
+
return;
|
|
335
|
+
void (async () => {
|
|
336
|
+
try {
|
|
337
|
+
await session.pc.setRemoteDescription({ type: "answer", sdp: signal.sdp });
|
|
338
|
+
session.hasReceivedSdp = true;
|
|
339
|
+
__privateMethod(this, _CallEngine_instances, setState_fn).call(this, session, "connecting");
|
|
340
|
+
await __privateMethod(this, _CallEngine_instances, drainRemoteCandidates_fn).call(this, session);
|
|
341
|
+
await __privateMethod(this, _CallEngine_instances, flushLocalCandidates_fn).call(this, session);
|
|
342
|
+
} catch (err) {
|
|
343
|
+
__privateMethod(this, _CallEngine_instances, log_fn).call(this, `setRemoteDescription(answer) failed: ${err.message}`);
|
|
344
|
+
}
|
|
345
|
+
})();
|
|
346
|
+
};
|
|
347
|
+
onCandidate_fn = function(peerId, signal) {
|
|
348
|
+
var _a;
|
|
349
|
+
const session = __privateGet(this, _sessions).get(signal.callId);
|
|
350
|
+
if (!session || session.peerId !== peerId || !signal.candidates)
|
|
351
|
+
return;
|
|
352
|
+
for (const c of signal.candidates) {
|
|
353
|
+
const init = {
|
|
354
|
+
candidate: c.sdp,
|
|
355
|
+
sdpMLineIndex: c.sdpMLineIndex,
|
|
356
|
+
sdpMid: (_a = c.sdpMid) != null ? _a : void 0
|
|
357
|
+
};
|
|
358
|
+
if (session.pc.remoteDescription) {
|
|
359
|
+
session.pc.addIceCandidate(init).catch((err) => __privateMethod(this, _CallEngine_instances, log_fn).call(this, `addIceCandidate failed: ${err.message}`));
|
|
360
|
+
} else {
|
|
361
|
+
session.remoteCandidateQueue.push(init);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
onBye_fn = function(peerId, signal) {
|
|
366
|
+
var _a;
|
|
367
|
+
const session = __privateGet(this, _sessions).get(signal.callId);
|
|
368
|
+
if (!session || session.peerId !== peerId)
|
|
369
|
+
return;
|
|
370
|
+
const reason = (_a = signal.reason) != null ? _a : "normal";
|
|
371
|
+
const state = reason === "busy" ? "busy" : reason === "declined" ? "declined" : "ended";
|
|
372
|
+
__privateMethod(this, _CallEngine_instances, setState_fn).call(this, session, state);
|
|
373
|
+
__privateMethod(this, _CallEngine_instances, cleanup_fn).call(this, session, reason);
|
|
374
|
+
__privateMethod(this, _CallEngine_instances, log_fn).call(this, `bye from ${peerId} callId=${signal.callId} reason=${reason}`);
|
|
375
|
+
};
|
|
376
|
+
onAuxiliary_fn = function(peerId, signal) {
|
|
377
|
+
const session = __privateGet(this, _sessions).get(signal.callId);
|
|
378
|
+
if (!session || session.peerId !== peerId)
|
|
379
|
+
return;
|
|
380
|
+
if (signal.type === "event") {
|
|
381
|
+
if (signal.event === "ringing")
|
|
382
|
+
__privateMethod(this, _CallEngine_instances, setState_fn).call(this, session, "remoteRinging");
|
|
383
|
+
else if (signal.event === "online")
|
|
384
|
+
__privateMethod(this, _CallEngine_instances, setState_fn).call(this, session, "remoteOnline");
|
|
385
|
+
} else if (signal.type === "action") {
|
|
386
|
+
if (signal.action === "reject") {
|
|
387
|
+
__privateMethod(this, _CallEngine_instances, setState_fn).call(this, session, "declined");
|
|
388
|
+
__privateMethod(this, _CallEngine_instances, cleanup_fn).call(this, session, "declined");
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
createSession_fn = function(callId, peerId, direction, kinds) {
|
|
393
|
+
const pc = __privateGet(this, _opts).createPeerConnection({ iceServers: __privateGet(this, _iceServers) });
|
|
394
|
+
const session = {
|
|
395
|
+
callId,
|
|
396
|
+
peerId,
|
|
397
|
+
direction,
|
|
398
|
+
pc,
|
|
399
|
+
audio: kinds.audio,
|
|
400
|
+
video: kinds.video,
|
|
401
|
+
data: kinds.data,
|
|
402
|
+
state: "connecting",
|
|
403
|
+
hasReceivedSdp: false,
|
|
404
|
+
localCandidateQueue: [],
|
|
405
|
+
remoteCandidateQueue: [],
|
|
406
|
+
closed: false
|
|
407
|
+
};
|
|
408
|
+
__privateGet(this, _sessions).set(callId, session);
|
|
409
|
+
__privateMethod(this, _CallEngine_instances, wirePeerConnection_fn).call(this, session);
|
|
410
|
+
return session;
|
|
411
|
+
};
|
|
412
|
+
wirePeerConnection_fn = function(session) {
|
|
413
|
+
const pc = session.pc;
|
|
414
|
+
pc.onicecandidate = (ev) => {
|
|
415
|
+
var _a, _b;
|
|
416
|
+
if (!ev.candidate || !ev.candidate.candidate)
|
|
417
|
+
return;
|
|
418
|
+
const sig = {
|
|
419
|
+
sdp: ev.candidate.candidate,
|
|
420
|
+
sdpMLineIndex: (_a = ev.candidate.sdpMLineIndex) != null ? _a : 0,
|
|
421
|
+
sdpMid: (_b = ev.candidate.sdpMid) != null ? _b : null
|
|
422
|
+
};
|
|
423
|
+
if (session.hasReceivedSdp) {
|
|
424
|
+
void __privateMethod(this, _CallEngine_instances, send_fn).call(this, session.peerId, { type: "candidate", candidates: [sig], callId: session.callId });
|
|
425
|
+
} else {
|
|
426
|
+
session.localCandidateQueue.push(sig);
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
pc.ontrack = (ev) => {
|
|
430
|
+
var _a;
|
|
431
|
+
const stream = (_a = ev.streams[0]) != null ? _a : __privateMethod(this, _CallEngine_instances, ensureRemoteStream_fn).call(this, session, ev.track);
|
|
432
|
+
if (session.remoteStream !== stream) {
|
|
433
|
+
session.remoteStream = stream;
|
|
434
|
+
this.emit("remoteStream", session.callId, stream);
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
pc.oniceconnectionstatechange = () => {
|
|
438
|
+
switch (pc.iceConnectionState) {
|
|
439
|
+
case "connected":
|
|
440
|
+
case "completed":
|
|
441
|
+
__privateMethod(this, _CallEngine_instances, setState_fn).call(this, session, "connected");
|
|
442
|
+
break;
|
|
443
|
+
case "disconnected":
|
|
444
|
+
__privateMethod(this, _CallEngine_instances, setState_fn).call(this, session, "disconnected");
|
|
445
|
+
break;
|
|
446
|
+
case "failed":
|
|
447
|
+
__privateMethod(this, _CallEngine_instances, setState_fn).call(this, session, "failed");
|
|
448
|
+
__privateMethod(this, _CallEngine_instances, cleanup_fn).call(this, session, "close", "failed");
|
|
449
|
+
break;
|
|
450
|
+
case "closed":
|
|
451
|
+
if (!session.closed)
|
|
452
|
+
__privateMethod(this, _CallEngine_instances, cleanup_fn).call(this, session, "close");
|
|
453
|
+
break;
|
|
454
|
+
default:
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
};
|
|
459
|
+
ensureRemoteStream_fn = function(session, track) {
|
|
460
|
+
if (!session.remoteStream)
|
|
461
|
+
session.remoteStream = new MediaStream();
|
|
462
|
+
session.remoteStream.addTrack(track);
|
|
463
|
+
return session.remoteStream;
|
|
464
|
+
};
|
|
465
|
+
attachLocalMedia_fn = async function(session) {
|
|
466
|
+
if (session.localStream)
|
|
467
|
+
return;
|
|
468
|
+
if (!__privateGet(this, _opts).getLocalMedia || !session.audio && !session.video)
|
|
469
|
+
return;
|
|
470
|
+
const stream = await __privateGet(this, _opts).getLocalMedia({ audio: session.audio, video: session.video });
|
|
471
|
+
session.localStream = stream;
|
|
472
|
+
for (const track of stream.getTracks()) {
|
|
473
|
+
session.pc.addTrack(track, stream);
|
|
474
|
+
}
|
|
475
|
+
this.emit("localStream", session.callId, stream);
|
|
476
|
+
};
|
|
477
|
+
flushLocalCandidates_fn = async function(session) {
|
|
478
|
+
if (!session.hasReceivedSdp || session.localCandidateQueue.length === 0)
|
|
479
|
+
return;
|
|
480
|
+
const queued = session.localCandidateQueue.splice(0);
|
|
481
|
+
for (const sig of queued) {
|
|
482
|
+
await __privateMethod(this, _CallEngine_instances, send_fn).call(this, session.peerId, { type: "candidate", candidates: [sig], callId: session.callId });
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
drainRemoteCandidates_fn = async function(session) {
|
|
486
|
+
if (!session.pc.remoteDescription)
|
|
487
|
+
return;
|
|
488
|
+
const queued = session.remoteCandidateQueue.splice(0);
|
|
489
|
+
for (const init of queued) {
|
|
490
|
+
await session.pc.addIceCandidate(init).catch((err) => __privateMethod(this, _CallEngine_instances, log_fn).call(this, `addIceCandidate (drain) failed: ${err.message}`));
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
send_fn = async function(peerId, signal) {
|
|
494
|
+
try {
|
|
495
|
+
await __privateGet(this, _opts).signaling.send(peerId, signal);
|
|
496
|
+
} catch (err) {
|
|
497
|
+
__privateMethod(this, _CallEngine_instances, log_fn).call(this, `signal send failed (${signal.type} \u2192 ${peerId}): ${err.message}`);
|
|
498
|
+
throw err;
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
sendByeSafe_fn = async function(session, reason) {
|
|
502
|
+
try {
|
|
503
|
+
await __privateGet(this, _opts).signaling.send(session.peerId, { type: "bye", reason, callId: session.callId });
|
|
504
|
+
} catch (err) {
|
|
505
|
+
__privateMethod(this, _CallEngine_instances, log_fn).call(this, `bye send failed: ${err.message}`);
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
cleanup_fn = function(session, reason, endedReason) {
|
|
509
|
+
var _a, _b;
|
|
510
|
+
if (session.closed)
|
|
511
|
+
return;
|
|
512
|
+
session.closed = true;
|
|
513
|
+
try {
|
|
514
|
+
session.pc.onicecandidate = null;
|
|
515
|
+
session.pc.ontrack = null;
|
|
516
|
+
session.pc.oniceconnectionstatechange = null;
|
|
517
|
+
session.pc.close();
|
|
518
|
+
} catch {
|
|
519
|
+
}
|
|
520
|
+
for (const track of (_b = (_a = session.localStream) == null ? void 0 : _a.getTracks()) != null ? _b : []) {
|
|
521
|
+
try {
|
|
522
|
+
track.stop();
|
|
523
|
+
} catch {
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
__privateGet(this, _sessions).delete(session.callId);
|
|
527
|
+
if (session.state !== "ended" && session.state !== "failed") {
|
|
528
|
+
session.state = endedReason === "failed" ? "failed" : "ended";
|
|
529
|
+
}
|
|
530
|
+
this.emit("ended", session.callId, endedReason != null ? endedReason : reason);
|
|
531
|
+
};
|
|
532
|
+
setState_fn = function(session, state) {
|
|
533
|
+
if (session.state === state)
|
|
534
|
+
return;
|
|
535
|
+
session.state = state;
|
|
536
|
+
this.emit("stateChanged", __privateGet(this, _toInfo).call(this, session), state);
|
|
537
|
+
};
|
|
538
|
+
_toInfo = new WeakMap();
|
|
539
|
+
newCallId_fn = function() {
|
|
540
|
+
if (__privateGet(this, _opts).generateCallId)
|
|
541
|
+
return __privateGet(this, _opts).generateCallId();
|
|
542
|
+
const c = globalThis.crypto;
|
|
543
|
+
if (c && typeof c.randomUUID === "function")
|
|
544
|
+
return c.randomUUID();
|
|
545
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (ch) => {
|
|
546
|
+
const r = Math.floor(Math.random() * 16);
|
|
547
|
+
const v = ch === "x" ? r : r & 3 | 8;
|
|
548
|
+
return v.toString(16);
|
|
549
|
+
});
|
|
550
|
+
};
|
|
551
|
+
log_fn = function(msg) {
|
|
552
|
+
var _a, _b;
|
|
553
|
+
(_b = (_a = __privateGet(this, _opts)).logger) == null ? void 0 : _b.call(_a, `[peer-webrtc] ${msg}`);
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
// node_modules/@decentnetwork/peer-webrtc/dist/signaling/broadcast.js
|
|
557
|
+
var _selfId, _channel, _handler;
|
|
558
|
+
var BroadcastChannelSignaling = class {
|
|
559
|
+
constructor(selfId, channelName = "peer-webrtc-demo") {
|
|
560
|
+
__privateAdd(this, _selfId);
|
|
561
|
+
__privateAdd(this, _channel);
|
|
562
|
+
__privateAdd(this, _handler);
|
|
563
|
+
__privateSet(this, _selfId, selfId);
|
|
564
|
+
__privateSet(this, _channel, new BroadcastChannel(channelName));
|
|
565
|
+
__privateGet(this, _channel).onmessage = (ev) => {
|
|
566
|
+
var _a;
|
|
567
|
+
const msg = ev.data;
|
|
568
|
+
if (!msg || msg.to !== __privateGet(this, _selfId) || msg.from === __privateGet(this, _selfId))
|
|
569
|
+
return;
|
|
570
|
+
(_a = __privateGet(this, _handler)) == null ? void 0 : _a.call(this, msg.from, msg.signal);
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
send(peerId, signal) {
|
|
574
|
+
const env = { from: __privateGet(this, _selfId), to: peerId, signal };
|
|
575
|
+
__privateGet(this, _channel).postMessage(env);
|
|
576
|
+
}
|
|
577
|
+
onSignal(handler) {
|
|
578
|
+
__privateSet(this, _handler, handler);
|
|
579
|
+
}
|
|
580
|
+
close() {
|
|
581
|
+
__privateGet(this, _channel).close();
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
_selfId = new WeakMap();
|
|
585
|
+
_channel = new WeakMap();
|
|
586
|
+
_handler = new WeakMap();
|
|
587
|
+
|
|
588
|
+
// node_modules/@decentnetwork/peer-webrtc/dist/guard.js
|
|
589
|
+
function decodeSignalBytesGuard(input) {
|
|
590
|
+
try {
|
|
591
|
+
return decodeSignal(input);
|
|
592
|
+
} catch {
|
|
593
|
+
return void 0;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// node_modules/@decentnetwork/peer-webrtc/dist/signaling/carrier.js
|
|
598
|
+
var DEFAULT_EXT = "carrier";
|
|
599
|
+
var _peer, _ext;
|
|
600
|
+
var CarrierSignaling = class {
|
|
601
|
+
constructor(peer, opts = {}) {
|
|
602
|
+
__privateAdd(this, _peer);
|
|
603
|
+
__privateAdd(this, _ext);
|
|
604
|
+
var _a;
|
|
605
|
+
__privateSet(this, _peer, peer);
|
|
606
|
+
__privateSet(this, _ext, (_a = opts.ext) != null ? _a : DEFAULT_EXT);
|
|
607
|
+
}
|
|
608
|
+
async send(peerId, signal) {
|
|
609
|
+
await __privateGet(this, _peer).sendInvite(peerId, encodeSignalBytes(signal), { ext: __privateGet(this, _ext) });
|
|
610
|
+
}
|
|
611
|
+
onSignal(handler) {
|
|
612
|
+
__privateGet(this, _peer).onInvite((evt) => {
|
|
613
|
+
if (evt.ext !== void 0 && evt.ext !== __privateGet(this, _ext))
|
|
614
|
+
return;
|
|
615
|
+
if (!looksLikeSignal(evt.data))
|
|
616
|
+
return;
|
|
617
|
+
const signal = decodeSignalBytesGuard(evt.data);
|
|
618
|
+
if (signal)
|
|
619
|
+
handler(evt.pubkey, signal);
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
_peer = new WeakMap();
|
|
624
|
+
_ext = new WeakMap();
|
|
625
|
+
return __toCommonJS(dist_exports);
|
|
626
|
+
})();
|
package/dist/ui/server.js
CHANGED
|
@@ -602,6 +602,28 @@ export function startFriendUi(opts) {
|
|
|
602
602
|
sendJson(res, r.ok ? 200 : 400, r);
|
|
603
603
|
return;
|
|
604
604
|
}
|
|
605
|
+
// WebRTC call signaling (peer-webrtc). The browser CallEngine sends
|
|
606
|
+
// RtcSignals here and long-polls call-poll for inbound ones; the daemon
|
|
607
|
+
// relays both over the Carrier "carrier" invite extension.
|
|
608
|
+
if (req.method === "POST" && url === "/api/call-signal") {
|
|
609
|
+
const { userid, data } = await readBody(req);
|
|
610
|
+
const r = await opts.call({ op: "call-signal", userid, data });
|
|
611
|
+
sendJson(res, r.ok ? 200 : 400, r);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
if (req.method === "GET" && url === "/api/call-poll") {
|
|
615
|
+
// The daemon holds this up to ~20s (long-poll). opts.call's own IPC
|
|
616
|
+
// timeout is 30s, so it returns before that; on any error, resolve
|
|
617
|
+
// empty so the browser simply re-polls.
|
|
618
|
+
try {
|
|
619
|
+
const r = await opts.call({ op: "call-poll" });
|
|
620
|
+
sendJson(res, 200, r.ok ? r.data ?? { signals: [] } : { signals: [] });
|
|
621
|
+
}
|
|
622
|
+
catch {
|
|
623
|
+
sendJson(res, 200, { signals: [] });
|
|
624
|
+
}
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
605
627
|
if (req.method === "POST" && url === "/api/friend-remove") {
|
|
606
628
|
const { userid } = await readBody(req);
|
|
607
629
|
const r = await opts.call({ op: "friend-remove", userid });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.160",
|
|
4
4
|
"description": "Private virtual LAN for self-hosted services and AI agents, built on Elastos Carrier. NAT-traversal, name service, ACL, all over a peer-to-peer mesh — no public IP required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -79,7 +79,8 @@
|
|
|
79
79
|
},
|
|
80
80
|
"dependencies": {
|
|
81
81
|
"@decentnetwork/dora": "^0.1.13",
|
|
82
|
-
"@decentnetwork/peer": "^0.1.
|
|
82
|
+
"@decentnetwork/peer": "^0.1.84",
|
|
83
|
+
"@decentnetwork/peer-webrtc": "^0.1.0",
|
|
83
84
|
"ink": "^5.2.1",
|
|
84
85
|
"js-yaml": "^4.1.0",
|
|
85
86
|
"node-forge": "^1.4.0",
|