@yezack/dsh-remote-mobile 0.0.0-stage → 1.9.1

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,34 @@
1
+ /**
2
+ * DeepSeek Harness (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/remote-mobile/settings.json(DSH 0.1.7+ 起官方已弃用 settings.yaml),
13
+ * 启动时一次性兼容迁移旧 settings.yaml / settings.yaml.imported 中的配置。
14
+ */
15
+ import { type SessionStoreOptions } from './auth/token.js';
16
+ /** Cordis 插件名称标识 */
17
+ export declare const name = "dsh-remote-mobile";
18
+ /** DSH 设置命名空间 */
19
+ export declare const REMOTE_MOBILE_SETTINGS_NAMESPACE = "dsh-remote-mobile";
20
+ /** Cordis 依赖注入声明:声明注入底座内置的 webServer 服务 */
21
+ export declare const inject: string[];
22
+ /**
23
+ * 插件生命周期挂载入口函数
24
+ *
25
+ * @param ctx - Cordis 应用上下文
26
+ * @param config - 插件持久化配置选项 (SessionStoreOptions)
27
+ */
28
+ export declare function apply(ctx: any, config?: SessionStoreOptions): void;
29
+ export * from './auth/tailscale.js';
30
+ export * from './auth/token.js';
31
+ export * from './auth/gate.js';
32
+ export * from './routes/api.js';
33
+ export * from './bridge/compat.js';
34
+ export * from './styles/style-snippets.js';
package/lib/index.js ADDED
@@ -0,0 +1,236 @@
1
+ /**
2
+ * DeepSeek Harness (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/remote-mobile/settings.json(DSH 0.1.7+ 起官方已弃用 settings.yaml),
13
+ * 启动时一次性兼容迁移旧 settings.yaml / settings.yaml.imported 中的配置。
14
+ */
15
+ import { SessionStore } from './auth/token.js';
16
+ import { createGlobalAuthGate } from './auth/gate.js';
17
+ import { createRoutes } from './routes/api.js';
18
+ import { StyleSnippetStore } from './styles/style-snippets.js';
19
+ import { getLocalTailscaleIp, getLocalLanIp } from './auth/tailscale.js';
20
+ import { patchHttpServerWithVirtualizer, mountIndexInjections, registerRemoteWebUiPairingBridge, takeOverConnectionAuth, } from './bridge/compat.js';
21
+ /** Cordis 插件名称标识 */
22
+ export const name = 'dsh-remote-mobile';
23
+ /** DSH 设置命名空间 */
24
+ export const REMOTE_MOBILE_SETTINGS_NAMESPACE = 'dsh-remote-mobile';
25
+ /** Cordis 依赖注入声明:声明注入底座内置的 webServer 服务 */
26
+ export const inject = ['webServer'];
27
+ /**
28
+ * 尽力接入 DSH Settings 服务(若宿主提供旧版 register API)。
29
+ *
30
+ * 说明:DSH 0.1.7+ 起官方 dsh-settings 已改为基于 profile cordis.patch.yml 的
31
+ * Config 表单模型(无 register 方法),本函数在该版本下会安全跳过;插件的配置
32
+ * 持久化改由自有 settings.json 承担(见 token.ts)。
33
+ */
34
+ function bindDshSettings(ctx, store) {
35
+ try {
36
+ let settingsService = null;
37
+ if (typeof ctx.get === 'function') {
38
+ settingsService = ctx.get('settings');
39
+ }
40
+ if (!settingsService?.register)
41
+ return;
42
+ const ns = REMOTE_MOBILE_SETTINGS_NAMESPACE;
43
+ const scope = settingsService.register(ns, undefined, {
44
+ base: store.getOptions(),
45
+ });
46
+ if (scope) {
47
+ // 1. 同步宿主 Settings 中的最新配置到 SessionStore
48
+ const initialVal = scope.get?.();
49
+ if (initialVal && typeof initialVal === 'object') {
50
+ store.updateOptions(initialVal, false);
51
+ }
52
+ // 2. 监听宿主 Settings 或 Web UI 配置变更
53
+ scope.watch?.(() => {
54
+ const updated = scope.get?.();
55
+ if (updated && typeof updated === 'object') {
56
+ store.updateOptions(updated, false);
57
+ }
58
+ });
59
+ // 3. 绑定反向写入钩子:插件 API 修改配置时同步写回宿主 Settings(旧版 API)
60
+ store.setSettingsMutator((patch) => {
61
+ try {
62
+ const ops = Object.entries(patch).map(([field, value]) => ({
63
+ op: 'set',
64
+ path: [field],
65
+ value,
66
+ }));
67
+ settingsService.mutate?.(ns, ops);
68
+ }
69
+ catch { }
70
+ });
71
+ }
72
+ }
73
+ catch { }
74
+ }
75
+ /**
76
+ * 插件生命周期挂载入口函数
77
+ *
78
+ * @param ctx - Cordis 应用上下文
79
+ * @param config - 插件持久化配置选项 (SessionStoreOptions)
80
+ */
81
+ export function apply(ctx, config = {}) {
82
+ // 1. 初始化会话与令牌持久化管理器
83
+ const store = new SessionStore(config);
84
+ const styleStore = new StyleSnippetStore();
85
+ const gateMiddleware = createGlobalAuthGate(store);
86
+ // 2. 接入 DSH Settings 服务 (命名空间: dsh-remote-mobile)
87
+ bindDshSettings(ctx, store);
88
+ // 3. 挂载底座 WebServer 相关的门禁拦截与路由注册
89
+ if (ctx.webServer) {
90
+ if (ctx.webServer.server) {
91
+ patchHttpServerWithVirtualizer(ctx.webServer.server, gateMiddleware, store, styleStore);
92
+ }
93
+ // 挂载 HTML 模板层的 Crypto Polyfill 注入钩子
94
+ mountIndexInjections(ctx);
95
+ // 注册插件专属的 HTTP & SSE API 路由
96
+ if (typeof ctx.webServer.register === 'function') {
97
+ const routes = createRoutes(store, styleStore);
98
+ for (const route of routes) {
99
+ ctx.webServer.register(route);
100
+ }
101
+ }
102
+ }
103
+ // 4. 向 Cordis 容器桥接注册 remoteWebUiPairing 服务(延迟裁决:与其他远程接入
104
+ // 类插件共存时自动让出,保证启动不冲突,详见 compat.ts)
105
+ registerRemoteWebUiPairingBridge(ctx, store);
106
+ // 5. 透明接管 DSH 底座 connection 鉴权服务(短路官方 Token 401 阻断)
107
+ /**
108
+ * connection 鉴权接管采用三点冗余策略:
109
+ * 1) 插件 apply 时立即尝试 —— 覆盖 connection 先于本插件就绪的场景
110
+ * 2) ctx.inject 动态注入 —— 覆盖 connection 后于本插件就绪的场景
111
+ * 3) printReadyBanner 中兜底 —— 覆盖上述两者都未捕获到的极端时序
112
+ *
113
+ * takeOverConnectionAuth 内部通过 __dsh_rm_auth_takeover__ 标记保证幂等,
114
+ * 同一 connection 实例不会被重复接管。
115
+ */
116
+ const attachConnectionAuth = (conn) => {
117
+ if (conn) {
118
+ takeOverConnectionAuth(conn, store);
119
+ }
120
+ };
121
+ // 1) 优先通过 ctx.get 安全获取(未注入时不抛错返回 undefined)
122
+ try {
123
+ const conn = typeof ctx.get === 'function' ? ctx.get('connection') : undefined;
124
+ if (conn)
125
+ attachConnectionAuth(conn);
126
+ }
127
+ catch (e) {
128
+ ctx.logger?.debug?.('[dsh-remote-mobile] 尝试直接获取 connection 失败:', e);
129
+ }
130
+ // 2) 声明动态子注入,在 connection 服务就绪时接管
131
+ if (typeof ctx.inject === 'function') {
132
+ try {
133
+ ctx.inject(['connection'], (connCtx) => {
134
+ try {
135
+ // 统一通过 get 获取服务实例,避免属性访问与方法获取返回不同对象
136
+ const conn = typeof connCtx?.get === 'function' ? connCtx.get('connection') : connCtx?.connection;
137
+ if (conn)
138
+ attachConnectionAuth(conn);
139
+ }
140
+ catch (e) {
141
+ ctx.logger?.debug?.('[dsh-remote-mobile] inject 接收 connection 注入失败:', e);
142
+ }
143
+ });
144
+ }
145
+ catch (e) {
146
+ ctx.logger?.debug?.('[dsh-remote-mobile] 注册 connection 动态注入失败:', e);
147
+ }
148
+ }
149
+ // 6. 服务就绪时输出提示横幅与接入地址
150
+ let printed = false;
151
+ let disposed = false;
152
+ let timer1 = null;
153
+ let timer2 = null;
154
+ try {
155
+ ctx.on?.('dispose', () => {
156
+ disposed = true;
157
+ if (timer1)
158
+ clearTimeout(timer1);
159
+ if (timer2)
160
+ clearTimeout(timer2);
161
+ });
162
+ }
163
+ catch { }
164
+ const printReadyBanner = () => {
165
+ // 生命周期守卫:若上下文已废弃/进入 dispose,放弃回调
166
+ if (disposed || printed)
167
+ return;
168
+ printed = true;
169
+ try {
170
+ const webServer = (typeof ctx.get === 'function' ? ctx.get('webServer') : undefined) || ctx.webServer;
171
+ if (webServer?.server) {
172
+ patchHttpServerWithVirtualizer(webServer.server, gateMiddleware, store, styleStore);
173
+ }
174
+ try {
175
+ const conn = typeof ctx.get === 'function' ? ctx.get('connection') : undefined;
176
+ if (conn)
177
+ attachConnectionAuth(conn);
178
+ }
179
+ catch (e) {
180
+ ctx.logger?.debug?.('[dsh-remote-mobile] 就绪回调获取 connection 失败:', e);
181
+ }
182
+ const tsIp = getLocalTailscaleIp();
183
+ const lanIp = getLocalLanIp();
184
+ const port = webServer?.port ?? 3080;
185
+ const opts = store.getOptions();
186
+ const divider = '─'.repeat(72);
187
+ console.log(`\n┌${divider}`);
188
+ console.log(`│ [dsh-remote-mobile] 官方临时 Token 鉴权已接管,PC/移动端访问无需携带官方token`);
189
+ console.log(`│ 远程安全网关已就绪,支持手机浏览器访问:`);
190
+ console.log(`│ • 本机回环: http://127.0.0.1:${port}`);
191
+ if (lanIp && lanIp !== '127.0.0.1') {
192
+ console.log(`│ • 局域网: http://${lanIp}:${port} (同 Wi-Fi 局域网直连)`);
193
+ }
194
+ if (tsIp) {
195
+ console.log(`│ • 虚拟私网: http://${tsIp}:${port} (Tailscale 私网异地直连)`);
196
+ }
197
+ console.log(`│ 访问与配置说明:`);
198
+ console.log(`│ - 手机/远程设备打开对应地址即可直接访问`);
199
+ console.log(`│ - 在本机打开 Web 界面 ->「设置」->「远程与移动端」进行安全管理:`);
200
+ console.log(`│ 可开启免密直连(局域网/Tailscale)、查看 6 位临时配对码或设置长期密码`);
201
+ console.log(`│ - 项目文档: https://github.com/IceApriler/dsh-remote-mobile`);
202
+ console.log(`└${divider}\n`);
203
+ ctx.logger?.info?.(`[dsh-remote-mobile] 移动端远程控制与安全门禁已激活 (上下文虚拟化桥接就绪,样式片段: ${styleStore.list().length} 个)`);
204
+ if (tsIp) {
205
+ ctx.logger?.info?.(`[dsh-remote-mobile] Tailscale 地址: http://${tsIp}:${port} (免密直连: ${opts.allowTailscale ? '已开启' : '已关闭'})`);
206
+ }
207
+ }
208
+ catch (e) {
209
+ // 上下文若在并发或极端时序下重载,静默退出,防止未捕获异常导致主进程崩溃
210
+ }
211
+ };
212
+ // 1) 优先在 loader 插件树就绪后紧跟官方提示打印
213
+ try {
214
+ const loader = typeof ctx.get === 'function' ? ctx.get('loader') : undefined;
215
+ const settled = loader?.await?.();
216
+ if (settled && typeof settled.then === 'function') {
217
+ settled.then(() => {
218
+ timer1 = setTimeout(printReadyBanner, 120);
219
+ }).catch((e) => {
220
+ ctx.logger?.debug?.('[dsh-remote-mobile] loader await 异常:', e);
221
+ });
222
+ }
223
+ }
224
+ catch (e) {
225
+ ctx.logger?.debug?.('[dsh-remote-mobile] 获取 loader 失败:', e);
226
+ }
227
+ // 2) 兜底定时器(避免无 loader 环境时不输出)
228
+ timer2 = setTimeout(printReadyBanner, 1500);
229
+ }
230
+ // 导出所有子模块类型与核心工具
231
+ export * from './auth/tailscale.js';
232
+ export * from './auth/token.js';
233
+ export * from './auth/gate.js';
234
+ export * from './routes/api.js';
235
+ export * from './bridge/compat.js';
236
+ export * from './styles/style-snippets.js';
@@ -0,0 +1,16 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { type SessionStore } from '../auth/token.js';
3
+ import { type StyleSnippetStore } from '../styles/style-snippets.js';
4
+ export declare function createRoutes(store: SessionStore, styleStore?: StyleSnippetStore): ({
5
+ method: string;
6
+ path: string;
7
+ handler: (_req: IncomingMessage, res: ServerResponse) => Promise<void>;
8
+ } | {
9
+ method: string;
10
+ path: string;
11
+ handler: (req: IncomingMessage, res: ServerResponse) => Promise<ServerResponse<IncomingMessage> | undefined>;
12
+ } | {
13
+ method?: undefined;
14
+ path: string;
15
+ handler: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
16
+ })[];