@decentnetwork/lan 0.1.89 → 0.1.91

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.
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @license React
3
+ * react.production.min.js
4
+ *
5
+ * Copyright (c) Facebook, Inc. and its affiliates.
6
+ *
7
+ * This source code is licensed under the MIT license found in the
8
+ * LICENSE file in the root directory of this source tree.
9
+ */
10
+ (function(){'use strict';(function(c,x){"object"===typeof exports&&"undefined"!==typeof module?x(exports):"function"===typeof define&&define.amd?define(["exports"],x):(c=c||self,x(c.React={}))})(this,function(c){function x(a){if(null===a||"object"!==typeof a)return null;a=V&&a[V]||a["@@iterator"];return"function"===typeof a?a:null}function w(a,b,e){this.props=a;this.context=b;this.refs=W;this.updater=e||X}function Y(){}function K(a,b,e){this.props=a;this.context=b;this.refs=W;this.updater=e||X}function Z(a,b,
11
+ e){var m,d={},c=null,h=null;if(null!=b)for(m in void 0!==b.ref&&(h=b.ref),void 0!==b.key&&(c=""+b.key),b)aa.call(b,m)&&!ba.hasOwnProperty(m)&&(d[m]=b[m]);var l=arguments.length-2;if(1===l)d.children=e;else if(1<l){for(var f=Array(l),k=0;k<l;k++)f[k]=arguments[k+2];d.children=f}if(a&&a.defaultProps)for(m in l=a.defaultProps,l)void 0===d[m]&&(d[m]=l[m]);return{$$typeof:y,type:a,key:c,ref:h,props:d,_owner:L.current}}function oa(a,b){return{$$typeof:y,type:a.type,key:b,ref:a.ref,props:a.props,_owner:a._owner}}
12
+ function M(a){return"object"===typeof a&&null!==a&&a.$$typeof===y}function pa(a){var b={"=":"=0",":":"=2"};return"$"+a.replace(/[=:]/g,function(a){return b[a]})}function N(a,b){return"object"===typeof a&&null!==a&&null!=a.key?pa(""+a.key):b.toString(36)}function B(a,b,e,m,d){var c=typeof a;if("undefined"===c||"boolean"===c)a=null;var h=!1;if(null===a)h=!0;else switch(c){case "string":case "number":h=!0;break;case "object":switch(a.$$typeof){case y:case qa:h=!0}}if(h)return h=a,d=d(h),a=""===m?"."+
13
+ N(h,0):m,ca(d)?(e="",null!=a&&(e=a.replace(da,"$&/")+"/"),B(d,b,e,"",function(a){return a})):null!=d&&(M(d)&&(d=oa(d,e+(!d.key||h&&h.key===d.key?"":(""+d.key).replace(da,"$&/")+"/")+a)),b.push(d)),1;h=0;m=""===m?".":m+":";if(ca(a))for(var l=0;l<a.length;l++){c=a[l];var f=m+N(c,l);h+=B(c,b,e,f,d)}else if(f=x(a),"function"===typeof f)for(a=f.call(a),l=0;!(c=a.next()).done;)c=c.value,f=m+N(c,l++),h+=B(c,b,e,f,d);else if("object"===c)throw b=String(a),Error("Objects are not valid as a React child (found: "+
14
+ ("[object Object]"===b?"object with keys {"+Object.keys(a).join(", ")+"}":b)+"). If you meant to render a collection of children, use an array instead.");return h}function C(a,b,e){if(null==a)return a;var c=[],d=0;B(a,c,"","",function(a){return b.call(e,a,d++)});return c}function ra(a){if(-1===a._status){var b=a._result;b=b();b.then(function(b){if(0===a._status||-1===a._status)a._status=1,a._result=b},function(b){if(0===a._status||-1===a._status)a._status=2,a._result=b});-1===a._status&&(a._status=
15
+ 0,a._result=b)}if(1===a._status)return a._result.default;throw a._result;}function O(a,b){var e=a.length;a.push(b);a:for(;0<e;){var c=e-1>>>1,d=a[c];if(0<D(d,b))a[c]=b,a[e]=d,e=c;else break a}}function p(a){return 0===a.length?null:a[0]}function E(a){if(0===a.length)return null;var b=a[0],e=a.pop();if(e!==b){a[0]=e;a:for(var c=0,d=a.length,k=d>>>1;c<k;){var h=2*(c+1)-1,l=a[h],f=h+1,g=a[f];if(0>D(l,e))f<d&&0>D(g,l)?(a[c]=g,a[f]=e,c=f):(a[c]=l,a[h]=e,c=h);else if(f<d&&0>D(g,e))a[c]=g,a[f]=e,c=f;else break a}}return b}
16
+ function D(a,b){var c=a.sortIndex-b.sortIndex;return 0!==c?c:a.id-b.id}function P(a){for(var b=p(r);null!==b;){if(null===b.callback)E(r);else if(b.startTime<=a)E(r),b.sortIndex=b.expirationTime,O(q,b);else break;b=p(r)}}function Q(a){z=!1;P(a);if(!u)if(null!==p(q))u=!0,R(S);else{var b=p(r);null!==b&&T(Q,b.startTime-a)}}function S(a,b){u=!1;z&&(z=!1,ea(A),A=-1);F=!0;var c=k;try{P(b);for(n=p(q);null!==n&&(!(n.expirationTime>b)||a&&!fa());){var m=n.callback;if("function"===typeof m){n.callback=null;
17
+ k=n.priorityLevel;var d=m(n.expirationTime<=b);b=v();"function"===typeof d?n.callback=d:n===p(q)&&E(q);P(b)}else E(q);n=p(q)}if(null!==n)var g=!0;else{var h=p(r);null!==h&&T(Q,h.startTime-b);g=!1}return g}finally{n=null,k=c,F=!1}}function fa(){return v()-ha<ia?!1:!0}function R(a){G=a;H||(H=!0,I())}function T(a,b){A=ja(function(){a(v())},b)}function ka(a){throw Error("act(...) is not supported in production builds of React.");}var y=Symbol.for("react.element"),qa=Symbol.for("react.portal"),sa=Symbol.for("react.fragment"),
18
+ ta=Symbol.for("react.strict_mode"),ua=Symbol.for("react.profiler"),va=Symbol.for("react.provider"),wa=Symbol.for("react.context"),xa=Symbol.for("react.forward_ref"),ya=Symbol.for("react.suspense"),za=Symbol.for("react.memo"),Aa=Symbol.for("react.lazy"),V=Symbol.iterator,X={isMounted:function(a){return!1},enqueueForceUpdate:function(a,b,c){},enqueueReplaceState:function(a,b,c,m){},enqueueSetState:function(a,b,c,m){}},la=Object.assign,W={};w.prototype.isReactComponent={};w.prototype.setState=function(a,
19
+ b){if("object"!==typeof a&&"function"!==typeof a&&null!=a)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,a,b,"setState")};w.prototype.forceUpdate=function(a){this.updater.enqueueForceUpdate(this,a,"forceUpdate")};Y.prototype=w.prototype;var t=K.prototype=new Y;t.constructor=K;la(t,w.prototype);t.isPureReactComponent=!0;var ca=Array.isArray,aa=Object.prototype.hasOwnProperty,L={current:null},
20
+ ba={key:!0,ref:!0,__self:!0,__source:!0},da=/\/+/g,g={current:null},J={transition:null};if("object"===typeof performance&&"function"===typeof performance.now){var Ba=performance;var v=function(){return Ba.now()}}else{var ma=Date,Ca=ma.now();v=function(){return ma.now()-Ca}}var q=[],r=[],Da=1,n=null,k=3,F=!1,u=!1,z=!1,ja="function"===typeof setTimeout?setTimeout:null,ea="function"===typeof clearTimeout?clearTimeout:null,na="undefined"!==typeof setImmediate?setImmediate:null;"undefined"!==typeof navigator&&
21
+ void 0!==navigator.scheduling&&void 0!==navigator.scheduling.isInputPending&&navigator.scheduling.isInputPending.bind(navigator.scheduling);var H=!1,G=null,A=-1,ia=5,ha=-1,U=function(){if(null!==G){var a=v();ha=a;var b=!0;try{b=G(!0,a)}finally{b?I():(H=!1,G=null)}}else H=!1};if("function"===typeof na)var I=function(){na(U)};else if("undefined"!==typeof MessageChannel){t=new MessageChannel;var Ea=t.port2;t.port1.onmessage=U;I=function(){Ea.postMessage(null)}}else I=function(){ja(U,0)};t={ReactCurrentDispatcher:g,
22
+ ReactCurrentOwner:L,ReactCurrentBatchConfig:J,Scheduler:{__proto__:null,unstable_ImmediatePriority:1,unstable_UserBlockingPriority:2,unstable_NormalPriority:3,unstable_IdlePriority:5,unstable_LowPriority:4,unstable_runWithPriority:function(a,b){switch(a){case 1:case 2:case 3:case 4:case 5:break;default:a=3}var c=k;k=a;try{return b()}finally{k=c}},unstable_next:function(a){switch(k){case 1:case 2:case 3:var b=3;break;default:b=k}var c=k;k=b;try{return a()}finally{k=c}},unstable_scheduleCallback:function(a,
23
+ b,c){var e=v();"object"===typeof c&&null!==c?(c=c.delay,c="number"===typeof c&&0<c?e+c:e):c=e;switch(a){case 1:var d=-1;break;case 2:d=250;break;case 5:d=1073741823;break;case 4:d=1E4;break;default:d=5E3}d=c+d;a={id:Da++,callback:b,priorityLevel:a,startTime:c,expirationTime:d,sortIndex:-1};c>e?(a.sortIndex=c,O(r,a),null===p(q)&&a===p(r)&&(z?(ea(A),A=-1):z=!0,T(Q,c-e))):(a.sortIndex=d,O(q,a),u||F||(u=!0,R(S)));return a},unstable_cancelCallback:function(a){a.callback=null},unstable_wrapCallback:function(a){var b=
24
+ k;return function(){var c=k;k=b;try{return a.apply(this,arguments)}finally{k=c}}},unstable_getCurrentPriorityLevel:function(){return k},unstable_shouldYield:fa,unstable_requestPaint:function(){},unstable_continueExecution:function(){u||F||(u=!0,R(S))},unstable_pauseExecution:function(){},unstable_getFirstCallbackNode:function(){return p(q)},get unstable_now(){return v},unstable_forceFrameRate:function(a){0>a||125<a?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):
25
+ ia=0<a?Math.floor(1E3/a):5},unstable_Profiling:null}};c.Children={map:C,forEach:function(a,b,c){C(a,function(){b.apply(this,arguments)},c)},count:function(a){var b=0;C(a,function(){b++});return b},toArray:function(a){return C(a,function(a){return a})||[]},only:function(a){if(!M(a))throw Error("React.Children.only expected to receive a single React element child.");return a}};c.Component=w;c.Fragment=sa;c.Profiler=ua;c.PureComponent=K;c.StrictMode=ta;c.Suspense=ya;c.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=
26
+ t;c.act=ka;c.cloneElement=function(a,b,c){if(null===a||void 0===a)throw Error("React.cloneElement(...): The argument must be a React element, but you passed "+a+".");var e=la({},a.props),d=a.key,k=a.ref,h=a._owner;if(null!=b){void 0!==b.ref&&(k=b.ref,h=L.current);void 0!==b.key&&(d=""+b.key);if(a.type&&a.type.defaultProps)var l=a.type.defaultProps;for(f in b)aa.call(b,f)&&!ba.hasOwnProperty(f)&&(e[f]=void 0===b[f]&&void 0!==l?l[f]:b[f])}var f=arguments.length-2;if(1===f)e.children=c;else if(1<f){l=
27
+ Array(f);for(var g=0;g<f;g++)l[g]=arguments[g+2];e.children=l}return{$$typeof:y,type:a.type,key:d,ref:k,props:e,_owner:h}};c.createContext=function(a){a={$$typeof:wa,_currentValue:a,_currentValue2:a,_threadCount:0,Provider:null,Consumer:null,_defaultValue:null,_globalName:null};a.Provider={$$typeof:va,_context:a};return a.Consumer=a};c.createElement=Z;c.createFactory=function(a){var b=Z.bind(null,a);b.type=a;return b};c.createRef=function(){return{current:null}};c.forwardRef=function(a){return{$$typeof:xa,
28
+ render:a}};c.isValidElement=M;c.lazy=function(a){return{$$typeof:Aa,_payload:{_status:-1,_result:a},_init:ra}};c.memo=function(a,b){return{$$typeof:za,type:a,compare:void 0===b?null:b}};c.startTransition=function(a,b){b=J.transition;J.transition={};try{a()}finally{J.transition=b}};c.unstable_act=ka;c.useCallback=function(a,b){return g.current.useCallback(a,b)};c.useContext=function(a){return g.current.useContext(a)};c.useDebugValue=function(a,b){};c.useDeferredValue=function(a){return g.current.useDeferredValue(a)};
29
+ c.useEffect=function(a,b){return g.current.useEffect(a,b)};c.useId=function(){return g.current.useId()};c.useImperativeHandle=function(a,b,c){return g.current.useImperativeHandle(a,b,c)};c.useInsertionEffect=function(a,b){return g.current.useInsertionEffect(a,b)};c.useLayoutEffect=function(a,b){return g.current.useLayoutEffect(a,b)};c.useMemo=function(a,b){return g.current.useMemo(a,b)};c.useReducer=function(a,b,c){return g.current.useReducer(a,b,c)};c.useRef=function(a){return g.current.useRef(a)};
30
+ c.useState=function(a){return g.current.useState(a)};c.useSyncExternalStore=function(a,b,c){return g.current.useSyncExternalStore(a,b,c)};c.useTransition=function(){return g.current.useTransition()};c.version="18.3.1"});
31
+ })();
@@ -8,6 +8,15 @@ export interface FriendUiOptions {
8
8
  /** If this node also runs a dora server, the path to its roster.yaml — the
9
9
  * UI shows the allocation table (userid → IP → name). Undefined = no dora. */
10
10
  doraRosterPath?: string;
11
+ /** Version/wire strings for the desktop "my node" panel — the daemon's diag
12
+ * doesn't carry them, so the CLI reads them off the installed packages and
13
+ * passes them in. */
14
+ meExtra?: {
15
+ lanVer?: string;
16
+ peerVer?: string;
17
+ wire?: string;
18
+ channel?: string;
19
+ };
11
20
  listenHost?: string;
12
21
  listenPort?: number;
13
22
  log?: (msg: string) => void;
package/dist/ui/server.js CHANGED
@@ -14,7 +14,30 @@
14
14
  //
15
15
  import http from "node:http";
16
16
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
17
+ import { fileURLToPath } from "node:url";
18
+ import { dirname, join } from "node:path";
17
19
  import yaml from "js-yaml";
20
+ // Directory holding the built desktop UI bundle (index.html, app.js, vendor/).
21
+ // scripts/build-ui.mjs emits it next to this compiled module at dist/ui/desktop/.
22
+ const DESKTOP_DIR = join(dirname(fileURLToPath(import.meta.url)), "desktop");
23
+ // ---- shapes the desktop UI (src/ui/desktop) consumes (DK_* in the design) ----
24
+ const fmtTime = (ts) => {
25
+ if (!ts)
26
+ return "";
27
+ const d = new Date(ts);
28
+ const today = new Date();
29
+ const sameDay = d.toDateString() === today.toDateString();
30
+ if (sameDay)
31
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
32
+ const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
33
+ const yest = new Date(today.getTime() - 86400000);
34
+ if (d.toDateString() === yest.toDateString())
35
+ return "Yest";
36
+ if (today.getTime() - d.getTime() < 7 * 86400000)
37
+ return days[d.getDay()];
38
+ return `${d.getDate()} ${["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][d.getMonth()]}`;
39
+ };
40
+ const viaFromTransport = (t) => t === "udp" || t === "both" ? "direct" : t === "tcp-relay" ? "relay" : null;
18
41
  export function startFriendUi(opts) {
19
42
  const host = opts.listenHost ?? "127.0.0.1";
20
43
  const port = opts.listenPort ?? 8765;
@@ -38,11 +61,38 @@ export function startFriendUi(opts) {
38
61
  const server = http.createServer(async (req, res) => {
39
62
  try {
40
63
  const url = (req.url || "/").split("?")[0];
64
+ // Desktop UI bundle (the design). Falls back to the classic page when
65
+ // the bundle hasn't been built (dist/ui/desktop missing).
66
+ const desktopIndex = join(DESKTOP_DIR, "index.html");
41
67
  if (req.method === "GET" && (url === "/" || url === "/index.html")) {
68
+ if (existsSync(desktopIndex)) {
69
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
70
+ res.end(readFileSync(desktopIndex));
71
+ return;
72
+ }
42
73
  res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
43
74
  res.end(PAGE);
44
75
  return;
45
76
  }
77
+ if (req.method === "GET" && (url === "/classic" || url === "/classic.html")) {
78
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
79
+ res.end(PAGE);
80
+ return;
81
+ }
82
+ // Static assets for the desktop bundle (app.js + vendored react UMD).
83
+ if (req.method === "GET" && (url === "/app.js" || url.startsWith("/vendor/"))) {
84
+ const rel = url === "/app.js" ? "app.js" : url.slice(1); // strip leading /
85
+ const file = join(DESKTOP_DIR, rel);
86
+ // Guard against path traversal: resolved file must stay under DESKTOP_DIR.
87
+ if (existsSync(file) && file.startsWith(DESKTOP_DIR)) {
88
+ res.writeHead(200, { "content-type": "application/javascript; charset=utf-8" });
89
+ res.end(readFileSync(file));
90
+ return;
91
+ }
92
+ res.writeHead(404);
93
+ res.end("not found");
94
+ return;
95
+ }
46
96
  if (req.method === "GET" && url === "/api/state") {
47
97
  const [diag, pending] = await Promise.all([
48
98
  opts.call({ op: "diag" }),
@@ -75,6 +125,101 @@ export function startFriendUi(opts) {
75
125
  sendJson(res, 200, { friends });
76
126
  return;
77
127
  }
128
+ // One bootstrap call for the desktop UI: composes the design's DK_*
129
+ // shapes (me / peers / requests / exits) from diag + friends-list +
130
+ // ipam + routes, so the client data layer is a single poll.
131
+ if (req.method === "GET" && url === "/api/desktop") {
132
+ const [diag, pending, flist] = await Promise.all([
133
+ opts.call({ op: "diag" }),
134
+ opts.call({ op: "friends-pending" }),
135
+ opts.call({ op: "friends-list" }),
136
+ ]);
137
+ const d = (diag.ok ? diag.data : {}) ?? {};
138
+ const identity = d.identity ?? {};
139
+ const tun = d.tun ?? {};
140
+ const node = d.node ?? {};
141
+ const diagFriends = d.friends ?? [];
142
+ const ipamList = d.ipam ?? [];
143
+ const ipByUserid = new Map();
144
+ for (const p of ipamList)
145
+ if (p.carrierId)
146
+ ipByUserid.set(p.carrierId, p.virtualIp);
147
+ const sessByUserid = new Map();
148
+ for (const f of diagFriends) {
149
+ const uid = f.carrierId || f.pubkey || "";
150
+ if (uid && f.session)
151
+ sessByUserid.set(uid, f.session);
152
+ }
153
+ const me = {
154
+ name: node.name || (identity.userid ?? "").slice(0, 8),
155
+ handle: node.name ? `@decentnetwork/${node.name}` : "@decentnetwork/peer",
156
+ userId: identity.userid ?? "",
157
+ carrier: identity.address ?? "",
158
+ netKey: identity.userid ?? "",
159
+ ip: d.allocatedIp ?? tun.ip ?? "",
160
+ online: !!tun.ip,
161
+ lanVer: opts.meExtra?.lanVer ?? "",
162
+ peerVer: opts.meExtra?.peerVer ?? "",
163
+ channel: opts.meExtra?.channel ?? "@next",
164
+ wire: opts.meExtra?.wire ?? "163",
165
+ };
166
+ const fl = (flist.ok ? (flist.data?.friends ?? []) : []);
167
+ const peers = fl.map((f) => {
168
+ const uid = f.userid ?? "";
169
+ const sess = sessByUserid.get(uid);
170
+ const status = f.status;
171
+ const online = status === "connected" || status === "online";
172
+ const realName = typeof f.name === "string" && f.name !== uid ? f.name : undefined;
173
+ const lm = f.lastMessage;
174
+ return {
175
+ id: uid,
176
+ alias: f.alias || realName || null,
177
+ userId: uid,
178
+ online,
179
+ via: online ? viaFromTransport(sess?.transport) : null,
180
+ ping: null,
181
+ ip: ipByUserid.get(uid) ?? "",
182
+ unread: f.unread ?? 0,
183
+ agent: false,
184
+ lastMsg: lm ? (lm.dir === "out" ? "you: " : "") + (lm.text ?? "") : "",
185
+ lastTime: fmtTime(lm?.ts),
186
+ wire: "163",
187
+ };
188
+ });
189
+ const pend = pending.ok ? (pending.data?.pending ?? []) : [];
190
+ const requests = pend.map((p, i) => ({
191
+ id: p.userid || p.address || `r${i}`,
192
+ carrier: p.address || p.userid || "",
193
+ userid: p.userid || "",
194
+ via: "lan",
195
+ time: "",
196
+ }));
197
+ // Exit nodes from routes.yaml regions, online-status resolved via ipam.
198
+ let routes = { regions: [], default: "direct" };
199
+ if (existsSync(opts.routesPath)) {
200
+ routes = yaml.load(readFileSync(opts.routesPath, "utf-8")) ?? routes;
201
+ }
202
+ const onlineIps = new Set();
203
+ for (const [uid, ip] of ipByUserid) {
204
+ const s = sessByUserid.get(uid);
205
+ if (s && s.transport && s.transport !== "none")
206
+ onlineIps.add(ip);
207
+ }
208
+ const exits = (routes.regions ?? []).map((r) => ({
209
+ region: r.name,
210
+ flag: (r.name || "?").slice(0, 2).toUpperCase(),
211
+ label: r.name,
212
+ nodes: (r.exits ?? []).map((ip, i) => ({
213
+ ip,
214
+ online: onlineIps.has(ip),
215
+ ping: null,
216
+ host: `${(r.name || "ex").slice(0, 2)}-${String(i + 1).padStart(2, "0")}`,
217
+ })),
218
+ }));
219
+ const activeExit = routes.default && routes.default !== "direct" ? routes.default : null;
220
+ sendJson(res, 200, { me, peers, requests, exits, activeExit });
221
+ return;
222
+ }
78
223
  if (req.method === "GET" && url === "/api/chat-history") {
79
224
  const peer = new URL(req.url || "/", "http://x").searchParams.get("peer") || undefined;
80
225
  const r = await opts.call({ op: "chat-history", userid: peer });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/lan",
3
- "version": "0.1.89",
3
+ "version": "0.1.91",
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",
@@ -58,7 +58,8 @@
58
58
  "access": "public"
59
59
  },
60
60
  "scripts": {
61
- "build": "tsc -p tsconfig.json && chmod +x dist/cli/index.js",
61
+ "build": "tsc -p tsconfig.json && chmod +x dist/cli/index.js && node scripts/build-ui.mjs",
62
+ "build:ui": "node scripts/build-ui.mjs",
62
63
  "build:helper": "cd helper/tun-helper && go build -o ../../bin/tun-helper-$(go env GOOS)-$(go env GOARCH) .",
63
64
  "build:helper:linux-amd64": "cd helper/tun-helper && GOOS=linux GOARCH=amd64 go build -o ../../bin/tun-helper-linux-amd64 .",
64
65
  "build:helper:linux-arm64": "cd helper/tun-helper && GOOS=linux GOARCH=arm64 go build -o ../../bin/tun-helper-linux-arm64 .",
@@ -89,6 +90,8 @@
89
90
  "@typescript-eslint/parser": "^7.10.0",
90
91
  "@vitest/coverage-v8": "^1.6.0",
91
92
  "eslint": "^8.57.0",
93
+ "react": "^18.3.1",
94
+ "react-dom": "^18.3.1",
92
95
  "ts-node": "^10.9.2",
93
96
  "typescript": "^5.4.5",
94
97
  "vitest": "^1.6.0"