@decentnetwork/lan 0.1.165 → 0.1.175
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/offline-call.d.ts +69 -0
- package/dist/carrier/offline-call.js +192 -0
- package/dist/carrier/peer-manager.d.ts +13 -3
- package/dist/carrier/peer-manager.js +57 -4
- package/dist/daemon/server.js +28 -4
- package/dist/types.d.ts +17 -0
- package/dist/ui/desktop/app.js +53 -9
- package/dist/ui/desktop/vendor/peer-webrtc.js +271 -10
- package/package.json +4 -3
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { type RtcSignal } from "@decentnetwork/peer-webrtc";
|
|
3
|
+
/**
|
|
4
|
+
* Configuration for the offline-call fallback. Defaults target the public
|
|
5
|
+
* Beagle gateways; the appKey/appName must match what the target device
|
|
6
|
+
* registered with (see the Beagle app's Service.swift).
|
|
7
|
+
*/
|
|
8
|
+
export interface OfflineCallConfig {
|
|
9
|
+
/** Master switch. When false the bridge is inert. */
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
/** Beagle push appKey (matches the callee's registration). */
|
|
12
|
+
appKey: string;
|
|
13
|
+
/** Beagle push appName — the callee app's CFBundleName ("Beagle"/"webrtc"). */
|
|
14
|
+
appName: string;
|
|
15
|
+
/** Socket.IO signaling gateway (primary). */
|
|
16
|
+
socketUrl: string;
|
|
17
|
+
/** Ordered socket.IO gateways to try — connects to the first that comes up,
|
|
18
|
+
* switches to the next on failure. Defaults to [socketUrl]. Both call peers
|
|
19
|
+
* must agree on a server, so keep this list in sync with the Beagle apps. */
|
|
20
|
+
socketUrls?: string[];
|
|
21
|
+
/** Push-API endpoints (POST /push-api/push-message), tried in order. */
|
|
22
|
+
pushEndpoints?: string[];
|
|
23
|
+
}
|
|
24
|
+
export declare const DEFAULT_OFFLINE_CONFIG: Omit<OfflineCallConfig, "enabled">;
|
|
25
|
+
/**
|
|
26
|
+
* Offline-call bridge — the socket.io + push path Beagle uses to reach a peer
|
|
27
|
+
* whose app is backgrounded/offline (so a live Carrier session doesn't exist).
|
|
28
|
+
*
|
|
29
|
+
* The daemon uses this as a FALLBACK: `sendCallSignal` tries Carrier first and
|
|
30
|
+
* drops to this only when there's no live session. Signaling then rides the
|
|
31
|
+
* socket.io room (keyed by the RtcSignal `callId`); the initial offer also fires
|
|
32
|
+
* a push to wake the callee. Inbound socket.io signals are re-emitted as
|
|
33
|
+
* `"signal"` events so the daemon forwards them to the browser exactly like
|
|
34
|
+
* Carrier invites.
|
|
35
|
+
*
|
|
36
|
+
* Runs entirely in the daemon (Node): socket.io-client for signaling, fetch for
|
|
37
|
+
* push. socket.io-client is an OPTIONAL dependency — if it isn't installed the
|
|
38
|
+
* bridge stays disabled and logs a hint rather than crashing.
|
|
39
|
+
*/
|
|
40
|
+
export declare class OfflineCallBridge extends EventEmitter {
|
|
41
|
+
#private;
|
|
42
|
+
constructor(opts: {
|
|
43
|
+
config: OfflineCallConfig;
|
|
44
|
+
selfUserId: string;
|
|
45
|
+
selfName: string;
|
|
46
|
+
log?: (msg: string) => void;
|
|
47
|
+
});
|
|
48
|
+
get enabled(): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Send a call signal to `userid` over the offline path. Decodes the signal to
|
|
51
|
+
* key the room by `callId`; for the initial offer it also pushes to wake the
|
|
52
|
+
* callee. Throws if the bridge can't be brought up (caller keeps the original
|
|
53
|
+
* Carrier error).
|
|
54
|
+
*/
|
|
55
|
+
/** Extract the callId from a raw signal payload (undefined if not decodable). */
|
|
56
|
+
callIdOf(data: Uint8Array | string): string | undefined;
|
|
57
|
+
/** True once this call has fallen back to the offline path. */
|
|
58
|
+
isOfflineCall(callId: string): boolean;
|
|
59
|
+
/** Mark a call as committed to the Carrier (online) path. */
|
|
60
|
+
markOnlineCall(callId: string): void;
|
|
61
|
+
/** True once this call has committed to the Carrier path. */
|
|
62
|
+
isOnlineCall(callId: string): boolean;
|
|
63
|
+
/** Drop a finished call's transport state (best-effort cleanup). */
|
|
64
|
+
forgetCall(callId: string): void;
|
|
65
|
+
sendSignal(userid: string, data: Uint8Array | string, hasVideo: boolean): Promise<void>;
|
|
66
|
+
/** Tear down the socket connection (e.g. on daemon shutdown). */
|
|
67
|
+
close(): void;
|
|
68
|
+
}
|
|
69
|
+
export type { RtcSignal };
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { SocketIoSignaling, BeaglePushClient, decodeSignal, encodeSignalBytes } from "@decentnetwork/peer-webrtc";
|
|
3
|
+
export const DEFAULT_OFFLINE_CONFIG = {
|
|
4
|
+
// From the Beagle app (Service.swift). Overridable via config.
|
|
5
|
+
appKey: "08726e938c624c4da9dcd9a85f2bbcd8",
|
|
6
|
+
appName: "Beagle",
|
|
7
|
+
socketUrl: "https://io.beagle.chat",
|
|
8
|
+
socketUrls: ["https://io.beagle.chat"],
|
|
9
|
+
pushEndpoints: [
|
|
10
|
+
"https://pushapi.beagle.chat/push-api/push-message",
|
|
11
|
+
"https://www.callpass.cn/push-api/push-message",
|
|
12
|
+
"https://tokyo.fi.chat:3004/push-api/push-message"
|
|
13
|
+
]
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Offline-call bridge — the socket.io + push path Beagle uses to reach a peer
|
|
17
|
+
* whose app is backgrounded/offline (so a live Carrier session doesn't exist).
|
|
18
|
+
*
|
|
19
|
+
* The daemon uses this as a FALLBACK: `sendCallSignal` tries Carrier first and
|
|
20
|
+
* drops to this only when there's no live session. Signaling then rides the
|
|
21
|
+
* socket.io room (keyed by the RtcSignal `callId`); the initial offer also fires
|
|
22
|
+
* a push to wake the callee. Inbound socket.io signals are re-emitted as
|
|
23
|
+
* `"signal"` events so the daemon forwards them to the browser exactly like
|
|
24
|
+
* Carrier invites.
|
|
25
|
+
*
|
|
26
|
+
* Runs entirely in the daemon (Node): socket.io-client for signaling, fetch for
|
|
27
|
+
* push. socket.io-client is an OPTIONAL dependency — if it isn't installed the
|
|
28
|
+
* bridge stays disabled and logs a hint rather than crashing.
|
|
29
|
+
*/
|
|
30
|
+
export class OfflineCallBridge extends EventEmitter {
|
|
31
|
+
#cfg;
|
|
32
|
+
#selfUserId;
|
|
33
|
+
#selfName;
|
|
34
|
+
#log;
|
|
35
|
+
#push;
|
|
36
|
+
#signaling;
|
|
37
|
+
#socket;
|
|
38
|
+
#connecting;
|
|
39
|
+
/** callIds that have fallen to the offline path — subsequent signals for them
|
|
40
|
+
* skip the Carrier retry entirely (no per-signal 1.5s stall). */
|
|
41
|
+
#offlineCalls = new Set();
|
|
42
|
+
/** callIds committed to the Carrier (online) path — a later transient session
|
|
43
|
+
* flap must NOT divert their signals to socket.io (splitting transports
|
|
44
|
+
* mid-call breaks ICE: the peer gets candidates over two channels). */
|
|
45
|
+
#onlineCalls = new Set();
|
|
46
|
+
constructor(opts) {
|
|
47
|
+
super();
|
|
48
|
+
this.#cfg = opts.config;
|
|
49
|
+
this.#selfUserId = opts.selfUserId;
|
|
50
|
+
this.#selfName = opts.selfName;
|
|
51
|
+
this.#log = opts.log ?? (() => { });
|
|
52
|
+
}
|
|
53
|
+
get enabled() {
|
|
54
|
+
return this.#cfg.enabled;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Send a call signal to `userid` over the offline path. Decodes the signal to
|
|
58
|
+
* key the room by `callId`; for the initial offer it also pushes to wake the
|
|
59
|
+
* callee. Throws if the bridge can't be brought up (caller keeps the original
|
|
60
|
+
* Carrier error).
|
|
61
|
+
*/
|
|
62
|
+
/** Extract the callId from a raw signal payload (undefined if not decodable). */
|
|
63
|
+
callIdOf(data) {
|
|
64
|
+
try {
|
|
65
|
+
return decodeSignal(data).callId || undefined;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** True once this call has fallen back to the offline path. */
|
|
72
|
+
isOfflineCall(callId) {
|
|
73
|
+
return this.#offlineCalls.has(callId);
|
|
74
|
+
}
|
|
75
|
+
/** Mark a call as committed to the Carrier (online) path. */
|
|
76
|
+
markOnlineCall(callId) {
|
|
77
|
+
this.#onlineCalls.add(callId);
|
|
78
|
+
}
|
|
79
|
+
/** True once this call has committed to the Carrier path. */
|
|
80
|
+
isOnlineCall(callId) {
|
|
81
|
+
return this.#onlineCalls.has(callId);
|
|
82
|
+
}
|
|
83
|
+
/** Drop a finished call's transport state (best-effort cleanup). */
|
|
84
|
+
forgetCall(callId) {
|
|
85
|
+
this.#offlineCalls.delete(callId);
|
|
86
|
+
this.#onlineCalls.delete(callId);
|
|
87
|
+
}
|
|
88
|
+
async sendSignal(userid, data, hasVideo) {
|
|
89
|
+
if (!this.#cfg.enabled)
|
|
90
|
+
throw new Error("offline calls disabled");
|
|
91
|
+
const signal = decodeSignal(data);
|
|
92
|
+
if (!signal.callId)
|
|
93
|
+
throw new Error("offline call signal has no callId");
|
|
94
|
+
this.#offlineCalls.add(signal.callId);
|
|
95
|
+
await this.#ensureConnected();
|
|
96
|
+
// Ring the callee for the first outbound signal of a call (the offer).
|
|
97
|
+
if (signal.type === "offer" && this.#push) {
|
|
98
|
+
const woke = await this.#push
|
|
99
|
+
.ring(userid, {
|
|
100
|
+
callerUserId: this.#selfUserId,
|
|
101
|
+
callId: signal.callId,
|
|
102
|
+
callerName: this.#selfName,
|
|
103
|
+
hasVideo,
|
|
104
|
+
channel: "socketio"
|
|
105
|
+
})
|
|
106
|
+
.catch(() => false);
|
|
107
|
+
this.#log(`offline call: push ${woke ? "sent" : "FAILED"} to ${userid} (call ${signal.callId})`);
|
|
108
|
+
}
|
|
109
|
+
this.#signaling.joinCall(signal.callId);
|
|
110
|
+
const kind = signal.type === "candidate"
|
|
111
|
+
? `candidate(${signal.candidates?.length ?? 0})`
|
|
112
|
+
: signal.type === "event" ? `event:${signal.event}` : signal.type;
|
|
113
|
+
this.#log(`offline call: SEND ${kind} to ${userid} (call ${signal.callId})`);
|
|
114
|
+
this.#signaling.send(userid, signal);
|
|
115
|
+
}
|
|
116
|
+
/** Bring up the socket.io connection + signaling/push clients once. */
|
|
117
|
+
async #ensureConnected() {
|
|
118
|
+
if (this.#signaling)
|
|
119
|
+
return;
|
|
120
|
+
if (this.#connecting)
|
|
121
|
+
return this.#connecting;
|
|
122
|
+
this.#connecting = this.#connect();
|
|
123
|
+
try {
|
|
124
|
+
await this.#connecting;
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
this.#connecting = undefined;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async #connect() {
|
|
131
|
+
let io;
|
|
132
|
+
try {
|
|
133
|
+
// Optional dependency — imported lazily so the daemon runs without it.
|
|
134
|
+
const mod = (await import("socket.io-client"));
|
|
135
|
+
io = mod.io;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
throw new Error("offline calls need socket.io-client — run `npm install socket.io-client` in decentlan");
|
|
139
|
+
}
|
|
140
|
+
// Try each gateway in order; connect to the first that comes up.
|
|
141
|
+
const urls = this.#cfg.socketUrls?.length ? this.#cfg.socketUrls : [this.#cfg.socketUrl];
|
|
142
|
+
let connectedUrl;
|
|
143
|
+
for (const url of urls) {
|
|
144
|
+
const sock = io(url, { transports: ["websocket"], forceNew: true, reconnection: false });
|
|
145
|
+
const ok = await new Promise((resolve) => {
|
|
146
|
+
const t = setTimeout(() => resolve(false), 7000);
|
|
147
|
+
sock.on("connect", () => { clearTimeout(t); resolve(true); });
|
|
148
|
+
sock.on("connect_error", () => { clearTimeout(t); resolve(false); });
|
|
149
|
+
});
|
|
150
|
+
if (ok) {
|
|
151
|
+
this.#socket = sock;
|
|
152
|
+
connectedUrl = url;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
this.#log(`offline call: socket gateway ${url} unreachable — trying next`);
|
|
156
|
+
try {
|
|
157
|
+
sock.disconnect?.();
|
|
158
|
+
}
|
|
159
|
+
catch { /* best-effort */ }
|
|
160
|
+
}
|
|
161
|
+
if (!this.#socket)
|
|
162
|
+
throw new Error(`offline calls: no reachable socket gateway (${urls.join(", ")})`);
|
|
163
|
+
this.#signaling = new SocketIoSignaling(this.#socket, { userId: this.#selfUserId });
|
|
164
|
+
this.#signaling.onSignal((peerId, signal) => {
|
|
165
|
+
// Re-emit inbound socket.io signals just like a Carrier invite so the
|
|
166
|
+
// daemon's existing call-signal queue forwards them to the browser.
|
|
167
|
+
const kind = signal.type === "candidate"
|
|
168
|
+
? `candidate(${signal.candidates?.length ?? 0})`
|
|
169
|
+
: signal.type === "event" ? `event:${signal.event}` : signal.type;
|
|
170
|
+
this.#log(`offline call: RECV ${kind} from ${peerId} (call ${signal.callId ?? "?"})`);
|
|
171
|
+
this.emit("signal", { userid: peerId, data: encodeSignalBytes(signal) });
|
|
172
|
+
});
|
|
173
|
+
this.#push = new BeaglePushClient({
|
|
174
|
+
appKey: this.#cfg.appKey,
|
|
175
|
+
appName: this.#cfg.appName,
|
|
176
|
+
endpoints: this.#cfg.pushEndpoints
|
|
177
|
+
});
|
|
178
|
+
this.#log(`offline call bridge connected to ${connectedUrl}`);
|
|
179
|
+
}
|
|
180
|
+
/** Tear down the socket connection (e.g. on daemon shutdown). */
|
|
181
|
+
close() {
|
|
182
|
+
try {
|
|
183
|
+
this.#socket?.disconnect?.();
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
/* best-effort */
|
|
187
|
+
}
|
|
188
|
+
this.#socket = undefined;
|
|
189
|
+
this.#signaling = undefined;
|
|
190
|
+
this.#push = undefined;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { Peer } from "@decentnetwork/peer";
|
|
6
6
|
import { EventEmitter } from "events";
|
|
7
|
+
import { type OfflineCallConfig } from "./offline-call.js";
|
|
7
8
|
import { PacketSession } from "./packet-session.js";
|
|
8
9
|
import type { PeerIdentity, RemotePeer } from "./types.js";
|
|
9
10
|
import type { BootstrapNode } from "../types.js";
|
|
@@ -21,6 +22,10 @@ export interface PeerManagerOptions {
|
|
|
21
22
|
/** Directory for persisting partially-received files so a dropped/restarted
|
|
22
23
|
* transfer resumes instead of restarting (断点续传). */
|
|
23
24
|
fileResumeDir?: string;
|
|
25
|
+
/** Offline-call fallback (socket.io + push). When enabled, a call to a peer
|
|
26
|
+
* with no live Carrier session rings it via the Beagle push gateway and
|
|
27
|
+
* signals over socket.io instead of failing. */
|
|
28
|
+
offlineCall?: OfflineCallConfig;
|
|
24
29
|
}
|
|
25
30
|
export declare class PeerManager extends EventEmitter {
|
|
26
31
|
private peer;
|
|
@@ -28,6 +33,9 @@ export declare class PeerManager extends EventEmitter {
|
|
|
28
33
|
private sessions;
|
|
29
34
|
private sessionCounter;
|
|
30
35
|
private logger;
|
|
36
|
+
private offlineBridge;
|
|
37
|
+
private offlineConfig;
|
|
38
|
+
private nickname;
|
|
31
39
|
constructor();
|
|
32
40
|
create(opts: PeerManagerOptions): Promise<void>;
|
|
33
41
|
start(): Promise<void>;
|
|
@@ -70,10 +78,12 @@ export declare class PeerManager extends EventEmitter {
|
|
|
70
78
|
* Send a WebRTC call-signaling payload to a friend over the Carrier
|
|
71
79
|
* friend-invite channel (the "carrier" extension), the exact transport the
|
|
72
80
|
* iOS/Android Beagle apps listen on for calls. `data` is an RtcSignal JSON
|
|
73
|
-
* string/bytes (see @decentnetwork/peer-webrtc). Realtime
|
|
74
|
-
* session
|
|
81
|
+
* string/bytes (see @decentnetwork/peer-webrtc). Realtime over Carrier when a
|
|
82
|
+
* live session exists; otherwise falls back to the offline path (socket.io +
|
|
83
|
+
* push) when configured, so a call to a backgrounded/offline phone still
|
|
84
|
+
* rings instead of failing.
|
|
75
85
|
*/
|
|
76
|
-
sendCallSignal(userid: string, data: Uint8Array | string): Promise<void>;
|
|
86
|
+
sendCallSignal(userid: string, data: Uint8Array | string, hasVideo?: boolean): Promise<void>;
|
|
77
87
|
/** True when the friend is a NATIVE Carrier client (iOS/Android/C): its
|
|
78
88
|
* relay/DHT key differs from its identity key. JS peers announce their
|
|
79
89
|
* identity key as their DHT key, so the two match. */
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { Peer } from "@decentnetwork/peer";
|
|
6
6
|
import { EventEmitter } from "events";
|
|
7
|
+
import { OfflineCallBridge } from "./offline-call.js";
|
|
7
8
|
import { PacketSession } from "./packet-session.js";
|
|
8
9
|
import { FrameCodec } from "./frame.js";
|
|
9
10
|
import { FRAME_OPCODE_HANDSHAKE_ACK, PACKET_ID_DL_SESSION, PACKET_ID_DL_DORA, PACKET_ID_DL_IP, PACKET_ID_DL_RATE, } from "./types.js";
|
|
@@ -14,12 +15,17 @@ export class PeerManager extends EventEmitter {
|
|
|
14
15
|
sessions = new Map();
|
|
15
16
|
sessionCounter = 1;
|
|
16
17
|
logger;
|
|
18
|
+
offlineBridge = null;
|
|
19
|
+
offlineConfig = null;
|
|
20
|
+
nickname = "";
|
|
17
21
|
constructor() {
|
|
18
22
|
super();
|
|
19
23
|
this.logger = new Logger({ prefix: "PeerManager" });
|
|
20
24
|
}
|
|
21
25
|
async create(opts) {
|
|
22
26
|
this.logger.info("Creating Peer instance");
|
|
27
|
+
this.offlineConfig = opts.offlineCall ?? null;
|
|
28
|
+
this.nickname = opts.nickname ?? "";
|
|
23
29
|
this.peer = await Peer.create({
|
|
24
30
|
keyFile: opts.keyFile,
|
|
25
31
|
compatibilityMode: "legacy",
|
|
@@ -50,6 +56,21 @@ export class PeerManager extends EventEmitter {
|
|
|
50
56
|
userid: this.peer.userid(),
|
|
51
57
|
};
|
|
52
58
|
this.logger.info(`Peer started: ${this.identity.address}`);
|
|
59
|
+
// Bring up the offline-call fallback (socket.io + push) if configured.
|
|
60
|
+
// Signals it receives are re-emitted as "call-signal" — identical to a
|
|
61
|
+
// Carrier invite — so the daemon's existing browser bridge handles them.
|
|
62
|
+
if (this.offlineConfig?.enabled) {
|
|
63
|
+
this.offlineBridge = new OfflineCallBridge({
|
|
64
|
+
config: this.offlineConfig,
|
|
65
|
+
selfUserId: this.identity.userid,
|
|
66
|
+
selfName: this.nickname || this.identity.userid,
|
|
67
|
+
log: (m) => this.logger.info(m),
|
|
68
|
+
});
|
|
69
|
+
this.offlineBridge.on("signal", (evt) => {
|
|
70
|
+
this.emit("call-signal", { pubkey: evt.userid, data: evt.data });
|
|
71
|
+
});
|
|
72
|
+
this.logger.info("Offline-call fallback enabled (socket.io + push)");
|
|
73
|
+
}
|
|
53
74
|
}
|
|
54
75
|
async joinNetwork() {
|
|
55
76
|
if (!this.peer) {
|
|
@@ -133,13 +154,45 @@ export class PeerManager extends EventEmitter {
|
|
|
133
154
|
* Send a WebRTC call-signaling payload to a friend over the Carrier
|
|
134
155
|
* friend-invite channel (the "carrier" extension), the exact transport the
|
|
135
156
|
* iOS/Android Beagle apps listen on for calls. `data` is an RtcSignal JSON
|
|
136
|
-
* string/bytes (see @decentnetwork/peer-webrtc). Realtime
|
|
137
|
-
* session
|
|
157
|
+
* string/bytes (see @decentnetwork/peer-webrtc). Realtime over Carrier when a
|
|
158
|
+
* live session exists; otherwise falls back to the offline path (socket.io +
|
|
159
|
+
* push) when configured, so a call to a backgrounded/offline phone still
|
|
160
|
+
* rings instead of failing.
|
|
138
161
|
*/
|
|
139
|
-
async sendCallSignal(userid, data) {
|
|
162
|
+
async sendCallSignal(userid, data, hasVideo = true) {
|
|
140
163
|
if (!this.peer)
|
|
141
164
|
throw new Error("Peer not created. Call create() first.");
|
|
142
|
-
|
|
165
|
+
// Once a call has fallen to the offline path, keep every later signal for it
|
|
166
|
+
// on that path — don't re-attempt Carrier per signal (that stalled the UI
|
|
167
|
+
// ~1.5s each and reordered signals across transports).
|
|
168
|
+
const callId = this.offlineBridge?.enabled ? this.offlineBridge.callIdOf(data) : undefined;
|
|
169
|
+
if (callId && this.offlineBridge.isOfflineCall(callId)) {
|
|
170
|
+
await this.offlineBridge.sendSignal(userid, data, hasVideo);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
// Fail fast (1.5s, not 5s) so an offline peer drops to push quickly.
|
|
175
|
+
await this.peer.sendInvite(userid, data, { ext: "carrier", establishTimeoutMs: 1500 });
|
|
176
|
+
// Committed to Carrier — keep this call there even if it briefly flaps.
|
|
177
|
+
if (callId)
|
|
178
|
+
this.offlineBridge?.markOnlineCall(callId);
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
// A call already committed to Carrier must NOT split to socket.io on a
|
|
182
|
+
// transient flap (it breaks ICE). Drop this signal; ICE recovers via the
|
|
183
|
+
// other candidates / a retried offer.
|
|
184
|
+
if (callId && this.offlineBridge?.isOnlineCall(callId)) {
|
|
185
|
+
this.logger.info(`sendCallSignal: dropped a Carrier signal for online call ${callId} (${err.message})`);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// No live session and not yet committed to Carrier — use the offline path.
|
|
189
|
+
if (this.offlineBridge?.enabled) {
|
|
190
|
+
this.logger.info(`sendCallSignal: Carrier failed (${err.message}) — offline path to ${userid}`);
|
|
191
|
+
await this.offlineBridge.sendSignal(userid, data, hasVideo);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
throw err;
|
|
195
|
+
}
|
|
143
196
|
}
|
|
144
197
|
/** True when the friend is a NATIVE Carrier client (iOS/Android/C): its
|
|
145
198
|
* relay/DHT key differs from its identity key. JS peers announce their
|
package/dist/daemon/server.js
CHANGED
|
@@ -5,6 +5,7 @@ import { resolve } from "path";
|
|
|
5
5
|
import { EventEmitter } from "events";
|
|
6
6
|
import { existsSync, readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
7
7
|
import { PeerManager } from "../carrier/peer-manager.js";
|
|
8
|
+
import { DEFAULT_OFFLINE_CONFIG } from "../carrier/offline-call.js";
|
|
8
9
|
import { TunDevice } from "../tun/tun-device.js";
|
|
9
10
|
import { RouteManager } from "../tun/route-manager.js";
|
|
10
11
|
import { Ipam } from "../ipam/ipam.js";
|
|
@@ -292,6 +293,18 @@ export class DaemonServer {
|
|
|
292
293
|
// Persist partially-received files so a dropped/restarted transfer
|
|
293
294
|
// resumes instead of restarting from 0 (断点续传).
|
|
294
295
|
fileResumeDir: resolve(this.configDir, "downloads", ".partial"),
|
|
296
|
+
// Offline-call fallback (socket.io + push): ring an offline/backgrounded
|
|
297
|
+
// phone via the Beagle push gateway when there's no live Carrier
|
|
298
|
+
// session. Enabled by default with the public Beagle gateways; override
|
|
299
|
+
// appKey/appName/socketUrl under `call:` in config.yaml.
|
|
300
|
+
offlineCall: {
|
|
301
|
+
enabled: this.config.call?.offlineEnabled ?? true,
|
|
302
|
+
appKey: this.config.call?.appKey ?? DEFAULT_OFFLINE_CONFIG.appKey,
|
|
303
|
+
appName: this.config.call?.appName ?? DEFAULT_OFFLINE_CONFIG.appName,
|
|
304
|
+
socketUrl: this.config.call?.socketUrl ?? DEFAULT_OFFLINE_CONFIG.socketUrl,
|
|
305
|
+
socketUrls: this.config.call?.socketUrls ?? DEFAULT_OFFLINE_CONFIG.socketUrls,
|
|
306
|
+
pushEndpoints: this.config.call?.pushEndpoints ?? DEFAULT_OFFLINE_CONFIG.pushEndpoints,
|
|
307
|
+
},
|
|
295
308
|
});
|
|
296
309
|
await this.peerManager.start();
|
|
297
310
|
this.logger.info(`Identity: ${this.peerManager.getAddress()}`);
|
|
@@ -467,10 +480,12 @@ export class DaemonServer {
|
|
|
467
480
|
// Beagle apps themselves use. Detected via the DHT-key signature.
|
|
468
481
|
if (this.peerManager?.isNativeFriend(userid)) {
|
|
469
482
|
// The FileModel JSON envelope is base64 (inflates ~1.34x) + a little
|
|
470
|
-
// JSON overhead, and the whole thing must fit the
|
|
471
|
-
// (CARRIER_MAX_APP_BULKMSG_LEN
|
|
472
|
-
//
|
|
473
|
-
|
|
483
|
+
// JSON overhead, and the whole thing must fit the 16MB bulkmsg cap
|
|
484
|
+
// (CARRIER_MAX_APP_BULKMSG_LEN, raised from 5MB alongside the receive
|
|
485
|
+
// reorder buffer in peer >= 0.1.87). 11MB raw → ~14.7MB envelope,
|
|
486
|
+
// safely under 16MB. Needs BOTH ends on the raised cap. (Very large /
|
|
487
|
+
// streaming transfers should use toxcore file transfer, not inline.)
|
|
488
|
+
const INLINE_MAX = 11 * 1024 * 1024;
|
|
474
489
|
if (data.length > INLINE_MAX) {
|
|
475
490
|
throw new Error(`This friend is a native (iOS/Android) client — files are sent inline and limited to ` +
|
|
476
491
|
`${(INLINE_MAX / 1024 / 1024).toFixed(1)} MB (this is ${(data.length / 1024 / 1024).toFixed(1)} MB).`);
|
|
@@ -563,6 +578,15 @@ export class DaemonServer {
|
|
|
563
578
|
});
|
|
564
579
|
}
|
|
565
580
|
const signals = this.callSignalQueue.splice(0, this.callSignalQueue.length);
|
|
581
|
+
if (signals.length) {
|
|
582
|
+
const kinds = signals.map((s) => { try {
|
|
583
|
+
return JSON.parse(s.data).type;
|
|
584
|
+
}
|
|
585
|
+
catch {
|
|
586
|
+
return "?";
|
|
587
|
+
} }).join(",");
|
|
588
|
+
this.logger.info(`call-poll -> browser: ${signals.length} signal(s) [${kinds}]`);
|
|
589
|
+
}
|
|
566
590
|
return { signals };
|
|
567
591
|
},
|
|
568
592
|
subscribe: (emit) => {
|
package/dist/types.d.ts
CHANGED
|
@@ -193,6 +193,23 @@ export interface DecentAgentNetConfig {
|
|
|
193
193
|
friends?: FriendsConfig;
|
|
194
194
|
dora?: DoraConfig;
|
|
195
195
|
forwarding?: ForwardingConfig;
|
|
196
|
+
call?: CallConfig;
|
|
197
|
+
}
|
|
198
|
+
export interface CallConfig {
|
|
199
|
+
/** Enable the offline-call fallback (socket.io + push). Default true. When a
|
|
200
|
+
* call target has no live Carrier session, ring it via the Beagle push
|
|
201
|
+
* gateway and signal over socket.io instead of failing. */
|
|
202
|
+
offlineEnabled?: boolean;
|
|
203
|
+
/** Beagle push appKey — must match what the callee's app registered with. */
|
|
204
|
+
appKey?: string;
|
|
205
|
+
/** Beagle push appName (the callee app's CFBundleName, e.g. "Beagle"). */
|
|
206
|
+
appName?: string;
|
|
207
|
+
/** Socket.IO signaling gateway URL (primary). */
|
|
208
|
+
socketUrl?: string;
|
|
209
|
+
/** Ordered socket.IO gateways to try (failover). Defaults to [socketUrl]. */
|
|
210
|
+
socketUrls?: string[];
|
|
211
|
+
/** Push-API endpoints (POST /push-api/push-message), tried in order. */
|
|
212
|
+
pushEndpoints?: string[];
|
|
196
213
|
}
|
|
197
214
|
export interface ForwardingConfig {
|
|
198
215
|
/** Pace + fair-queue egress IP data (default true). Prevents one bursty flow
|
package/dist/ui/desktop/app.js
CHANGED
|
@@ -1633,18 +1633,17 @@ function useCallController(selfId) {
|
|
|
1633
1633
|
engineRef.current = null;
|
|
1634
1634
|
};
|
|
1635
1635
|
}, [selfId]);
|
|
1636
|
-
const start = React.useCallback(
|
|
1636
|
+
const start = React.useCallback((peerId, video) => {
|
|
1637
1637
|
const eng = engineRef.current;
|
|
1638
1638
|
if (!eng) {
|
|
1639
1639
|
alert("Calls unavailable (WebRTC not supported here)");
|
|
1640
1640
|
return;
|
|
1641
1641
|
}
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
setActive(
|
|
1645
|
-
} catch (e) {
|
|
1642
|
+
setActive({ callId: null, peerId, video: !!video, direction: "outgoing", state: "ringing" });
|
|
1643
|
+
eng.call(peerId, { audio: true, video: !!video }).then((callId) => setActive((a) => a && a.peerId === peerId && a.callId === null ? { ...a, callId } : a)).catch((e) => {
|
|
1644
|
+
setActive((a) => a && a.peerId === peerId ? null : a);
|
|
1646
1645
|
alert("Could not start call: " + (e && e.message || e));
|
|
1647
|
-
}
|
|
1646
|
+
});
|
|
1648
1647
|
}, []);
|
|
1649
1648
|
const accept = React.useCallback(async () => {
|
|
1650
1649
|
const eng = engineRef.current;
|
|
@@ -1666,7 +1665,7 @@ function useCallController(selfId) {
|
|
|
1666
1665
|
const hangup = React.useCallback(() => {
|
|
1667
1666
|
const eng = engineRef.current;
|
|
1668
1667
|
setActive((a) => {
|
|
1669
|
-
if (eng && a) eng.hangup(a.callId);
|
|
1668
|
+
if (eng && a && a.callId) eng.hangup(a.callId);
|
|
1670
1669
|
return null;
|
|
1671
1670
|
});
|
|
1672
1671
|
setLocalStream(null);
|
|
@@ -1674,11 +1673,11 @@ function useCallController(selfId) {
|
|
|
1674
1673
|
}, []);
|
|
1675
1674
|
const setMuted = React.useCallback((m) => {
|
|
1676
1675
|
const eng = engineRef.current, a = active;
|
|
1677
|
-
if (eng && a) eng.setLocalTrackEnabled(a.callId, "audio", !m);
|
|
1676
|
+
if (eng && a && a.callId) eng.setLocalTrackEnabled(a.callId, "audio", !m);
|
|
1678
1677
|
}, [active]);
|
|
1679
1678
|
const setVideoOn = React.useCallback((v) => {
|
|
1680
1679
|
const eng = engineRef.current, a = active;
|
|
1681
|
-
if (eng && a) eng.setLocalTrackEnabled(a.callId, "video", v);
|
|
1680
|
+
if (eng && a && a.callId) eng.setLocalTrackEnabled(a.callId, "video", v);
|
|
1682
1681
|
}, [active]);
|
|
1683
1682
|
return { incoming, active, localStream, remoteStream, start, accept, reject, hangup, setMuted, setVideoOn };
|
|
1684
1683
|
}
|
|
@@ -1694,8 +1693,53 @@ function AudioSink({ stream }) {
|
|
|
1694
1693
|
}, [stream]);
|
|
1695
1694
|
return /* @__PURE__ */ React.createElement("audio", { ref, autoPlay: true });
|
|
1696
1695
|
}
|
|
1696
|
+
function useRingtone(active) {
|
|
1697
|
+
React.useEffect(() => {
|
|
1698
|
+
if (!active) return void 0;
|
|
1699
|
+
let ctx;
|
|
1700
|
+
try {
|
|
1701
|
+
ctx = new (window.AudioContext || window.webkitAudioContext)();
|
|
1702
|
+
} catch (e) {
|
|
1703
|
+
return void 0;
|
|
1704
|
+
}
|
|
1705
|
+
if (ctx.state === "suspended") {
|
|
1706
|
+
ctx.resume().catch(() => {
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1709
|
+
let stopped = false;
|
|
1710
|
+
const timers = [];
|
|
1711
|
+
const burst = () => {
|
|
1712
|
+
if (stopped || ctx.state === "closed") return;
|
|
1713
|
+
[440, 480].forEach((f) => {
|
|
1714
|
+
const o = ctx.createOscillator(), g = ctx.createGain();
|
|
1715
|
+
o.type = "sine";
|
|
1716
|
+
o.frequency.value = f;
|
|
1717
|
+
o.connect(g);
|
|
1718
|
+
g.connect(ctx.destination);
|
|
1719
|
+
const t = ctx.currentTime;
|
|
1720
|
+
g.gain.setValueAtTime(1e-4, t);
|
|
1721
|
+
g.gain.exponentialRampToValueAtTime(0.12, t + 0.05);
|
|
1722
|
+
g.gain.setValueAtTime(0.12, t + 0.9);
|
|
1723
|
+
g.gain.exponentialRampToValueAtTime(1e-4, t + 1);
|
|
1724
|
+
o.start(t);
|
|
1725
|
+
o.stop(t + 1.05);
|
|
1726
|
+
});
|
|
1727
|
+
timers.push(setTimeout(burst, 3e3));
|
|
1728
|
+
};
|
|
1729
|
+
burst();
|
|
1730
|
+
return () => {
|
|
1731
|
+
stopped = true;
|
|
1732
|
+
timers.forEach(clearTimeout);
|
|
1733
|
+
try {
|
|
1734
|
+
ctx.close();
|
|
1735
|
+
} catch (e) {
|
|
1736
|
+
}
|
|
1737
|
+
};
|
|
1738
|
+
}, [active]);
|
|
1739
|
+
}
|
|
1697
1740
|
function IncomingCallModal({ T, ctl, peers }) {
|
|
1698
1741
|
const inc = ctl.incoming;
|
|
1742
|
+
useRingtone(!!inc);
|
|
1699
1743
|
if (!inc) return null;
|
|
1700
1744
|
const peer = (peers || []).find((p) => p.id === inc.peerId) || { id: inc.peerId };
|
|
1701
1745
|
const name = peer.alias || peer.name || shortKey(inc.peerId, 10, 6);
|
|
@@ -28,10 +28,12 @@ var PeerWebRTC = (() => {
|
|
|
28
28
|
// node_modules/@decentnetwork/peer-webrtc/dist/index.js
|
|
29
29
|
var dist_exports = {};
|
|
30
30
|
__export(dist_exports, {
|
|
31
|
+
BeaglePushClient: () => BeaglePushClient,
|
|
31
32
|
BroadcastChannelSignaling: () => BroadcastChannelSignaling,
|
|
32
33
|
CallEngine: () => CallEngine,
|
|
33
34
|
CarrierSignaling: () => CarrierSignaling,
|
|
34
35
|
Emitter: () => Emitter,
|
|
36
|
+
SocketIoSignaling: () => SocketIoSignaling,
|
|
35
37
|
decodeSignal: () => decodeSignal,
|
|
36
38
|
decodeSignalBytesGuard: () => decodeSignalBytesGuard,
|
|
37
39
|
encodeSignal: () => encodeSignal,
|
|
@@ -197,17 +199,26 @@ var PeerWebRTC = (() => {
|
|
|
197
199
|
* "remoteStream"/"ended" as the call progresses.
|
|
198
200
|
*/
|
|
199
201
|
async call(peerId, kinds = {}) {
|
|
200
|
-
var _a, _b, _c;
|
|
202
|
+
var _a, _b, _c, _d, _e;
|
|
201
203
|
const audio = (_a = kinds.audio) != null ? _a : true;
|
|
202
204
|
const video = (_b = kinds.video) != null ? _b : false;
|
|
203
205
|
const data = (_c = kinds.data) != null ? _c : false;
|
|
204
206
|
const callId = __privateMethod(this, _CallEngine_instances, newCallId_fn).call(this);
|
|
205
207
|
const session = __privateMethod(this, _CallEngine_instances, createSession_fn).call(this, callId, peerId, "outgoing", { audio, video, data });
|
|
206
208
|
await __privateMethod(this, _CallEngine_instances, attachLocalMedia_fn).call(this, session);
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
if (audio && !((_d = session.localStream) == null ? void 0 : _d.getAudioTracks().length)) {
|
|
210
|
+
try {
|
|
211
|
+
session.pc.addTransceiver("audio", { direction: "recvonly" });
|
|
212
|
+
} catch {
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (video && !((_e = session.localStream) == null ? void 0 : _e.getVideoTracks().length)) {
|
|
216
|
+
try {
|
|
217
|
+
session.pc.addTransceiver("video", { direction: "recvonly" });
|
|
218
|
+
} catch {
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const offer = await session.pc.createOffer();
|
|
211
222
|
await session.pc.setLocalDescription(offer);
|
|
212
223
|
const options = [];
|
|
213
224
|
if (audio)
|
|
@@ -330,8 +341,9 @@ var PeerWebRTC = (() => {
|
|
|
330
341
|
};
|
|
331
342
|
onAnswer_fn = function(peerId, signal) {
|
|
332
343
|
const session = __privateGet(this, _sessions).get(__privateMethod(this, _CallEngine_instances, normId_fn).call(this, signal.callId));
|
|
333
|
-
if (!session ||
|
|
344
|
+
if (!session || !signal.sdp)
|
|
334
345
|
return;
|
|
346
|
+
session.peerId = peerId;
|
|
335
347
|
void (async () => {
|
|
336
348
|
try {
|
|
337
349
|
await session.pc.setRemoteDescription({ type: "answer", sdp: signal.sdp });
|
|
@@ -347,7 +359,7 @@ var PeerWebRTC = (() => {
|
|
|
347
359
|
onCandidate_fn = function(peerId, signal) {
|
|
348
360
|
var _a;
|
|
349
361
|
const session = __privateGet(this, _sessions).get(__privateMethod(this, _CallEngine_instances, normId_fn).call(this, signal.callId));
|
|
350
|
-
if (!session ||
|
|
362
|
+
if (!session || !signal.candidates)
|
|
351
363
|
return;
|
|
352
364
|
for (const c of signal.candidates) {
|
|
353
365
|
const init = {
|
|
@@ -365,7 +377,7 @@ var PeerWebRTC = (() => {
|
|
|
365
377
|
onBye_fn = function(peerId, signal) {
|
|
366
378
|
var _a;
|
|
367
379
|
const session = __privateGet(this, _sessions).get(__privateMethod(this, _CallEngine_instances, normId_fn).call(this, signal.callId));
|
|
368
|
-
if (!session
|
|
380
|
+
if (!session)
|
|
369
381
|
return;
|
|
370
382
|
const reason = (_a = signal.reason) != null ? _a : "normal";
|
|
371
383
|
const state = reason === "busy" ? "busy" : reason === "declined" ? "declined" : "ended";
|
|
@@ -375,7 +387,7 @@ var PeerWebRTC = (() => {
|
|
|
375
387
|
};
|
|
376
388
|
onAuxiliary_fn = function(peerId, signal) {
|
|
377
389
|
const session = __privateGet(this, _sessions).get(__privateMethod(this, _CallEngine_instances, normId_fn).call(this, signal.callId));
|
|
378
|
-
if (!session
|
|
390
|
+
if (!session)
|
|
379
391
|
return;
|
|
380
392
|
if (signal.type === "event") {
|
|
381
393
|
if (signal.event === "ringing")
|
|
@@ -467,7 +479,24 @@ var PeerWebRTC = (() => {
|
|
|
467
479
|
return;
|
|
468
480
|
if (!__privateGet(this, _opts).getLocalMedia || !session.audio && !session.video)
|
|
469
481
|
return;
|
|
470
|
-
|
|
482
|
+
let stream;
|
|
483
|
+
try {
|
|
484
|
+
stream = await __privateGet(this, _opts).getLocalMedia({ audio: session.audio, video: session.video });
|
|
485
|
+
} catch (err) {
|
|
486
|
+
__privateMethod(this, _CallEngine_instances, log_fn).call(this, `getLocalMedia(audio:${session.audio},video:${session.video}) failed: ${err.message}`);
|
|
487
|
+
if (session.video && session.audio) {
|
|
488
|
+
try {
|
|
489
|
+
stream = await __privateGet(this, _opts).getLocalMedia({ audio: true, video: false });
|
|
490
|
+
__privateMethod(this, _CallEngine_instances, log_fn).call(this, `local camera unavailable \u2014 sending audio only, still receiving video, call ${session.callId}`);
|
|
491
|
+
} catch (err2) {
|
|
492
|
+
__privateMethod(this, _CallEngine_instances, log_fn).call(this, `audio-only fallback failed too: ${err2.message}`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (!stream) {
|
|
497
|
+
__privateMethod(this, _CallEngine_instances, log_fn).call(this, `no local media for ${session.callId}; connecting receive-only`);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
471
500
|
session.localStream = stream;
|
|
472
501
|
for (const track of stream.getTracks()) {
|
|
473
502
|
session.pc.addTrack(track, stream);
|
|
@@ -631,5 +660,237 @@ var PeerWebRTC = (() => {
|
|
|
631
660
|
};
|
|
632
661
|
_peer = new WeakMap();
|
|
633
662
|
_ext = new WeakMap();
|
|
663
|
+
|
|
664
|
+
// node_modules/@decentnetwork/peer-webrtc/dist/signaling/socketio.js
|
|
665
|
+
var _socket, _userId, _handler2, _rooms, _pending, _lastOffer, _wired, _SocketIoSignaling_instances, roomKey_fn, ensureRoom_fn, emitMessage_fn, wire_fn;
|
|
666
|
+
var SocketIoSignaling = class {
|
|
667
|
+
constructor(socket, opts) {
|
|
668
|
+
__privateAdd(this, _SocketIoSignaling_instances);
|
|
669
|
+
__privateAdd(this, _socket);
|
|
670
|
+
__privateAdd(this, _userId);
|
|
671
|
+
__privateAdd(this, _handler2);
|
|
672
|
+
/** Rooms we've asked to join (callId -> confirmed self-membership). */
|
|
673
|
+
__privateAdd(this, _rooms, /* @__PURE__ */ new Map());
|
|
674
|
+
/** Signals queued per room until the server confirms our membership. */
|
|
675
|
+
__privateAdd(this, _pending, /* @__PURE__ */ new Map());
|
|
676
|
+
/** Last offer per callId, RE-SENT when the peer joins the room — the callee
|
|
677
|
+
* (woken by a push) joins seconds after we sent the offer, and socket.io does
|
|
678
|
+
* NOT replay, so without this the peer never sees the offer and never
|
|
679
|
+
* answers (offline call stuck at "connecting"). */
|
|
680
|
+
__privateAdd(this, _lastOffer, /* @__PURE__ */ new Map());
|
|
681
|
+
/** The remote peer id per room, learned from inbound messages so a queued
|
|
682
|
+
* peer-greeting can be addressed. */
|
|
683
|
+
__privateAdd(this, _wired, false);
|
|
684
|
+
__privateSet(this, _socket, socket);
|
|
685
|
+
__privateSet(this, _userId, opts.userId);
|
|
686
|
+
}
|
|
687
|
+
onSignal(handler) {
|
|
688
|
+
__privateSet(this, _handler2, handler);
|
|
689
|
+
__privateMethod(this, _SocketIoSignaling_instances, wire_fn).call(this);
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Deliver a signal to `peerId`. Joins the call's room (keyed by
|
|
693
|
+
* `signal.callId`) on first use and queues until the server confirms
|
|
694
|
+
* membership, then emits — mirroring the iOS provider, which queues until
|
|
695
|
+
* `connectToRoom`.
|
|
696
|
+
*/
|
|
697
|
+
send(peerId, signal) {
|
|
698
|
+
var _a;
|
|
699
|
+
__privateMethod(this, _SocketIoSignaling_instances, wire_fn).call(this);
|
|
700
|
+
const callId = signal.callId;
|
|
701
|
+
if (!callId) {
|
|
702
|
+
__privateMethod(this, _SocketIoSignaling_instances, emitMessage_fn).call(this, signal);
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
if (signal.type === "offer")
|
|
706
|
+
__privateGet(this, _lastOffer).set(callId, signal);
|
|
707
|
+
const room = __privateMethod(this, _SocketIoSignaling_instances, ensureRoom_fn).call(this, callId);
|
|
708
|
+
if (room.joined) {
|
|
709
|
+
__privateMethod(this, _SocketIoSignaling_instances, emitMessage_fn).call(this, signal);
|
|
710
|
+
} else {
|
|
711
|
+
const q = (_a = __privateGet(this, _pending).get(callId)) != null ? _a : [];
|
|
712
|
+
q.push(signal);
|
|
713
|
+
__privateGet(this, _pending).set(callId, q);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Explicitly join the room for an INCOMING call (e.g. after a push
|
|
718
|
+
* notification delivered the `callId`), so inbound signals for it are
|
|
719
|
+
* received. Safe to call more than once.
|
|
720
|
+
*/
|
|
721
|
+
joinCall(callId) {
|
|
722
|
+
__privateMethod(this, _SocketIoSignaling_instances, wire_fn).call(this);
|
|
723
|
+
__privateMethod(this, _SocketIoSignaling_instances, ensureRoom_fn).call(this, callId);
|
|
724
|
+
}
|
|
725
|
+
/** Leave a call's room (best-effort) and forget its queued state. */
|
|
726
|
+
leaveCall(callId) {
|
|
727
|
+
__privateGet(this, _rooms).delete(callId);
|
|
728
|
+
__privateGet(this, _pending).delete(callId);
|
|
729
|
+
__privateGet(this, _lastOffer).delete(callId);
|
|
730
|
+
__privateGet(this, _socket).emit("leave-channel", { channel: __privateMethod(this, _SocketIoSignaling_instances, roomKey_fn).call(this, callId), sender: __privateGet(this, _userId) });
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
_socket = new WeakMap();
|
|
734
|
+
_userId = new WeakMap();
|
|
735
|
+
_handler2 = new WeakMap();
|
|
736
|
+
_rooms = new WeakMap();
|
|
737
|
+
_pending = new WeakMap();
|
|
738
|
+
_lastOffer = new WeakMap();
|
|
739
|
+
_wired = new WeakMap();
|
|
740
|
+
_SocketIoSignaling_instances = new WeakSet();
|
|
741
|
+
/** The socket.io ROOM key. The native Beagle apps join the room with the
|
|
742
|
+
* call's `UUID.uuidString`, which Swift renders UPPERCASE — so a lowercase
|
|
743
|
+
* `crypto.randomUUID()` from JS would land us in a DIFFERENT room and we'd
|
|
744
|
+
* never receive the peer's answer/candidates. Canonicalize to UPPERCASE to
|
|
745
|
+
* share the native peer's room. (The RtcSignal payload callId stays as-is;
|
|
746
|
+
* the CallEngine matches it case-insensitively.) */
|
|
747
|
+
roomKey_fn = function(callId) {
|
|
748
|
+
return callId.toUpperCase();
|
|
749
|
+
};
|
|
750
|
+
ensureRoom_fn = function(callId) {
|
|
751
|
+
let room = __privateGet(this, _rooms).get(callId);
|
|
752
|
+
if (!room) {
|
|
753
|
+
room = { joined: false };
|
|
754
|
+
__privateGet(this, _rooms).set(callId, room);
|
|
755
|
+
__privateGet(this, _socket).emit("new-channel", { channel: __privateMethod(this, _SocketIoSignaling_instances, roomKey_fn).call(this, callId), sender: __privateGet(this, _userId) });
|
|
756
|
+
}
|
|
757
|
+
return room;
|
|
758
|
+
};
|
|
759
|
+
emitMessage_fn = function(signal) {
|
|
760
|
+
__privateGet(this, _socket).emit("message", { data: encodeSignal(signal), sender: __privateGet(this, _userId) });
|
|
761
|
+
};
|
|
762
|
+
wire_fn = function() {
|
|
763
|
+
if (__privateGet(this, _wired))
|
|
764
|
+
return;
|
|
765
|
+
__privateSet(this, _wired, true);
|
|
766
|
+
__privateGet(this, _socket).on("message", (...args) => {
|
|
767
|
+
var _a;
|
|
768
|
+
const payload = firstObject(args);
|
|
769
|
+
if (!(payload == null ? void 0 : payload.data) || !payload.sender)
|
|
770
|
+
return;
|
|
771
|
+
if (payload.sender === __privateGet(this, _userId))
|
|
772
|
+
return;
|
|
773
|
+
const signal = decodeSignalBytesGuard(payload.data);
|
|
774
|
+
if (signal)
|
|
775
|
+
(_a = __privateGet(this, _handler2)) == null ? void 0 : _a.call(this, payload.sender, signal);
|
|
776
|
+
});
|
|
777
|
+
__privateGet(this, _socket).on("connectToRoom", (...args) => {
|
|
778
|
+
const payload = firstObject(args);
|
|
779
|
+
const sender = payload == null ? void 0 : payload.sender;
|
|
780
|
+
if (!sender)
|
|
781
|
+
return;
|
|
782
|
+
if (sender === __privateGet(this, _userId)) {
|
|
783
|
+
for (const [callId, room] of __privateGet(this, _rooms)) {
|
|
784
|
+
room.joined = true;
|
|
785
|
+
const q = __privateGet(this, _pending).get(callId);
|
|
786
|
+
if (q && q.length) {
|
|
787
|
+
for (const sig of q)
|
|
788
|
+
__privateMethod(this, _SocketIoSignaling_instances, emitMessage_fn).call(this, sig);
|
|
789
|
+
__privateGet(this, _pending).delete(callId);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
} else {
|
|
793
|
+
for (const callId of __privateGet(this, _rooms).keys()) {
|
|
794
|
+
__privateMethod(this, _SocketIoSignaling_instances, emitMessage_fn).call(this, { type: "event", callId, event: "online" });
|
|
795
|
+
const offer = __privateGet(this, _lastOffer).get(callId);
|
|
796
|
+
if (offer)
|
|
797
|
+
__privateMethod(this, _SocketIoSignaling_instances, emitMessage_fn).call(this, offer);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
__privateGet(this, _socket).on("disconnectToRoom", (...args) => {
|
|
802
|
+
const payload = firstObject(args);
|
|
803
|
+
const sender = payload == null ? void 0 : payload.sender;
|
|
804
|
+
if (sender && sender === __privateGet(this, _userId)) {
|
|
805
|
+
for (const room of __privateGet(this, _rooms).values())
|
|
806
|
+
room.joined = false;
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
};
|
|
810
|
+
function firstObject(args) {
|
|
811
|
+
const first = args[0];
|
|
812
|
+
return first && typeof first === "object" ? first : void 0;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// node_modules/@decentnetwork/peer-webrtc/dist/signaling/push.js
|
|
816
|
+
var DEFAULT_ENDPOINTS = [
|
|
817
|
+
"https://pushapi.beagle.chat/push-api/push-message",
|
|
818
|
+
"https://www.callpass.cn/push-api/push-message",
|
|
819
|
+
"https://tokyo.fi.chat:3004/push-api/push-message"
|
|
820
|
+
];
|
|
821
|
+
var _appKey, _appName, _endpoints, _fetch;
|
|
822
|
+
var BeaglePushClient = class {
|
|
823
|
+
constructor(opts) {
|
|
824
|
+
__privateAdd(this, _appKey);
|
|
825
|
+
__privateAdd(this, _appName);
|
|
826
|
+
__privateAdd(this, _endpoints);
|
|
827
|
+
__privateAdd(this, _fetch);
|
|
828
|
+
var _a, _b;
|
|
829
|
+
__privateSet(this, _appKey, opts.appKey);
|
|
830
|
+
__privateSet(this, _appName, opts.appName);
|
|
831
|
+
__privateSet(this, _endpoints, ((_a = opts.endpoints) == null ? void 0 : _a.length) ? opts.endpoints : DEFAULT_ENDPOINTS);
|
|
832
|
+
const f = (_b = opts.fetch) != null ? _b : globalThis.fetch;
|
|
833
|
+
if (!f)
|
|
834
|
+
throw new Error("BeaglePushClient: no fetch available \u2014 pass opts.fetch");
|
|
835
|
+
__privateSet(this, _fetch, f);
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Ring an offline peer. `calleeUserId` is the destination (their Carrier user
|
|
839
|
+
* id); the payload describes the call. Tries each endpoint until one accepts.
|
|
840
|
+
* Resolves `true` on success, `false` if every endpoint failed.
|
|
841
|
+
*/
|
|
842
|
+
async sendCallPush(calleeUserId, payload) {
|
|
843
|
+
var _a;
|
|
844
|
+
const params = new URLSearchParams({
|
|
845
|
+
account: calleeUserId,
|
|
846
|
+
payload: JSON.stringify(payload),
|
|
847
|
+
appKey: __privateGet(this, _appKey),
|
|
848
|
+
appName: __privateGet(this, _appName)
|
|
849
|
+
}).toString();
|
|
850
|
+
for (const endpoint of __privateGet(this, _endpoints)) {
|
|
851
|
+
try {
|
|
852
|
+
const url = endpoint + (endpoint.includes("?") ? "&" : "?") + params;
|
|
853
|
+
const res = await __privateGet(this, _fetch).call(this, url, { method: "POST" });
|
|
854
|
+
if (!res.ok)
|
|
855
|
+
continue;
|
|
856
|
+
const json = await res.json().catch(() => ({}));
|
|
857
|
+
const code = json.code;
|
|
858
|
+
const ok = code === void 0 || code === 0 || code === "0" || ((_a = json.data) == null ? void 0 : _a.success) === true;
|
|
859
|
+
if (ok)
|
|
860
|
+
return true;
|
|
861
|
+
} catch {
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return false;
|
|
865
|
+
}
|
|
866
|
+
/** Convenience: ring `calleeUserId` for a call over `channel`. */
|
|
867
|
+
ring(calleeUserId, args) {
|
|
868
|
+
var _a;
|
|
869
|
+
return this.sendCallPush(calleeUserId, {
|
|
870
|
+
userId: args.callerUserId,
|
|
871
|
+
hasVideo: args.hasVideo ? "true" : "false",
|
|
872
|
+
callId: args.callId,
|
|
873
|
+
callName: args.callerName,
|
|
874
|
+
action: "call",
|
|
875
|
+
channel: (_a = args.channel) != null ? _a : "socketio"
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
/** Convenience: cancel a ring (e.g. caller hung up before answer). */
|
|
879
|
+
cancel(calleeUserId, args) {
|
|
880
|
+
var _a;
|
|
881
|
+
return this.sendCallPush(calleeUserId, {
|
|
882
|
+
userId: args.callerUserId,
|
|
883
|
+
hasVideo: args.hasVideo ? "true" : "false",
|
|
884
|
+
callId: args.callId,
|
|
885
|
+
callName: args.callerName,
|
|
886
|
+
action: "decline",
|
|
887
|
+
channel: (_a = args.channel) != null ? _a : "socketio"
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
_appKey = new WeakMap();
|
|
892
|
+
_appName = new WeakMap();
|
|
893
|
+
_endpoints = new WeakMap();
|
|
894
|
+
_fetch = new WeakMap();
|
|
634
895
|
return __toCommonJS(dist_exports);
|
|
635
896
|
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.175",
|
|
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,12 +79,13 @@
|
|
|
79
79
|
},
|
|
80
80
|
"dependencies": {
|
|
81
81
|
"@decentnetwork/dora": "^0.1.13",
|
|
82
|
-
"@decentnetwork/peer": "^0.1.
|
|
83
|
-
"@decentnetwork/peer-webrtc": "^0.
|
|
82
|
+
"@decentnetwork/peer": "^0.1.91",
|
|
83
|
+
"@decentnetwork/peer-webrtc": "^0.2.7",
|
|
84
84
|
"ink": "^5.2.1",
|
|
85
85
|
"js-yaml": "^4.1.0",
|
|
86
86
|
"node-forge": "^1.4.0",
|
|
87
87
|
"react": "^18.3.1",
|
|
88
|
+
"socket.io-client": "^4.7.5",
|
|
88
89
|
"yargs": "^17.7.2"
|
|
89
90
|
},
|
|
90
91
|
"devDependencies": {
|