dsh-pocket 1.7.4 → 1.8.0
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/README.en.md +5 -0
- package/README.md +5 -0
- package/client/api.js +1 -0
- package/client/client.js +18 -2
- package/client/index.jsx +16 -0
- package/lib/index.js +66 -23
- package/lib/proxy.mjs +12 -2
- package/lib/web-rpc.js +15 -3
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -91,6 +91,11 @@ On the same page click "**Enable anywhere**" → wait for the tunnel (first run
|
|
|
91
91
|
- LAN mode exposes nothing publicly; only devices on the same network can reach it.
|
|
92
92
|
- Built for personal use; the PIN lives in `$DSH_HOME/dsh-pocket/token` and is re-rolled by restarting the public tunnel.
|
|
93
93
|
|
|
94
|
+
## 💻 DSH Desktop
|
|
95
|
+
|
|
96
|
+
- In the desktop app, **QR screen-mirroring works**; **update/restart are managed by the desktop app** (auto-disabled here).
|
|
97
|
+
- ⚠️ The desktop **advanced mode** doesn't support phone access yet (it disables the web layout; the phone gets no layout service → blank screen). Switch back to **compatibility** mode and restart; phones opening an advanced-mode page will see a clear notice overlay.
|
|
98
|
+
|
|
94
99
|
## 🩹 Troubleshooting (traps users step on)
|
|
95
100
|
|
|
96
101
|
| Symptom | Cause & fix |
|
package/README.md
CHANGED
|
@@ -91,6 +91,11 @@ npx @deepseek-ai/dsh web
|
|
|
91
91
|
- 局域网模式不暴露公网,只有同一网络内的设备能访问
|
|
92
92
|
- 适合个人自用;密码存本机 `$DSH_HOME/dsh-pocket/token`,可随时重开公网换新
|
|
93
93
|
|
|
94
|
+
## 💻 DSH Desktop(桌面版)
|
|
95
|
+
|
|
96
|
+
- 桌面版里 dsh-pocket 的**扫码同屏**正常可用;**更新/重启由桌面版管理**(插件内这两项自动停用)
|
|
97
|
+
- ⚠️ 桌面端 **advanced 模式**暂不支持手机访问(该模式禁用网页布局、手机拿不到 layout 服务,会白屏)——请切回 **compatibility** 模式后重启;advanced 模式下手机打开会看到明确的提示层
|
|
98
|
+
|
|
94
99
|
## 🩹 常见问题(别踩的坑)
|
|
95
100
|
|
|
96
101
|
| 现象 | 原因与解决 |
|
package/client/api.js
CHANGED
package/client/client.js
CHANGED
|
@@ -47,7 +47,8 @@ var POCKET_ENDPOINTS = Object.freeze({
|
|
|
47
47
|
tunnelStop: "tunnel.stop",
|
|
48
48
|
version: "pocket.version",
|
|
49
49
|
update: "pocket.update",
|
|
50
|
-
restart: "pocket.restart"
|
|
50
|
+
restart: "pocket.restart",
|
|
51
|
+
lanTokenRefresh: "token.lanRefresh"
|
|
51
52
|
});
|
|
52
53
|
function compareVersions(a, b) {
|
|
53
54
|
const pa = String(a).replace(/^[vV]/, "").split(".");
|
|
@@ -1429,6 +1430,13 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1429
1430
|
} catch {
|
|
1430
1431
|
}
|
|
1431
1432
|
};
|
|
1433
|
+
const refreshLanPin = async () => {
|
|
1434
|
+
try {
|
|
1435
|
+
const r = await call(POCKET_ENDPOINTS.lanTokenRefresh, {});
|
|
1436
|
+
setStatus((s) => ({ ...s, lanToken: r.lanToken }));
|
|
1437
|
+
} catch {
|
|
1438
|
+
}
|
|
1439
|
+
};
|
|
1432
1440
|
const lanUrl = status?.lanUrl;
|
|
1433
1441
|
const tunnelUrl = status?.tunnelUrl;
|
|
1434
1442
|
const tunnelPhase = tunnelState?.phase ?? "idle";
|
|
@@ -1503,7 +1511,15 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1503
1511
|
null,
|
|
1504
1512
|
(0, import_react2.createElement)("img", { src: status.lanQr, alt: "LAN QR", style: styles.qr }),
|
|
1505
1513
|
(0, import_react2.createElement)("div", { style: styles.code }, lanUrl),
|
|
1506
|
-
(0, import_react2.createElement)("div", { style: styles.muted }, "\u624B\u673A\u8FDE\u63A5\u540C\u4E00 WiFi \u540E\u626B\u7801\u5373\u53EF\u6253\u5F00")
|
|
1514
|
+
(0, import_react2.createElement)("div", { style: styles.muted }, "\u624B\u673A\u8FDE\u63A5\u540C\u4E00 WiFi \u540E\u626B\u7801\u5373\u53EF\u6253\u5F00"),
|
|
1515
|
+
status.lanToken ? (0, import_react2.createElement)(
|
|
1516
|
+
"div",
|
|
1517
|
+
{ style: { marginTop: 6, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)", lineHeight: 1.5 } },
|
|
1518
|
+
"\u{1F510} \u8BBF\u95EE\u5BC6\u7801\uFF1A",
|
|
1519
|
+
status.lanToken,
|
|
1520
|
+
"\uFF08\u624B\u673A\u6253\u5F00\u9700\u8F93\u5165\uFF1B\u4E0E\u516C\u7F51\u5BC6\u7801\u5206\u5F00\uFF09",
|
|
1521
|
+
(0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12, marginLeft: 8 }, onClick: refreshLanPin }, "\u5237\u65B0 | Refresh")
|
|
1522
|
+
) : null
|
|
1507
1523
|
) : (0, import_react2.createElement)("div", { style: styles.muted }, "\u4EE3\u7406\u672A\u5C31\u7EEA\u2026 | proxy starting\u2026")
|
|
1508
1524
|
),
|
|
1509
1525
|
// 公网
|
package/client/index.jsx
CHANGED
|
@@ -167,6 +167,14 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
167
167
|
try { setStatus(await call(POCKET_ENDPOINTS.tunnelStop, {})); } catch { /* 忽略 */ }
|
|
168
168
|
};
|
|
169
169
|
|
|
170
|
+
// 刷新局域网访问密码(旧密码立即作废)
|
|
171
|
+
const refreshLanPin = async () => {
|
|
172
|
+
try {
|
|
173
|
+
const r = await call(POCKET_ENDPOINTS.lanTokenRefresh, {});
|
|
174
|
+
setStatus((s) => ({ ...s, lanToken: r.lanToken }));
|
|
175
|
+
} catch { /* 忽略 */ }
|
|
176
|
+
};
|
|
177
|
+
|
|
170
178
|
const lanUrl = status?.lanUrl;
|
|
171
179
|
const tunnelUrl = status?.tunnelUrl;
|
|
172
180
|
const tunnelPhase = tunnelState?.phase ?? 'idle';
|
|
@@ -235,6 +243,14 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
235
243
|
h('img', { src: status.lanQr, alt: 'LAN QR', style: styles.qr }),
|
|
236
244
|
h('div', { style: styles.code }, lanUrl),
|
|
237
245
|
h('div', { style: styles.muted }, '手机连接同一 WiFi 后扫码即可打开'),
|
|
246
|
+
status.lanToken
|
|
247
|
+
? h('div', { style: { marginTop: 6, fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)', lineHeight: 1.5 } },
|
|
248
|
+
'🔐 访问密码:',
|
|
249
|
+
status.lanToken,
|
|
250
|
+
'(手机打开需输入;与公网密码分开)',
|
|
251
|
+
h('button', { style: { ...styles.btn, height: 26, padding: '0 10px', fontSize: 12, marginLeft: 8 }, onClick: refreshLanPin }, '刷新 | Refresh'),
|
|
252
|
+
)
|
|
253
|
+
: null,
|
|
238
254
|
)
|
|
239
255
|
: h('div', { style: styles.muted }, '代理未就绪… | proxy starting…'),
|
|
240
256
|
),
|
package/lib/index.js
CHANGED
|
@@ -19,7 +19,7 @@ import { homedir } from 'node:os';
|
|
|
19
19
|
import { createPocketService } from './service.mjs';
|
|
20
20
|
import { installPocketRpc } from './web-rpc.js';
|
|
21
21
|
import { restartHost } from './restart.js';
|
|
22
|
-
import { desktopEnvPatchScript, DEFAULT_INJECT } from './proxy.mjs';
|
|
22
|
+
import { desktopEnvPatchScript, advancedNoticeScript, DEFAULT_INJECT } from './proxy.mjs';
|
|
23
23
|
|
|
24
24
|
const name = 'dsh-pocket';
|
|
25
25
|
const inject = ['connection', 'webServer'];
|
|
@@ -41,33 +41,58 @@ function currentVersion() {
|
|
|
41
41
|
/** 进程启动时加载的版本(模块加载瞬间固化;用于识别「磁盘已更新但进程还是旧代码」)。 */
|
|
42
42
|
const loadedVersion = currentVersion();
|
|
43
43
|
|
|
44
|
-
// ----------
|
|
45
|
-
// 8 位数字,存本机 $DSH_HOME/dsh-pocket
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
function
|
|
49
|
-
return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), tokenRel);
|
|
50
|
-
}
|
|
51
|
-
/** 生成 8 位数字密码并写文件(同步,保证后续读取一致)。 */
|
|
52
|
-
function writeNewToken() {
|
|
53
|
-
const fresh = String(Math.floor(10000000 + Math.random() * 90000000));
|
|
44
|
+
// ---------- 访问密码(issue #13 + #18) ----------
|
|
45
|
+
// 公网与局域网**分开**:各自 8 位数字,存本机 $DSH_HOME/dsh-pocket/。
|
|
46
|
+
// 公网密码(token):每次开启公网时轮换(旧链接作废);
|
|
47
|
+
// 局域网密码(token-lan):手动刷新(设置页按钮)。
|
|
48
|
+
function writePinToFile(p, fresh) {
|
|
54
49
|
try {
|
|
55
|
-
mkdirSync(dirname(
|
|
56
|
-
writeFileSync(
|
|
50
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
51
|
+
writeFileSync(p, fresh, { mode: 0o600 });
|
|
57
52
|
} catch { /* 忽略 */ }
|
|
58
53
|
return fresh;
|
|
59
54
|
}
|
|
60
|
-
|
|
61
|
-
function getAccessToken() {
|
|
55
|
+
function readPin(p) {
|
|
62
56
|
try {
|
|
63
|
-
const existing = readFileSync(
|
|
57
|
+
const existing = readFileSync(p, 'utf8').trim();
|
|
64
58
|
if (/^\d{8}$/.test(existing)) return existing;
|
|
65
59
|
} catch { /* 无文件 */ }
|
|
66
|
-
return
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
function newPin() {
|
|
63
|
+
return String(Math.floor(10000000 + Math.random() * 90000000));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// --- 公网密码 ---
|
|
67
|
+
const tokenRel = join('dsh-pocket', 'token');
|
|
68
|
+
function tokenPath() {
|
|
69
|
+
return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), tokenRel);
|
|
70
|
+
}
|
|
71
|
+
/** 当前公网访问密码;文件不存在时生成一个。 */
|
|
72
|
+
function getAccessToken() {
|
|
73
|
+
return readPin(tokenPath()) ?? writePinToFile(tokenPath(), newPin());
|
|
67
74
|
}
|
|
68
|
-
/**
|
|
75
|
+
/** 轮换公网密码(开启公网时调用,旧密码/链接立即作废)。 */
|
|
69
76
|
function rotateAccessToken() {
|
|
70
|
-
return
|
|
77
|
+
return writePinToFile(tokenPath(), newPin());
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --- 局域网密码(issue #18:局域网与公网分开,手动刷新) ---
|
|
81
|
+
const lanTokenRel = join('dsh-pocket', 'token-lan');
|
|
82
|
+
function lanTokenPath() {
|
|
83
|
+
return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), lanTokenRel);
|
|
84
|
+
}
|
|
85
|
+
/** 当前局域网访问密码;文件不存在时生成一个。 */
|
|
86
|
+
function getLanToken() {
|
|
87
|
+
return readPin(lanTokenPath()) ?? writePinToFile(lanTokenPath(), newPin());
|
|
88
|
+
}
|
|
89
|
+
/** 刷新局域网密码(设置页按钮触发,旧密码立即作废)。 */
|
|
90
|
+
function refreshLanToken() {
|
|
91
|
+
return writePinToFile(lanTokenPath(), newPin());
|
|
92
|
+
}
|
|
93
|
+
/** 按 Host 分发的访问密码:公网隧道用公网密码,局域网用局域网密码。 */
|
|
94
|
+
function tokenForHost(host) {
|
|
95
|
+
return /trycloudflare\.com$/i.test(String(host ?? '')) ? getAccessToken() : getLanToken();
|
|
71
96
|
}
|
|
72
97
|
|
|
73
98
|
const restartNoticeRel = join('dsh-pocket', 'restarted.json');
|
|
@@ -152,6 +177,19 @@ export function apply(ctx, config = {}, internals = {}) {
|
|
|
152
177
|
logger.info('dsh-pocket: DSH Desktop detected — update/restart disabled here | 检测到桌面端环境,更新/重启已关闭');
|
|
153
178
|
}
|
|
154
179
|
|
|
180
|
+
// 桌面端 advanced 模式检测(issue #19):dsh-plugin-desktop 的 mode 配置存在
|
|
181
|
+
// $DSH_HOME/settings.yaml 的 dsh-plugin-desktop 命名空间下。advanced 组合禁用网页版
|
|
182
|
+
// ui-layout、手机页面又拿不到桌面 layout → 手机访问白屏,这里注入覆盖层提示用户切回。
|
|
183
|
+
const desktopAdvanced = isDesktop && (() => {
|
|
184
|
+
try {
|
|
185
|
+
const raw = readFileSync(join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'settings.yaml'), 'utf8');
|
|
186
|
+
return /dsh-plugin-desktop\s*:\s*[\s\S]{0,300}?mode\s*:\s*advanced/i.test(raw);
|
|
187
|
+
} catch { return false; }
|
|
188
|
+
})();
|
|
189
|
+
if (desktopAdvanced) {
|
|
190
|
+
logger.warn('dsh-pocket: DSH Desktop advanced mode — phone access unsupported, injecting notice | 桌面端 advanced 模式:手机访问暂不支持,已注入提示');
|
|
191
|
+
}
|
|
192
|
+
|
|
155
193
|
const service = internals.service ?? createPocketService({
|
|
156
194
|
dshPort,
|
|
157
195
|
port: internals.port ?? config.port ?? 3081,
|
|
@@ -160,11 +198,14 @@ export function apply(ctx, config = {}, internals = {}) {
|
|
|
160
198
|
// 桌面端:手机扫码访问的页面缺 dsh-desktop-mode/platform 参数会让 dsh-plugin-desktop
|
|
161
199
|
// client 崩溃(issue #3/#4)。给代理注入「桌面参数补丁」(history.replaceState 补齐参数,
|
|
162
200
|
// 无跳转;compatibility 模式不套桌面布局,避免移动端按钮叠加)。保留默认 polyfill。
|
|
163
|
-
|
|
164
|
-
|
|
201
|
+
// advanced 模式再加警告覆盖层(issue #19)。
|
|
202
|
+
injectHtml: isDesktop
|
|
203
|
+
? DEFAULT_INJECT + desktopEnvPatchScript(process.platform) + (desktopAdvanced ? advancedNoticeScript() : '')
|
|
204
|
+
: undefined,
|
|
205
|
+
// 访问密码(issue #13 + #18):局域网与公网**分开**,所有代理访问都要验证
|
|
165
206
|
auth: {
|
|
166
|
-
getToken: () =>
|
|
167
|
-
isProtected: (
|
|
207
|
+
getToken: (host) => tokenForHost(host),
|
|
208
|
+
isProtected: () => true,
|
|
168
209
|
},
|
|
169
210
|
// 每次公网隧道就绪 → 轮换 8 位密码(旧密码/旧链接立即作废)
|
|
170
211
|
onTunnelReady: () => {
|
|
@@ -179,6 +220,8 @@ export function apply(ctx, config = {}, internals = {}) {
|
|
|
179
220
|
service,
|
|
180
221
|
desktop: isDesktop,
|
|
181
222
|
getToken: () => getAccessToken(),
|
|
223
|
+
getLanToken: () => getLanToken(),
|
|
224
|
+
refreshLanToken: () => refreshLanToken(),
|
|
182
225
|
runUpdate: internals.runUpdate ?? { currentVersion, perform: performUpdate, loadedVersion: () => loadedVersion },
|
|
183
226
|
restart: internals.restart ?? (() => pocketRestart(service)),
|
|
184
227
|
restartNotice: internals.restartNotice ?? consumeRestartNotice,
|
package/lib/proxy.mjs
CHANGED
|
@@ -52,6 +52,16 @@ function isCompressed(headers) {
|
|
|
52
52
|
/** 默认注入到经代理的 HTML 文档里:crypto.randomUUID polyfill(非安全上下文必需)。 */
|
|
53
53
|
export const DEFAULT_INJECT = RANDOM_UUID_POLYFILL;
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* DSH Desktop advanced 模式不支持的提示覆盖层(issue #19)。
|
|
57
|
+
* advanced 组合会禁用网页版 ui-layout,而桌面 layout 只在 advanced client 提供——
|
|
58
|
+
* 手机页面被注入 compatibility 后无任何 layout 服务 → 启动白屏(Failed to load plugins)。
|
|
59
|
+
* 该脚本在页面上叠加一个固定警告层,让用户明确知道原因(而不是无解白屏)。
|
|
60
|
+
*/
|
|
61
|
+
export function advancedNoticeScript() {
|
|
62
|
+
return `<script data-dsh-pocket-advanced-notice="1">!function(){try{var d=document.createElement('div');d.style.cssText='position:fixed;inset:0;z-index:2147483647;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.55);color:#fff;font:15px/1.7 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;text-align:center;padding:24px';d.textContent='DSH 桌面端处于 advanced 模式,手机访问暂不支持。请在桌面端设置中切回 compatibility 模式后重启。| DSH Desktop is in advanced mode — phone access is not supported yet. Switch back to compatibility in the desktop app and restart.';document.documentElement.appendChild(d);}catch(e){}}();</script>`;
|
|
63
|
+
}
|
|
64
|
+
|
|
55
65
|
// ---------- 可选访问令牌认证(issue #13) ----------
|
|
56
66
|
// 只对公网隧道 Host(trycloudflare.com)强制;局域网免密码。
|
|
57
67
|
// 登录成功后种 HttpOnly 会话 cookie(浏览器关闭失效)→ SPA 内部 API/WS 自动携带。
|
|
@@ -135,7 +145,7 @@ export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DE
|
|
|
135
145
|
if (auth) {
|
|
136
146
|
const host = String(req.headers.host ?? '');
|
|
137
147
|
const protectedHost = isProtectedHost(host, auth.isProtected);
|
|
138
|
-
const token = protectedHost ? (auth.getToken?.() ?? null) : null;
|
|
148
|
+
const token = protectedHost ? (auth.getToken?.(host) ?? null) : null;
|
|
139
149
|
if (protectedHost && token) {
|
|
140
150
|
// 登录提交:校验密码 → 种 HttpOnly 会话 cookie → 回首页
|
|
141
151
|
if (req.method === 'POST' && req.url?.startsWith('/pocket-login')) {
|
|
@@ -248,7 +258,7 @@ export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DE
|
|
|
248
258
|
// WebSocket 同样校验(防止绕过 HTTP 认证从 WS 进入)
|
|
249
259
|
if (auth) {
|
|
250
260
|
const host = String(req.headers.host ?? '');
|
|
251
|
-
const token = isProtectedHost(host, auth.isProtected) ? (auth.getToken?.() ?? null) : null;
|
|
261
|
+
const token = isProtectedHost(host, auth.isProtected) ? (auth.getToken?.(host) ?? null) : null;
|
|
252
262
|
if (token && !authCheck(req, token)) {
|
|
253
263
|
socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
|
|
254
264
|
socket.destroy();
|
package/lib/web-rpc.js
CHANGED
|
@@ -25,7 +25,7 @@ export function killHint(port) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/** 注册 /dsh-pocket 逻辑通道(仅本机 loopback 可调)。 */
|
|
28
|
-
export function installPocketRpc(ctx, { service, log = console, desktop = false, runUpdate = null, restart = null, restartNotice = null, getToken = null }) {
|
|
28
|
+
export function installPocketRpc(ctx, { service, log = console, desktop = false, runUpdate = null, restart = null, restartNotice = null, getToken = null, getLanToken = null, refreshLanToken = null }) {
|
|
29
29
|
if (!ctx?.connection?.rpc?.handle) {
|
|
30
30
|
log.warn?.('dsh-pocket: DSH Host Connection RPC unavailable — settings tab disabled | 无 Connection RPC,设置页不可用');
|
|
31
31
|
return () => {};
|
|
@@ -33,18 +33,30 @@ export function installPocketRpc(ctx, { service, log = console, desktop = false,
|
|
|
33
33
|
return ctx.connection.rpc.handle(POCKET_RPC_CHANNEL, async (endpoint, payload = {}, signal) => {
|
|
34
34
|
if (signal?.aborted) return fail('cancelled', 'The request was cancelled.');
|
|
35
35
|
|
|
36
|
-
// status
|
|
36
|
+
// status 响应:服务状态 + 重启提示 + 停止命令 + 桌面端标志 + 公网/局域网访问密码
|
|
37
37
|
const statusPayload = async () => {
|
|
38
38
|
let notice = null;
|
|
39
39
|
try { notice = (await restartNotice?.()) ?? null; } catch { notice = null; }
|
|
40
40
|
const s = await service.status();
|
|
41
|
-
return ok({
|
|
41
|
+
return ok({
|
|
42
|
+
...redactStatus(s),
|
|
43
|
+
desktop,
|
|
44
|
+
restartNotice: notice,
|
|
45
|
+
killHint: killHint(s.dshPort ?? 3080),
|
|
46
|
+
accessToken: getToken?.() ?? null,
|
|
47
|
+
lanToken: getLanToken?.() ?? null,
|
|
48
|
+
});
|
|
42
49
|
};
|
|
43
50
|
|
|
44
51
|
try {
|
|
45
52
|
if (endpoint === POCKET_ENDPOINTS.status) {
|
|
46
53
|
return await statusPayload();
|
|
47
54
|
}
|
|
55
|
+
if (endpoint === POCKET_ENDPOINTS.lanTokenRefresh) {
|
|
56
|
+
const fresh = refreshLanToken?.() ?? null;
|
|
57
|
+
if (!fresh) return fail('bad-request', '局域网密码刷新不可用 | LAN PIN refresh unavailable');
|
|
58
|
+
return ok({ lanToken: fresh });
|
|
59
|
+
}
|
|
48
60
|
if (endpoint === POCKET_ENDPOINTS.tunnelStart) {
|
|
49
61
|
await service.startTunnel();
|
|
50
62
|
return await statusPayload();
|
package/package.json
CHANGED