@decentnetwork/lan 0.1.255 → 0.1.257
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/dist/carrier/peer-manager.d.ts +23 -0
- package/dist/carrier/peer-manager.js +64 -1
- package/dist/carrier/types.d.ts +4 -0
- package/dist/carrier/types.js +20 -0
- package/dist/daemon/ipc.d.ts +11 -2
- package/dist/daemon/ipc.js +18 -0
- package/dist/daemon/server.js +17 -0
- package/dist/ui/desktop/app.js +36 -2
- package/package.json +1 -1
|
@@ -27,6 +27,17 @@ export interface PeerManagerOptions {
|
|
|
27
27
|
* signals over socket.io instead of failing. */
|
|
28
28
|
offlineCall?: OfflineCallConfig;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* True when a Carrier invite send failed for a reason that will recur
|
|
32
|
+
* identically on every retry, so retrying or waiting for a reconnect is
|
|
33
|
+
* pointless and the signal must take another transport or be lost.
|
|
34
|
+
*
|
|
35
|
+
* The one that bit us: the native invite channel caps application data at
|
|
36
|
+
* CARRIER_MAX_INVITE_DATA_LEN (8192, from carrier.h — it is a wire constant
|
|
37
|
+
* shared with the iOS/Android SDK, not something we can raise unilaterally),
|
|
38
|
+
* while a Chrome SDP offer/answer runs 8.4-9.8 KB.
|
|
39
|
+
*/
|
|
40
|
+
export declare function isPermanentInviteFailure(err: unknown): boolean;
|
|
30
41
|
export declare class PeerManager extends EventEmitter {
|
|
31
42
|
private peer;
|
|
32
43
|
private identity;
|
|
@@ -70,6 +81,18 @@ export declare class PeerManager extends EventEmitter {
|
|
|
70
81
|
sign(message: Uint8Array): Uint8Array;
|
|
71
82
|
/** Offer a file to a friend (toxcore-standard transfer). Returns the fileId. */
|
|
72
83
|
sendFile(userid: string, data: Uint8Array, name: string): string | null;
|
|
84
|
+
/**
|
|
85
|
+
* Send an APPLICATION packet to a friend. The payload is opaque to
|
|
86
|
+
* decentlan — this is the extension point that lets an app (beagle, an
|
|
87
|
+
* agent, any IPC client) run its own protocol over our Carrier session
|
|
88
|
+
* without a change in here.
|
|
89
|
+
*
|
|
90
|
+
* `id` must be in PACKET_ID_APP_MIN..MAX. Rejecting anything else is a
|
|
91
|
+
* security boundary, not tidiness: our own 161-164 carry session handshakes,
|
|
92
|
+
* dora control and IP frames, so a caller allowed to pick those ids could
|
|
93
|
+
* forge a handshake or inject a packet onto a peer's TUN.
|
|
94
|
+
*/
|
|
95
|
+
sendAppPacket(userid: string, id: number, data: Uint8Array): Promise<void>;
|
|
73
96
|
/** Send a file INLINE over the message channel (FileModel JSON envelope,
|
|
74
97
|
* bulkmsg-split) — the only file path native iOS/C Carrier clients can
|
|
75
98
|
* receive online. Use for native friends; JS friends keep sendFile(). */
|
|
@@ -7,8 +7,31 @@ import { EventEmitter } from "events";
|
|
|
7
7
|
import { OfflineCallBridge } from "./offline-call.js";
|
|
8
8
|
import { PacketSession } from "./packet-session.js";
|
|
9
9
|
import { FrameCodec } from "./frame.js";
|
|
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";
|
|
10
|
+
import { FRAME_OPCODE_HANDSHAKE_ACK, PACKET_ID_DL_SESSION, PACKET_ID_DL_DORA, PACKET_ID_DL_IP, PACKET_ID_DL_RATE, isAppPacketId, } from "./types.js";
|
|
11
11
|
import { Logger } from "../utils/logger.js";
|
|
12
|
+
/**
|
|
13
|
+
* True when a Carrier invite send failed for a reason that will recur
|
|
14
|
+
* identically on every retry, so retrying or waiting for a reconnect is
|
|
15
|
+
* pointless and the signal must take another transport or be lost.
|
|
16
|
+
*
|
|
17
|
+
* The one that bit us: the native invite channel caps application data at
|
|
18
|
+
* CARRIER_MAX_INVITE_DATA_LEN (8192, from carrier.h — it is a wire constant
|
|
19
|
+
* shared with the iOS/Android SDK, not something we can raise unilaterally),
|
|
20
|
+
* while a Chrome SDP offer/answer runs 8.4-9.8 KB.
|
|
21
|
+
*/
|
|
22
|
+
export function isPermanentInviteFailure(err) {
|
|
23
|
+
return /invite data must be/i.test(err?.message ?? "");
|
|
24
|
+
}
|
|
25
|
+
/** Best-effort RtcSignal type ("offer"/"answer"/"candidate"/"bye") for logs. */
|
|
26
|
+
function signalKind(data) {
|
|
27
|
+
try {
|
|
28
|
+
const text = typeof data === "string" ? data : Buffer.from(data).toString("utf-8");
|
|
29
|
+
return String(JSON.parse(text).type ?? "signal");
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return "signal";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
12
35
|
export class PeerManager extends EventEmitter {
|
|
13
36
|
peer = null;
|
|
14
37
|
identity = null;
|
|
@@ -142,6 +165,25 @@ export class PeerManager extends EventEmitter {
|
|
|
142
165
|
throw new Error("Peer not created. Call create() first.");
|
|
143
166
|
return this.peer.sendFile(userid, data, { name });
|
|
144
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Send an APPLICATION packet to a friend. The payload is opaque to
|
|
170
|
+
* decentlan — this is the extension point that lets an app (beagle, an
|
|
171
|
+
* agent, any IPC client) run its own protocol over our Carrier session
|
|
172
|
+
* without a change in here.
|
|
173
|
+
*
|
|
174
|
+
* `id` must be in PACKET_ID_APP_MIN..MAX. Rejecting anything else is a
|
|
175
|
+
* security boundary, not tidiness: our own 161-164 carry session handshakes,
|
|
176
|
+
* dora control and IP frames, so a caller allowed to pick those ids could
|
|
177
|
+
* forge a handshake or inject a packet onto a peer's TUN.
|
|
178
|
+
*/
|
|
179
|
+
async sendAppPacket(userid, id, data) {
|
|
180
|
+
if (!this.peer)
|
|
181
|
+
throw new Error("Peer not created. Call create() first.");
|
|
182
|
+
if (!isAppPacketId(id)) {
|
|
183
|
+
throw new Error(`packet id ${id} is outside the application range 165-191`);
|
|
184
|
+
}
|
|
185
|
+
await this.peer.sendCustomPacket(userid, id, data);
|
|
186
|
+
}
|
|
145
187
|
/** Send a file INLINE over the message channel (FileModel JSON envelope,
|
|
146
188
|
* bulkmsg-split) — the only file path native iOS/C Carrier clients can
|
|
147
189
|
* receive online. Use for native friends; JS friends keep sendFile(). */
|
|
@@ -181,7 +223,21 @@ export class PeerManager extends EventEmitter {
|
|
|
181
223
|
// A call already committed to Carrier must NOT split to socket.io on a
|
|
182
224
|
// transient flap (it breaks ICE). Drop this signal; ICE recovers via the
|
|
183
225
|
// other candidates / a retried offer.
|
|
226
|
+
//
|
|
227
|
+
// That reasoning only holds for a TRANSIENT failure. An oversize payload
|
|
228
|
+
// fails identically on every retry, so "ICE recovers via a retried offer"
|
|
229
|
+
// is false — the retry is the same too-big offer. Chrome's SDP runs
|
|
230
|
+
// 8.4-9.8 KB against the 8192-byte native invite cap (carrier.h), which
|
|
231
|
+
// silently killed every desktop→phone call: the offer never left, so the
|
|
232
|
+
// phone never rang, and on an inbound call our answer never left, so it
|
|
233
|
+
// rang and then never connected. Route a permanently-undeliverable signal
|
|
234
|
+
// to the offline bridge instead of dropping it on the floor.
|
|
184
235
|
if (callId && this.offlineBridge?.isOnlineCall(callId)) {
|
|
236
|
+
if (this.offlineBridge.enabled && isPermanentInviteFailure(err)) {
|
|
237
|
+
this.logger.warn(`sendCallSignal: ${signalKind(data)} for call ${callId} cannot go over Carrier (${err.message}) — offline path to ${userid}`);
|
|
238
|
+
await this.offlineBridge.sendSignal(userid, data, hasVideo);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
185
241
|
this.logger.info(`sendCallSignal: dropped a Carrier signal for online call ${callId} (${err.message})`);
|
|
186
242
|
return;
|
|
187
243
|
}
|
|
@@ -650,6 +706,13 @@ export class PeerManager extends EventEmitter {
|
|
|
650
706
|
return;
|
|
651
707
|
}
|
|
652
708
|
this.handleDecodedFrame(pubkey, frame);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
// Application range: hand the bytes up untouched. We do not parse them,
|
|
712
|
+
// so a malformed app payload can never reach any decentlan code path —
|
|
713
|
+
// validating it is the receiving app's job, at its own trust boundary.
|
|
714
|
+
if (isAppPacketId(id)) {
|
|
715
|
+
this.emit("app-packet", pubkey, id, data);
|
|
653
716
|
}
|
|
654
717
|
}
|
|
655
718
|
catch (error) {
|
package/dist/carrier/types.d.ts
CHANGED
|
@@ -12,3 +12,7 @@ export declare const PACKET_ID_DL_SESSION = 161;
|
|
|
12
12
|
export declare const PACKET_ID_DL_DORA = 162;
|
|
13
13
|
export declare const PACKET_ID_DL_IP = 163;
|
|
14
14
|
export declare const PACKET_ID_DL_RATE = 164;
|
|
15
|
+
export declare const PACKET_ID_APP_MIN = 165;
|
|
16
|
+
export declare const PACKET_ID_APP_MAX = 191;
|
|
17
|
+
/** True when `id` is in the application range apps may send and receive on. */
|
|
18
|
+
export declare function isAppPacketId(id: unknown): id is number;
|
package/dist/carrier/types.js
CHANGED
|
@@ -31,3 +31,23 @@ export const PACKET_ID_DL_IP = 163; // lossless: IP data frames (must traverse r
|
|
|
31
31
|
// so it stops overrunning the path and inducing congestion drops. Payload: a
|
|
32
32
|
// single uint32be = delivered bytes/sec. Rare + tiny, so lossless is free.
|
|
33
33
|
export const PACKET_ID_DL_RATE = 164;
|
|
34
|
+
// --- application packet range -----------------------------------------------
|
|
35
|
+
// 165-191 is handed to APPLICATIONS built on this daemon (beagle, agents, any
|
|
36
|
+
// IPC client). decentlan carries these bytes and never interprets them, so an
|
|
37
|
+
// app can add a protocol of its own without a change here — the reason this
|
|
38
|
+
// range exists at all is so no app ever has to modify the SDK again.
|
|
39
|
+
//
|
|
40
|
+
// Lossless (160-191), like everything else we own: an app-level control message
|
|
41
|
+
// that vanishes on a relay-only session is worse than useless. See the note on
|
|
42
|
+
// PACKET_ID_DL_IP for why lossy is not an option on GFW-crossing paths.
|
|
43
|
+
//
|
|
44
|
+
// The range deliberately STARTS above our own ids. An IPC client is local, but
|
|
45
|
+
// it is not necessarily this daemon's code, and letting it emit on 161-164
|
|
46
|
+
// would let it forge session handshakes, dora control, or IP frames at a peer —
|
|
47
|
+
// i.e. inject onto someone's TUN. The bound is enforced on send AND on receive.
|
|
48
|
+
export const PACKET_ID_APP_MIN = 165;
|
|
49
|
+
export const PACKET_ID_APP_MAX = 191;
|
|
50
|
+
/** True when `id` is in the application range apps may send and receive on. */
|
|
51
|
+
export function isAppPacketId(id) {
|
|
52
|
+
return typeof id === "number" && Number.isInteger(id) && id >= PACKET_ID_APP_MIN && id <= PACKET_ID_APP_MAX;
|
|
53
|
+
}
|
package/dist/daemon/ipc.d.ts
CHANGED
|
@@ -94,6 +94,12 @@ export interface IpcHandlers {
|
|
|
94
94
|
* Carrier "carrier" friend-invite extension — the channel iOS/Android Beagle
|
|
95
95
|
* listen on for calls. Backs the desktop UI's audio/video calls. */
|
|
96
96
|
callSignal: (userid: string, data: string) => Promise<void>;
|
|
97
|
+
/** Send an application packet (opaque bytes on a Carrier custom-packet id in
|
|
98
|
+
* the 165-191 app range) to a friend. The extension point for apps built on
|
|
99
|
+
* this daemon: beagle's chat components ride it, and so can anything else,
|
|
100
|
+
* without adding an op per feature. Inbound ones arrive as
|
|
101
|
+
* `{type:"app", userid, packetId, data}` events on `subscribe`. */
|
|
102
|
+
appSend: (userid: string, packetId: number, data: Uint8Array) => Promise<void>;
|
|
97
103
|
/** Long-poll for inbound call-signaling payloads. Resolves with any queued
|
|
98
104
|
* signals immediately, otherwise holds the connection until one arrives or a
|
|
99
105
|
* ~20s timeout elapses (then resolves empty). The UI re-polls to get
|
|
@@ -121,7 +127,7 @@ export interface IpcHandlers {
|
|
|
121
127
|
selfRestart: () => Promise<Record<string, unknown>>;
|
|
122
128
|
}
|
|
123
129
|
export interface IpcRequest {
|
|
124
|
-
op: "friend-request" | "ping" | "diag" | "friends-pending" | "friends-accept" | "friends-reject" | "chat-send" | "chat-log-local" | "file-log-local" | "chat-history" | "friends-list" | "friend-remove" | "friend-set-alias" | "friends-autoaccept" | "set-profile" | "file-send" | "file-delete" | "file-cancel" | "file-retry" | "chat-mark-read" | "subscribe" | "sign" | "call-signal" | "call-poll" | "proxy-reload" | "proxy-access" | "self-restart";
|
|
130
|
+
op: "friend-request" | "ping" | "diag" | "friends-pending" | "friends-accept" | "friends-reject" | "chat-send" | "chat-log-local" | "file-log-local" | "chat-history" | "friends-list" | "friend-remove" | "friend-set-alias" | "friends-autoaccept" | "set-profile" | "file-send" | "file-delete" | "file-cancel" | "file-retry" | "chat-mark-read" | "subscribe" | "sign" | "call-signal" | "call-poll" | "proxy-reload" | "proxy-access" | "app-send" | "self-restart";
|
|
125
131
|
address?: string;
|
|
126
132
|
hello?: string;
|
|
127
133
|
userid?: string;
|
|
@@ -137,8 +143,11 @@ export interface IpcRequest {
|
|
|
137
143
|
ts?: number;
|
|
138
144
|
enabled?: boolean;
|
|
139
145
|
ids?: string[];
|
|
140
|
-
/** RtcSignal JSON payload for the "call-signal" op
|
|
146
|
+
/** RtcSignal JSON payload for the "call-signal" op; base64 payload for
|
|
147
|
+
* "app-send". */
|
|
141
148
|
data?: string;
|
|
149
|
+
/** Carrier custom-packet id for the "app-send" op (165-191). */
|
|
150
|
+
packetId?: number;
|
|
142
151
|
}
|
|
143
152
|
export interface IpcResponseOk {
|
|
144
153
|
ok: true;
|
package/dist/daemon/ipc.js
CHANGED
|
@@ -276,6 +276,24 @@ export class IpcServer {
|
|
|
276
276
|
await this.handlers.callSignal(req.userid, req.data);
|
|
277
277
|
return;
|
|
278
278
|
}
|
|
279
|
+
case "app-send": {
|
|
280
|
+
if (!req.userid)
|
|
281
|
+
throw new Error("userid is required");
|
|
282
|
+
if (typeof req.packetId !== "number")
|
|
283
|
+
throw new Error("packetId is required");
|
|
284
|
+
if (typeof req.data !== "string")
|
|
285
|
+
throw new Error("data is required (base64)");
|
|
286
|
+
// Base64 is the only sane way to carry bytes through a newline-delimited
|
|
287
|
+
// JSON socket. Buffer.from is lenient — it silently skips invalid chars
|
|
288
|
+
// rather than throwing — so re-encode and compare: a caller that sends
|
|
289
|
+
// corrupt base64 must be told, not have truncated bytes put on the wire.
|
|
290
|
+
const bytes = Buffer.from(req.data, "base64");
|
|
291
|
+
if (bytes.toString("base64").replace(/=+$/, "") !== req.data.replace(/=+$/, "")) {
|
|
292
|
+
throw new Error("data is not valid base64");
|
|
293
|
+
}
|
|
294
|
+
await this.handlers.appSend(req.userid, req.packetId, new Uint8Array(bytes));
|
|
295
|
+
return { sent: bytes.length };
|
|
296
|
+
}
|
|
279
297
|
case "call-poll":
|
|
280
298
|
return await this.handlers.callPoll();
|
|
281
299
|
case "proxy-reload":
|
package/dist/daemon/server.js
CHANGED
|
@@ -698,6 +698,11 @@ export class DaemonServer {
|
|
|
698
698
|
// session (throws if the peer is unreachable, so the UI can surface it).
|
|
699
699
|
await this.peerManager.sendCallSignal(userid, data);
|
|
700
700
|
},
|
|
701
|
+
appSend: async (userid, packetId, data) => {
|
|
702
|
+
// Opaque passthrough. The range check lives in PeerManager so it
|
|
703
|
+
// applies to every caller, not just this socket.
|
|
704
|
+
await this.peerManager.sendAppPacket(userid, packetId, data);
|
|
705
|
+
},
|
|
701
706
|
callPoll: async () => {
|
|
702
707
|
// Drain immediately if signals are queued; otherwise hold up to ~20s
|
|
703
708
|
// for one to arrive (near-instant delivery without a WebSocket). The
|
|
@@ -1196,6 +1201,18 @@ export class DaemonServer {
|
|
|
1196
1201
|
for (const wake of waiters)
|
|
1197
1202
|
wake();
|
|
1198
1203
|
});
|
|
1204
|
+
// Application packets (165-191) — push straight to IPC subscribers as
|
|
1205
|
+
// base64. The daemon never parses them: whatever protocol an app runs up
|
|
1206
|
+
// there is the app's business, and keeping it opaque is what lets apps
|
|
1207
|
+
// evolve without touching this SDK.
|
|
1208
|
+
this.peerManager.on("app-packet", (pubkey, packetId, data) => {
|
|
1209
|
+
this.ipcEvents.emit("event", {
|
|
1210
|
+
type: "app",
|
|
1211
|
+
userid: pubkey,
|
|
1212
|
+
packetId,
|
|
1213
|
+
data: Buffer.from(data).toString("base64"),
|
|
1214
|
+
});
|
|
1215
|
+
});
|
|
1199
1216
|
this.peerManager.on("friend-request", (req) => {
|
|
1200
1217
|
void pubkeyHexToUserid(req.pubkey).then((userid) => {
|
|
1201
1218
|
const who = `${req.name || "(unnamed)"} ${userid}`;
|
package/dist/ui/desktop/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
window.__DK_UI_VERSION="0.1.
|
|
1
|
+
window.__DK_UI_VERSION="0.1.257";
|
|
2
2
|
const ICON_PATHS = {
|
|
3
3
|
// ---- tab bar (the four must feel like one set) ----
|
|
4
4
|
users: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
|
|
@@ -2454,6 +2454,40 @@ const CALL_ICE_SERVERS = [
|
|
|
2454
2454
|
{ urls: "turn:tokyo.fi.chat:3478", username: "allcom", credential: "allcompass" },
|
|
2455
2455
|
{ urls: "turn:tokyo.fi.chat:3478?transport=tcp", username: "allcom", credential: "allcompass" }
|
|
2456
2456
|
];
|
|
2457
|
+
const VIDEO_CODEC_KEEP = /^video\/(H264|VP8|rtx|red|ulpfec)$/i;
|
|
2458
|
+
const SDP_BUDGET = 8192;
|
|
2459
|
+
function trimVideoCodecs(pc) {
|
|
2460
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
2461
|
+
const supported = (_c = (_b = (_a = window.RTCRtpSender) == null ? void 0 : _a.getCapabilities) == null ? void 0 : _b.call(_a, "video")) == null ? void 0 : _c.codecs;
|
|
2462
|
+
if (!supported) return;
|
|
2463
|
+
const keep = supported.filter((c) => VIDEO_CODEC_KEEP.test(c.mimeType));
|
|
2464
|
+
if (!keep.length || keep.length === supported.length) return;
|
|
2465
|
+
for (const t of pc.getTransceivers()) {
|
|
2466
|
+
const kind = ((_e = (_d = t.receiver) == null ? void 0 : _d.track) == null ? void 0 : _e.kind) || ((_g = (_f = t.sender) == null ? void 0 : _f.track) == null ? void 0 : _g.kind);
|
|
2467
|
+
if (kind !== "video" || !t.setCodecPreferences) continue;
|
|
2468
|
+
try {
|
|
2469
|
+
t.setCodecPreferences(keep);
|
|
2470
|
+
} catch (e) {
|
|
2471
|
+
console.warn("[call] setCodecPreferences failed", e);
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
function createLeanPeerConnection(config) {
|
|
2476
|
+
const pc = new RTCPeerConnection(config);
|
|
2477
|
+
for (const method of ["createOffer", "createAnswer"]) {
|
|
2478
|
+
const original = pc[method].bind(pc);
|
|
2479
|
+
pc[method] = async (...args) => {
|
|
2480
|
+
trimVideoCodecs(pc);
|
|
2481
|
+
const desc = await original(...args);
|
|
2482
|
+
const bytes = new TextEncoder().encode(desc.sdp || "").length;
|
|
2483
|
+
console[bytes > SDP_BUDGET ? "warn" : "log"](
|
|
2484
|
+
`[call] ${method} SDP ${bytes}B / ${SDP_BUDGET}B budget` + (bytes > SDP_BUDGET ? " \u2014 too large for the Carrier invite channel, falling back to the offline path" : "")
|
|
2485
|
+
);
|
|
2486
|
+
return desc;
|
|
2487
|
+
};
|
|
2488
|
+
}
|
|
2489
|
+
return pc;
|
|
2490
|
+
}
|
|
2457
2491
|
function useCallController(selfId, onCallLog) {
|
|
2458
2492
|
const [incoming, setIncoming] = React.useState(null);
|
|
2459
2493
|
const [active, setActive] = React.useState(null);
|
|
@@ -2476,7 +2510,7 @@ function useCallController(selfId, onCallLog) {
|
|
|
2476
2510
|
const engine = new CallEngine({
|
|
2477
2511
|
selfId,
|
|
2478
2512
|
signaling,
|
|
2479
|
-
createPeerConnection: (c) =>
|
|
2513
|
+
createPeerConnection: (c) => createLeanPeerConnection(c),
|
|
2480
2514
|
getLocalMedia: (k) => navigator.mediaDevices.getUserMedia({ audio: k.audio, video: k.video }),
|
|
2481
2515
|
getDisplayMedia: (c) => navigator.mediaDevices.getDisplayMedia(c || { video: true, audio: false }),
|
|
2482
2516
|
iceServers: CALL_ICE_SERVERS,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.257",
|
|
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",
|