dsh-remote-mobile 1.1.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/lib/index.d.ts ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * DeepSeek Hub (DSH) 移动端远程控制与网络安全门禁插件
3
+ *
4
+ * 【插件主入口】
5
+ * 本插件为 DSH 提供完整的远程接入与安全治理体系,包括:
6
+ * 1. RSA 2048 端到端传输加密与防中间人窃听;
7
+ * 2. 6 位一次性安全配对码与 365 天长效会话 Token;
8
+ * 3. 自定义高强度长期密码与加盐哈希存储;
9
+ * 4. Tailscale 虚拟私网与局域网 LAN 智能免密直连切换;
10
+ * 5. 防暴力破解限流与单 IP 异常访问熔断审计;
11
+ * 6. 全链路网络上下文虚拟化桥接与 @linxin666 生态无缝兼容。
12
+ * 7. 接入 DSH Settings 服务,配置统一持久化于 ~/.dsh/settings.yaml 的 dsh-remote-mobile 命名空间。
13
+ */
14
+ import { type SessionStoreOptions } from './auth/token.js';
15
+ /** Cordis 插件名称标识 */
16
+ export declare const name = "dsh-remote-mobile";
17
+ /** DSH 设置命名空间 */
18
+ export declare const REMOTE_MOBILE_SETTINGS_NAMESPACE = "dsh-remote-mobile";
19
+ /** Cordis 依赖注入声明:声明注入底座内置的 webServer 服务 */
20
+ export declare const inject: string[];
21
+ /**
22
+ * 插件生命周期挂载入口函数
23
+ *
24
+ * @param ctx - Cordis 应用上下文
25
+ * @param config - 插件持久化配置选项 (SessionStoreOptions)
26
+ */
27
+ export declare function apply(ctx: any, config?: SessionStoreOptions): void;
28
+ export * from './auth/tailscale.js';
29
+ export * from './auth/token.js';
30
+ export * from './auth/gate.js';
31
+ export * from './routes/api.js';
32
+ export * from './bridge/compat.js';
package/lib/index.js ADDED
@@ -0,0 +1,117 @@
1
+ /**
2
+ * DeepSeek Hub (DSH) 移动端远程控制与网络安全门禁插件
3
+ *
4
+ * 【插件主入口】
5
+ * 本插件为 DSH 提供完整的远程接入与安全治理体系,包括:
6
+ * 1. RSA 2048 端到端传输加密与防中间人窃听;
7
+ * 2. 6 位一次性安全配对码与 365 天长效会话 Token;
8
+ * 3. 自定义高强度长期密码与加盐哈希存储;
9
+ * 4. Tailscale 虚拟私网与局域网 LAN 智能免密直连切换;
10
+ * 5. 防暴力破解限流与单 IP 异常访问熔断审计;
11
+ * 6. 全链路网络上下文虚拟化桥接与 @linxin666 生态无缝兼容。
12
+ * 7. 接入 DSH Settings 服务,配置统一持久化于 ~/.dsh/settings.yaml 的 dsh-remote-mobile 命名空间。
13
+ */
14
+ import { SessionStore } from './auth/token.js';
15
+ import { createGlobalAuthGate } from './auth/gate.js';
16
+ import { createRoutes } from './routes/api.js';
17
+ import { getLocalTailscaleIp } from './auth/tailscale.js';
18
+ import { patchHttpServerWithVirtualizer, mountIndexInjections, registerRemoteWebUiPairingBridge, } from './bridge/compat.js';
19
+ /** Cordis 插件名称标识 */
20
+ export const name = 'dsh-remote-mobile';
21
+ /** DSH 设置命名空间 */
22
+ export const REMOTE_MOBILE_SETTINGS_NAMESPACE = 'dsh-remote-mobile';
23
+ /** Cordis 依赖注入声明:声明注入底座内置的 webServer 服务 */
24
+ export const inject = ['webServer'];
25
+ /**
26
+ * 注册并绑定 DSH Settings 服务(若存在)
27
+ */
28
+ function bindDshSettings(ctx, store) {
29
+ try {
30
+ let settingsService = null;
31
+ if (typeof ctx.get === 'function') {
32
+ settingsService = ctx.get('settings');
33
+ }
34
+ if (!settingsService?.register)
35
+ return;
36
+ const ns = REMOTE_MOBILE_SETTINGS_NAMESPACE;
37
+ const scope = settingsService.register(ns, undefined, {
38
+ base: store.getOptions(),
39
+ });
40
+ if (scope) {
41
+ // 1. 同步 settings.yaml 中的最新配置到 SessionStore
42
+ const initialVal = scope.get?.();
43
+ if (initialVal && typeof initialVal === 'object') {
44
+ store.updateOptions(initialVal, false);
45
+ }
46
+ // 2. 监听 settings.yaml 或 Web UI 配置变更
47
+ scope.watch?.(() => {
48
+ const updated = scope.get?.();
49
+ if (updated && typeof updated === 'object') {
50
+ store.updateOptions(updated, false);
51
+ }
52
+ });
53
+ // 3. 绑定反向写入钩子:当插件 API 修改配置时同步写回 settings.yaml
54
+ store.setSettingsMutator((patch) => {
55
+ try {
56
+ const ops = Object.entries(patch).map(([field, value]) => ({
57
+ op: 'set',
58
+ path: [field],
59
+ value,
60
+ }));
61
+ settingsService.mutate?.(ns, ops);
62
+ }
63
+ catch { }
64
+ });
65
+ }
66
+ }
67
+ catch { }
68
+ }
69
+ /**
70
+ * 插件生命周期挂载入口函数
71
+ *
72
+ * @param ctx - Cordis 应用上下文
73
+ * @param config - 插件持久化配置选项 (SessionStoreOptions)
74
+ */
75
+ export function apply(ctx, config = {}) {
76
+ // 1. 初始化会话与令牌持久化管理器
77
+ const store = new SessionStore(config);
78
+ const gateMiddleware = createGlobalAuthGate(store);
79
+ // 2. 接入 DSH Settings 服务 (命名空间: dsh-remote-mobile)
80
+ bindDshSettings(ctx, store);
81
+ // 3. 挂载底座 WebServer 相关的门禁拦截与路由注册
82
+ if (ctx.webServer) {
83
+ if (ctx.webServer.server) {
84
+ patchHttpServerWithVirtualizer(ctx.webServer.server, gateMiddleware, store);
85
+ }
86
+ // 挂载 HTML 模板层的 Crypto Polyfill 注入钩子
87
+ mountIndexInjections(ctx);
88
+ // 注册插件专属的 HTTP & SSE API 路由
89
+ if (typeof ctx.webServer.register === 'function') {
90
+ const routes = createRoutes(store);
91
+ for (const route of routes) {
92
+ ctx.webServer.register(route);
93
+ }
94
+ }
95
+ }
96
+ // 4. 向 Cordis 容器桥接注册 remoteWebUiPairing 服务
97
+ // 完美兼容 @linxin666 生态插件(如 dsh-pet 宠物、任务看板等),实现免登信任自动共享
98
+ registerRemoteWebUiPairingBridge(ctx, store);
99
+ // 5. 延迟补丁与服务启动日志输出
100
+ setTimeout(() => {
101
+ if (ctx.webServer && ctx.webServer.server) {
102
+ patchHttpServerWithVirtualizer(ctx.webServer.server, gateMiddleware, store);
103
+ }
104
+ const tsIp = getLocalTailscaleIp();
105
+ const opts = store.getOptions();
106
+ ctx.logger?.info?.(`[dsh-remote-mobile] 移动端远程控制与安全门禁已激活 (上下文虚拟化桥接就绪)`);
107
+ if (tsIp) {
108
+ ctx.logger?.info?.(`[dsh-remote-mobile] Tailscale 地址: http://${tsIp}:3080 (免密直连: ${opts.allowTailscale ? '已开启' : '已关闭'})`);
109
+ }
110
+ }, 300);
111
+ }
112
+ // 导出所有子模块类型与核心工具
113
+ export * from './auth/tailscale.js';
114
+ export * from './auth/token.js';
115
+ export * from './auth/gate.js';
116
+ export * from './routes/api.js';
117
+ export * from './bridge/compat.js';
@@ -0,0 +1,11 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { type SessionStore } from '../auth/token.js';
3
+ export declare function createRoutes(store: SessionStore): ({
4
+ method: string;
5
+ path: string;
6
+ handler: (_req: IncomingMessage, res: ServerResponse) => Promise<void>;
7
+ } | {
8
+ method: string;
9
+ path: string;
10
+ handler: (req: IncomingMessage, res: ServerResponse) => Promise<ServerResponse<IncomingMessage> | undefined>;
11
+ })[];
@@ -0,0 +1,364 @@
1
+ import { AUTH_COOKIE_NAME, readGlobalLocale } from '../auth/token.js';
2
+ import { getLocalTailscaleIp, getLocalLanIp, getAllNetworkIps, getClientIp, isTailscaleIp, isLanIp } from '../auth/tailscale.js';
3
+ import { getPublicKeyPem, decryptWithPrivateKey, validateSecretStrength, RSA_KEY_FILE } from '../auth/crypto.js';
4
+ import { getLoginPageHtml } from './login-page.js';
5
+ function jsonResponse(res, status, data) {
6
+ res.writeHead(status, {
7
+ 'Content-Type': 'application/json; charset=utf-8',
8
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
9
+ });
10
+ res.end(JSON.stringify(data));
11
+ }
12
+ function getRealClientIp(req) {
13
+ const real = req.headers['x-dsh-real-ip'];
14
+ if (typeof real === 'string' && real)
15
+ return real;
16
+ return getClientIp(req);
17
+ }
18
+ function parseJsonBody(req) {
19
+ return new Promise((resolve) => {
20
+ let body = '';
21
+ req.on('data', (chunk) => {
22
+ body += chunk;
23
+ if (body.length > 1e5)
24
+ req.destroy();
25
+ });
26
+ req.on('end', () => {
27
+ try {
28
+ resolve(JSON.parse(body || '{}'));
29
+ }
30
+ catch {
31
+ resolve({});
32
+ }
33
+ });
34
+ req.on('error', () => resolve({}));
35
+ });
36
+ }
37
+ export function createRoutes(store) {
38
+ return [
39
+ // 0. RSA 公钥获取接口 (RSA-OAEP-SHA256)
40
+ {
41
+ method: 'GET',
42
+ path: '/api/remote-mobile/public-key',
43
+ handler: async (_req, res) => {
44
+ jsonResponse(res, 200, {
45
+ success: true,
46
+ publicKey: getPublicKeyPem(),
47
+ algorithm: 'RSA-OAEP-SHA256',
48
+ });
49
+ },
50
+ },
51
+ // 1. 状态查询接口 (返回 Tailscale IP、局域网 LAN IP 与 IP 防暴破安全统计)
52
+ {
53
+ method: 'GET',
54
+ path: '/api/remote-mobile/status',
55
+ handler: async (_req, res) => {
56
+ const opts = store.getOptions();
57
+ const tsIp = getLocalTailscaleIp();
58
+ const lanIp = getLocalLanIp();
59
+ const ips = getAllNetworkIps();
60
+ const sessionStats = store.getSessionsList();
61
+ const ipSecurityStats = store.getIpSecurityStats();
62
+ jsonResponse(res, 200, {
63
+ success: true,
64
+ locale: readGlobalLocale(),
65
+ tailscaleIp: tsIp,
66
+ lanIp,
67
+ networkIps: ips,
68
+ allowTailscale: opts.allowTailscale,
69
+ allowLan: opts.allowLan,
70
+ hasSecret: Boolean(opts.secretHash || opts.secret),
71
+ maxVisitsPerMinute: opts.maxVisitsPerMinute,
72
+ maxFailedAttempts: opts.maxFailedAttempts,
73
+ lockDurationMs: opts.lockDurationMs,
74
+ devicesCount: sessionStats.count,
75
+ devices: sessionStats.devices,
76
+ ipSecurityStats,
77
+ persistPath: store.persistPath,
78
+ rsaKeyPath: RSA_KEY_FILE,
79
+ publicKey: getPublicKeyPem(),
80
+ });
81
+ },
82
+ },
83
+ // 2. 生成 6 位短期配对码
84
+ {
85
+ method: 'POST',
86
+ path: '/api/remote-mobile/generate-code',
87
+ handler: async (_req, res) => {
88
+ const info = store.generateShortCode();
89
+ jsonResponse(res, 200, {
90
+ success: true,
91
+ code: info.code,
92
+ expiresAt: info.expiresAt,
93
+ });
94
+ },
95
+ },
96
+ // 3. 手机端验证与换取长效 Cookie (支持 RSA-OAEP 密文解密 + 防暴力破解 429 拦截)
97
+ {
98
+ method: 'POST',
99
+ path: '/api/remote-mobile/verify',
100
+ handler: async (req, res) => {
101
+ const body = await parseJsonBody(req);
102
+ let credential = '';
103
+ if (body.encryptedCredential) {
104
+ const decrypted = decryptWithPrivateKey(body.encryptedCredential);
105
+ credential = (decrypted || body.credential || '').trim();
106
+ }
107
+ else if (body.credential) {
108
+ credential = String(body.credential).trim();
109
+ }
110
+ const ua = req.headers['user-agent'];
111
+ const ip = getRealClientIp(req);
112
+ const result = store.verify(credential, ua, ip);
113
+ if (result.success && result.token) {
114
+ const maxAge = 365 * 24 * 60 * 60;
115
+ const cookieVal = `${AUTH_COOKIE_NAME}=${result.token}; Path=/; Max-Age=${maxAge}; HttpOnly; SameSite=Lax`;
116
+ res.writeHead(200, {
117
+ 'Content-Type': 'application/json; charset=utf-8',
118
+ 'Set-Cookie': cookieVal,
119
+ });
120
+ res.end(JSON.stringify({ success: true, token: result.token }));
121
+ }
122
+ else if (result.locked) {
123
+ jsonResponse(res, 429, {
124
+ success: false,
125
+ locked: true,
126
+ remainingSeconds: result.remainingSeconds,
127
+ reason: result.reason || '连续尝试错误过多,IP已被安全锁定',
128
+ });
129
+ }
130
+ else {
131
+ jsonResponse(res, 401, { success: false, reason: result.reason || '验证失败' });
132
+ }
133
+ },
134
+ },
135
+ // 3.1 手动解除某个 IP 的防暴破锁定
136
+ {
137
+ method: 'POST',
138
+ path: '/api/remote-mobile/unlock-ip',
139
+ handler: async (req, res) => {
140
+ const body = await parseJsonBody(req);
141
+ const ip = (body.ip || '').trim();
142
+ const ok = store.unlockIp(ip);
143
+ jsonResponse(res, 200, { success: ok, ip });
144
+ },
145
+ },
146
+ // 3.2 一键清空所有 IP 访问与安全审计日志
147
+ {
148
+ method: 'POST',
149
+ path: '/api/remote-mobile/clear-ip-stats',
150
+ handler: async (_req, res) => {
151
+ store.clearAllIpSecurityStats();
152
+ jsonResponse(res, 200, { success: true });
153
+ },
154
+ },
155
+ // 4. 切换 Tailscale / 局域网免密直连开关
156
+ {
157
+ method: 'POST',
158
+ path: '/api/remote-mobile/toggle-bypass',
159
+ handler: async (req, res) => {
160
+ const body = await parseJsonBody(req);
161
+ const updates = {};
162
+ if (typeof body.allowTailscale === 'boolean') {
163
+ updates.allowTailscale = body.allowTailscale;
164
+ }
165
+ if (typeof body.allowLan === 'boolean') {
166
+ updates.allowLan = body.allowLan;
167
+ }
168
+ if (typeof body.enabled === 'boolean') {
169
+ updates.allowTailscale = body.enabled;
170
+ }
171
+ store.updateOptions(updates);
172
+ const opts = store.getOptions();
173
+ jsonResponse(res, 200, {
174
+ success: true,
175
+ allowTailscale: opts.allowTailscale,
176
+ allowLan: opts.allowLan,
177
+ });
178
+ },
179
+ },
180
+ {
181
+ method: 'POST',
182
+ path: '/api/remote-mobile/update-options',
183
+ handler: async (req, res) => {
184
+ const body = await parseJsonBody(req);
185
+ const updates = {};
186
+ if (typeof body.allowTailscale === 'boolean') {
187
+ updates.allowTailscale = body.allowTailscale;
188
+ }
189
+ if (typeof body.allowLan === 'boolean') {
190
+ updates.allowLan = body.allowLan;
191
+ }
192
+ if (typeof body.enabled === 'boolean') {
193
+ updates.allowTailscale = body.enabled;
194
+ }
195
+ if (typeof body.maxVisitsPerMinute === 'number' && body.maxVisitsPerMinute > 0) {
196
+ updates.maxVisitsPerMinute = Math.floor(body.maxVisitsPerMinute);
197
+ }
198
+ if (typeof body.maxFailedAttempts === 'number' && body.maxFailedAttempts > 0) {
199
+ updates.maxFailedAttempts = Math.floor(body.maxFailedAttempts);
200
+ }
201
+ if (typeof body.lockDurationMs === 'number' && body.lockDurationMs > 0) {
202
+ updates.lockDurationMs = Math.floor(body.lockDurationMs);
203
+ }
204
+ store.updateOptions(updates);
205
+ const opts = store.getOptions();
206
+ jsonResponse(res, 200, {
207
+ success: true,
208
+ allowTailscale: opts.allowTailscale,
209
+ allowLan: opts.allowLan,
210
+ maxVisitsPerMinute: opts.maxVisitsPerMinute,
211
+ maxFailedAttempts: opts.maxFailedAttempts,
212
+ lockDurationMs: opts.lockDurationMs,
213
+ });
214
+ },
215
+ },
216
+ // 5. 设置并持久化长期访问密码 (支持 RSA 密文 + 6位以上字母数字复杂度校验)
217
+ {
218
+ method: 'POST',
219
+ path: '/api/remote-mobile/set-secret',
220
+ handler: async (req, res) => {
221
+ const body = await parseJsonBody(req);
222
+ let secret = '';
223
+ if (body.encryptedSecret) {
224
+ secret = decryptWithPrivateKey(body.encryptedSecret).trim();
225
+ }
226
+ else if (typeof body.secret === 'string') {
227
+ secret = body.secret.trim();
228
+ }
229
+ // 校验密码复杂度
230
+ const check = validateSecretStrength(secret);
231
+ if (!check.valid) {
232
+ return jsonResponse(res, 400, {
233
+ success: false,
234
+ reason: check.reason || '密码不满足强度要求',
235
+ });
236
+ }
237
+ store.updateOptions({ secret });
238
+ jsonResponse(res, 200, { success: true, hasSecret: Boolean(secret) });
239
+ },
240
+ },
241
+ // 5.1 清除长期访问密码
242
+ {
243
+ method: 'POST',
244
+ path: '/api/remote-mobile/clear-secret',
245
+ handler: async (_req, res) => {
246
+ store.updateOptions({ secret: '', secretHash: '' });
247
+ jsonResponse(res, 200, { success: true, hasSecret: false });
248
+ },
249
+ },
250
+ // 6. 撤销单个设备授权
251
+ {
252
+ method: 'POST',
253
+ path: '/api/remote-mobile/revoke-device',
254
+ handler: async (req, res) => {
255
+ const body = await parseJsonBody(req);
256
+ const token = body.token || '';
257
+ const ok = store.revokeDevice(token);
258
+ jsonResponse(res, 200, { success: ok });
259
+ },
260
+ },
261
+ // 7. 一键撤销所有设备
262
+ {
263
+ method: 'POST',
264
+ path: '/api/remote-mobile/revoke-all',
265
+ handler: async (_req, res) => {
266
+ store.revokeAllSessions();
267
+ jsonResponse(res, 200, { success: true });
268
+ },
269
+ },
270
+ // 8. 独立手机登录引导页面(已授权或 Tailscale/局域网免密设备访问时自动 302 跳转回工作区,支持 IP 访问计数与限频)
271
+ {
272
+ method: 'GET',
273
+ path: '/auth',
274
+ handler: async (req, res) => {
275
+ const clientIp = getRealClientIp(req);
276
+ const opts = store.getOptions();
277
+ const isTs = isTailscaleIp(clientIp);
278
+ const isLan = isLanIp(clientIp);
279
+ const token = store.extractTokenFromRequest(req);
280
+ const ua = req.headers['user-agent'] || '';
281
+ const hasValidToken = token ? store.validateToken(token, clientIp) : false;
282
+ // 如果开启了免密直连(Tailscale 或 局域网)或者已持有有效 Token,直接放行直跳根目录
283
+ if ((opts.allowTailscale && isTs) || (opts.allowLan && isLan) || hasValidToken) {
284
+ res.writeHead(302, { Location: '/' });
285
+ return res.end();
286
+ }
287
+ // 记录 IP 访问计数并进行防高频刷量限频与设备型号解析
288
+ const visitCheck = store.recordAuthVisit(clientIp, ua);
289
+ if (!visitCheck.allowed) {
290
+ res.writeHead(429, { 'Content-Type': 'text/html; charset=utf-8' });
291
+ return res.end(`
292
+ <!DOCTYPE html>
293
+ <html>
294
+ <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>安全限制 - DSH</title>
295
+ <style>body{background:#0f172a;color:#fff;font-family:-apple-system,sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;padding:20px;box-sizing:border-box;text-align:center;}
296
+ .card{background:rgba(255,255,255,0.06);border:1px solid rgba(239,68,68,0.4);border-radius:16px;padding:28px 20px;max-width:380px;box-shadow:0 8px 32px rgba(0,0,0,0.4);}
297
+ h2{color:#ef4444;margin:0 0 10px 0;font-size:20px;}p{color:#94a3b8;font-size:14px;line-height:1.6;margin:0;}</style>
298
+ </head>
299
+ <body><div class="card"><h2>⚠️ 访问限制 (429)</h2><p>${visitCheck.reason || '当前 IP 请求过于频繁或已被临时锁定,请稍后刷新重试。'}</p></div></body>
300
+ </html>
301
+ `);
302
+ }
303
+ const acceptLang = req.headers['accept-language'] || '';
304
+ const reqLocale = acceptLang.toLowerCase().startsWith('en') ? 'en' : readGlobalLocale();
305
+ const html = getLoginPageHtml(undefined, reqLocale);
306
+ res.writeHead(200, {
307
+ 'Content-Type': 'text/html; charset=utf-8',
308
+ 'Cache-Control': 'no-cache',
309
+ });
310
+ res.end(html);
311
+ },
312
+ },
313
+ // 9. SSE 实时事件监听通道 (Server-Sent Events 0延时推送,彻底告别轮询)
314
+ {
315
+ method: 'GET',
316
+ path: '/api/remote-mobile/events',
317
+ handler: async (req, res) => {
318
+ res.writeHead(200, {
319
+ 'Content-Type': 'text/event-stream',
320
+ 'Cache-Control': 'no-cache, no-transform',
321
+ 'Connection': 'keep-alive',
322
+ 'Access-Control-Allow-Origin': '*',
323
+ });
324
+ res.write(': connected\n\n');
325
+ const onConnected = (device) => {
326
+ res.write(`data: ${JSON.stringify({ type: 'device-connected', device })}\n\n`);
327
+ };
328
+ const onOnline = (device) => {
329
+ res.write(`data: ${JSON.stringify({ type: 'device-online', device })}\n\n`);
330
+ };
331
+ const onRevoked = (data) => {
332
+ res.write(`data: ${JSON.stringify({ type: 'device-revoked', data })}\n\n`);
333
+ };
334
+ const onSecurityUpdated = (data) => {
335
+ res.write(`data: ${JSON.stringify({ type: 'ip-security-updated', ...data })}\n\n`);
336
+ };
337
+ const onSecurityAlert = (data) => {
338
+ res.write(`data: ${JSON.stringify({ type: 'ip-security-alert', ...data })}\n\n`);
339
+ };
340
+ store.on('device-connected', onConnected);
341
+ store.on('device-online', onOnline);
342
+ store.on('device-revoked', onRevoked);
343
+ store.on('ip-security-updated', onSecurityUpdated);
344
+ store.on('ip-security-alert', onSecurityAlert);
345
+ // 心跳定时器 (每 25 秒维持连接)
346
+ const keepAlive = setInterval(() => {
347
+ res.write(': ping\n\n');
348
+ }, 25000);
349
+ if (typeof keepAlive.unref === 'function') {
350
+ ;
351
+ keepAlive.unref();
352
+ }
353
+ req.on('close', () => {
354
+ clearInterval(keepAlive);
355
+ store.off('device-connected', onConnected);
356
+ store.off('device-online', onOnline);
357
+ store.off('device-revoked', onRevoked);
358
+ store.off('ip-security-updated', onSecurityUpdated);
359
+ store.off('ip-security-alert', onSecurityAlert);
360
+ });
361
+ },
362
+ },
363
+ ];
364
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * 手机端授权与登录页面 HTML
3
+ */
4
+ export declare function getLoginPageHtml(publicKeyPem?: string, lang?: 'zh' | 'en'): string;