dsh-remote-bridge 2.2.4 → 2.3.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 CHANGED
@@ -6,6 +6,17 @@ interface RootView {
6
6
  id: string;
7
7
  label: string;
8
8
  createdAt: number;
9
+ /**
10
+ * 这是不是插件自动登记的「DSH 工作区」根。
11
+ *
12
+ * 用户要求:默认把当前 DSH 的工作目录作为一个授权根显示出来,**不可删除**,
13
+ * 但可以正常访问(手机端照常浏览)。标记只影响**删除**这一件事 ——
14
+ * 它在数据库里就是一条普通的根,`files.*` 授权、配对、写盘全走同一套。
15
+ *
16
+ * 不写成 `readonly`,是为了让 REST 契约保持「服务端给什么就是什么」:
17
+ * 老客户端不认识这个字段会忽略它,行为不变。
18
+ */
19
+ builtin?: boolean;
9
20
  }
10
21
  interface DeviceView {
11
22
  id: string;
@@ -69,11 +80,49 @@ declare class WorkspaceDatabase {
69
80
  private readonly db;
70
81
  constructor(dataDir: string);
71
82
  close(): void;
83
+ /**
84
+ * 导出 `roots` 的建表语句(只读,诊断与迁移测试用)。
85
+ *
86
+ * 存在的理由:迁移要判断「UNIQUE 还在不在」,单看 `PRAGMA table_info` 看不出来 ——
87
+ * 表结构读出来一模一样,区别在约束上。这个方法让「重建真的发生了」可以被断言,
88
+ * 而不是靠迁移没报错来推测。
89
+ */
90
+ dumpSchema(): string;
72
91
  getSetting(key: string): string | undefined;
73
92
  setSetting(key: string, value: string): void;
74
- addRoot(inputPath: string, label?: string): Promise<StoredRoot>;
93
+ addRoot(inputPath: string, label?: string, options?: {
94
+ allowDuplicate?: boolean;
95
+ }): Promise<StoredRoot>;
75
96
  listRoots(): StoredRoot[];
76
97
  getRoot(id: string): StoredRoot | undefined;
98
+ /**
99
+ * 登记「DSH 工作区」根 —— **幂等**。
100
+ *
101
+ * 用户要求:当前 DSH 的工作目录要显示成一个授权根,**不可删除、但可以正常访问**。
102
+ * 它不是另起一张表,而是**一条普通的根**加一个 `builtin` 标记:配对、`files.*`
103
+ * 授权、回收站、写盘全走现成逻辑,少一套需要同步维护的规则。
104
+ *
105
+ * ## 同目录已有一条普通根时,另起一条而不是复用它
106
+ *
107
+ * 这是实测踩到的现实情况:用户早于这个功能**自己**加过同一条目录(本机那条是
108
+ * 10-08 手工加的),路径和 DSH 工作区一模一样。
109
+ *
110
+ * 初版在这里「尊重用户选择」直接返回,结果就是**内置项永远登记不上** —— 因为它
111
+ * 每次都撞上那条已有的普通根然后什么都不做。界面上那个「不可删除」也就永远出不来。
112
+ * 用户的选择和功能的正确性在这里是冲突的:用户要的是「这条要不可删」,不是
113
+ * 「这条不能变」。
114
+ *
115
+ * 所以改成**另起一条**:用户原来那条保持可删(那是他的目录,他有权删),内置那条
116
+ * 独立存在。代价是列表里同一个路径出现两行 —— 这是「两条记录、两种权限」的必然
117
+ * 结果,比让功能失效或擅自改他的配置都更可接受。
118
+ *
119
+ * 幂等不再靠 UNIQUE(它已经被去掉了),而是靠「已存在 `builtin = 1` 的同路径就
120
+ * 直接返回」。插件**每次启动都可以无脑调一次**。
121
+ *
122
+ * 只有传入路径确实存在才登记:工作区可能被用户删掉,这时留一条指向虚空的授权根,
123
+ * 手机浏览时会一路报错,比不显示更糟。
124
+ */
125
+ ensureBuiltinRoot(realPath: string, label: string): Promise<StoredRoot | undefined>;
77
126
  removeRoot(id: string): void;
78
127
  createPairing(rootIds: string[], scopes: DeviceScope[], ttlMs?: number): {
79
128
  code: string;
@@ -89,7 +138,39 @@ declare class WorkspaceDatabase {
89
138
  listDevices(): DeviceView[];
90
139
  isDeviceActive(id: string): boolean;
91
140
  updateDevice(id: string, scopes: DeviceScope[], rootIds: string[]): DeviceView;
141
+ /**
142
+ * 撤销设备 = **直接删除这一行**(用户要求:已撤销的不显示,并自动从数据库中删除)。
143
+ *
144
+ * 原先是打 `revoked_at` 标记保留行,两处都不好:
145
+ * - 界面上会出现一长串灰掉的「已撤销」,本机实测堆到了 32 条,全是历史测试与旧设备;
146
+ * - 行留着就等于凭据的哈希还在库里,是个长期负担 —— 令牌已经没用了,留着没有意义。
147
+ *
148
+ * 删除是安全的,因为:
149
+ * - `device_roots` 是 `ON DELETE CASCADE`,授权行自动清掉;
150
+ * - `audit.actor_id` 是**普通字符串不是外键**,审计留痕(谁在什么时候撤了谁)一条不少;
151
+ * - `authenticate` 仍保留 `revoked_at IS NULL` 作为兜底,万一有别处留下的标记行,
152
+ * 它们依然登不进来 —— 两层保险不冲突。
153
+ */
92
154
  revokeDevice(id: string): void;
155
+ /**
156
+ * 一次性清理历史遗留的「标记式」撤销设备(`revoked_at IS NOT NULL` 的行)。
157
+ *
158
+ * 撤销改成直接删除之前,每次撤销都只是打个标记,于是库越用越臃肿 —— 本机实测堆到
159
+ * **32 条**,全是历史测试探针和早已弃用的旧手机,界面上就是一长串灰字。
160
+ *
161
+ * 幂等:没有这种行时返回 0,不报错。插件每次启动调一次即可。
162
+ * 走 `revokeDevice` 的删除语义,但**不能**借它写 `device.revoke` 审计 ——
163
+ * 那会把「系统清理」记成管理员逐个撤销,审计会失真。清理本身只写一条汇总。
164
+ */
165
+ /**
166
+ * 仅供测试:造一行「撤销改成删除之前」的标记式撤销数据。
167
+ *
168
+ * 真实场景来自历史库(每次撤销只打 `revoked_at` 标记,于是越积越多)。测试需要个
169
+ * 干净的办法制造这种行,而它不该走生产代码路径 —— 所以显式开一个测试专用入口,
170
+ * 并起一个自见其明的名字,免得有人把它当成正常 API。
171
+ */
172
+ markRevokedForTest(id: string): void;
173
+ purgeRevokedDevices(): number;
93
174
  insertTrash(record: TrashRecord): void;
94
175
  updateTrashStatus(id: string, status: string): void;
95
176
  getTrash(id: string): TrashRecord | undefined;
@@ -103,6 +184,22 @@ declare class WorkspaceDatabase {
103
184
  } | undefined;
104
185
  saveIdempotent(deviceId: string, key: string, status: number, body: unknown): void;
105
186
  private migrate;
187
+ /** `roots.real_path` 上是否还带 UNIQUE 约束。 */
188
+ private rootsPathIsUnique;
189
+ /**
190
+ * 重建 `roots` 去掉 UNIQUE。
191
+ *
192
+ * 这是一次**有真实数据风险**的操作,所以每一步都写清楚理由:
193
+ * - `PRAGMA foreign_keys=OFF`:`device_roots.root_id` 引用 `roots(id)`,而重建要
194
+ * DROP 掉被引用的那张表。开着外键会直接失败。
195
+ * - 事务:`new_roots` 建好、数据搬完、**最后**才 DROP 老表并改名。中途失败会整体
196
+ * 回滚,原表原封不动。
197
+ * - 搬数据用显式列名而不是 `SELECT *`:老表可能还没有 `builtin` 列(两种迁移顺序
198
+ * 都可能出现),按列名搬才不会在某一台机器上错位。
199
+ */
200
+ private dropRootsUniqueConstraint;
201
+ /** `roots` 表是否已有指定列。 */
202
+ private hasRootColumn;
106
203
  }
107
204
  //#endregion
108
205
  //#region src/host/settings-adapter.d.ts
@@ -1348,6 +1445,13 @@ interface RouterOptions {
1348
1445
  settings: DshSettingsAdapter;
1349
1446
  remote: RemoteControl;
1350
1447
  maxUploadBytes: number;
1448
+ /**
1449
+ * 解析不出会话工作目录时的图片兜底目录(设置面板里只读展示)。
1450
+ *
1451
+ * 可选:默认按 `database.dataDir` 推出来,测试因此不必每条都补一个字段。
1452
+ * 生产路径由 `index.ts` 传入与交接实际使用**同一个**目录,避免「界面显示 A、落盘在 B」。
1453
+ */
1454
+ imageFallbackDir?: string;
1351
1455
  }
1352
1456
  declare class ApiRouter {
1353
1457
  private readonly options;
@@ -1359,6 +1463,8 @@ declare class ApiRouter {
1359
1463
  private dispatchContent;
1360
1464
  private requireRoot;
1361
1465
  private authorizedRoots;
1466
+ /** 兜底交接目录:没配就用状态目录下的 `image-handoff`(与 `index.ts` 的默认一致)。 */
1467
+ private imageFallbackDir;
1362
1468
  private audit;
1363
1469
  private readIdempotent;
1364
1470
  private saveIdempotent;
@@ -1414,13 +1520,19 @@ declare class RemoteApiServer implements RemoteControl {
1414
1520
  private eventAllowed;
1415
1521
  private trimOperations;
1416
1522
  }
1523
+ /** 一条挂在 DSH webserver 上的插件自有路由。 */
1524
+ interface EmbeddedRoute {
1525
+ kind: 'exact' | 'prefix';
1526
+ path: string;
1527
+ handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>;
1528
+ }
1417
1529
  declare function attachEmbeddedRoutes(webServer: {
1418
1530
  register(route: {
1419
1531
  kind: 'exact' | 'prefix';
1420
1532
  path: string;
1421
1533
  handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>;
1422
1534
  }): () => void;
1423
- }, router: ApiRouter, remote: RemoteApiServer): () => void;
1535
+ }, router: ApiRouter, remote: RemoteApiServer, extraRoutes?: readonly EmbeddedRoute[]): () => void;
1424
1536
  //#endregion
1425
1537
  //#region src/index.d.ts
1426
1538
  /**