dsh-zen-remote 1.1.15 → 2.0.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.
Files changed (272) hide show
  1. package/AGENTS.md +54 -9
  2. package/README.md +249 -190
  3. package/README.zh-CN.md +248 -202
  4. package/assets/home.png +0 -0
  5. package/assets/info.png +0 -0
  6. package/assets/pairing.png +0 -0
  7. package/assets/session.png +0 -0
  8. package/assets/sheet.png +0 -0
  9. package/cordis.patch.yml +12 -11
  10. package/cordis.patch.yml.example +24 -9
  11. package/docs/interface.md +82 -4
  12. package/docs/remote-access.en.md +99 -58
  13. package/docs/remote-access.md +99 -42
  14. package/dsh-push.mjs +67 -54
  15. package/lan-gate.mjs +88 -20
  16. package/lib/activity.js +168 -0
  17. package/lib/activity.js.map +1 -0
  18. package/lib/admin-routes.js +526 -0
  19. package/lib/admin-routes.js.map +1 -0
  20. package/lib/client-pairing.js +224 -0
  21. package/lib/client-pairing.js.map +1 -0
  22. package/lib/client-routes.js +674 -0
  23. package/lib/client-routes.js.map +1 -0
  24. package/lib/client.js +11268 -5844
  25. package/lib/config.js +346 -0
  26. package/lib/config.js.map +1 -0
  27. package/lib/fetch-route-intercept.js +418 -0
  28. package/lib/fetch-route-intercept.js.map +1 -0
  29. package/lib/fingerprint.js +363 -0
  30. package/lib/fingerprint.js.map +1 -0
  31. package/lib/http.js +69 -0
  32. package/lib/http.js.map +1 -0
  33. package/lib/index.js +675 -54
  34. package/lib/index.js.map +1 -1
  35. package/lib/intercept-shape.js +81 -0
  36. package/lib/intercept-shape.js.map +1 -0
  37. package/lib/intercept.js +3040 -0
  38. package/lib/intercept.js.map +1 -0
  39. package/lib/lan-gate-server.cjs +532 -121
  40. package/lib/merge-streams.js +1165 -0
  41. package/lib/merge-streams.js.map +1 -0
  42. package/lib/relay-access.js +496 -0
  43. package/lib/relay-access.js.map +1 -0
  44. package/lib/relay-client.js +1303 -0
  45. package/lib/relay-client.js.map +1 -0
  46. package/lib/relay-filter.js +282 -0
  47. package/lib/relay-filter.js.map +1 -0
  48. package/lib/relay-server.js +1792 -0
  49. package/lib/relay-server.js.map +1 -0
  50. package/lib/restart-fields.js +34 -0
  51. package/lib/restart-fields.js.map +1 -0
  52. package/lib/restart-watcher.js +106 -0
  53. package/lib/restart-watcher.js.map +1 -0
  54. package/lib/session-reference.js +157 -0
  55. package/lib/session-reference.js.map +1 -0
  56. package/lib/share-export.js +16 -1
  57. package/lib/share-export.js.map +1 -1
  58. package/lib/share-ops.js +86 -0
  59. package/lib/share-ops.js.map +1 -0
  60. package/lib/share-store.js +294 -0
  61. package/lib/share-store.js.map +1 -0
  62. package/lib/types/activity.d.ts +99 -0
  63. package/lib/types/activity.d.ts.map +1 -0
  64. package/lib/types/admin-routes.d.ts +129 -0
  65. package/lib/types/admin-routes.d.ts.map +1 -0
  66. package/lib/types/client/MobileDrawerFooter.d.ts.map +1 -1
  67. package/lib/types/client/MobileHome.d.ts +1 -1
  68. package/lib/types/client/MobileHome.d.ts.map +1 -1
  69. package/lib/types/client/MobileHomeChips.d.ts +1 -1
  70. package/lib/types/client/MobileHomeChips.d.ts.map +1 -1
  71. package/lib/types/client/MobileSessionHeader.d.ts +19 -8
  72. package/lib/types/client/MobileSessionHeader.d.ts.map +1 -1
  73. package/lib/types/client/MobileSessionInfo.d.ts +20 -3
  74. package/lib/types/client/MobileSessionInfo.d.ts.map +1 -1
  75. package/lib/types/client/RemoteComposerBanner.d.ts +42 -0
  76. package/lib/types/client/RemoteComposerBanner.d.ts.map +1 -0
  77. package/lib/types/client/RemoteHeaderIcon.d.ts +37 -0
  78. package/lib/types/client/RemoteHeaderIcon.d.ts.map +1 -0
  79. package/lib/types/client/RemoteShareMenu.d.ts +28 -0
  80. package/lib/types/client/RemoteShareMenu.d.ts.map +1 -0
  81. package/lib/types/client/RemoteStatusIcon.d.ts +36 -0
  82. package/lib/types/client/RemoteStatusIcon.d.ts.map +1 -0
  83. package/lib/types/client/compat/desktop.d.ts +21 -0
  84. package/lib/types/client/compat/desktop.d.ts.map +1 -0
  85. package/lib/types/client/compat/types.d.ts +57 -0
  86. package/lib/types/client/compat/types.d.ts.map +1 -1
  87. package/lib/types/client/current-session-report.d.ts +37 -0
  88. package/lib/types/client/current-session-report.d.ts.map +1 -0
  89. package/lib/types/client/effects/gestures.d.ts.map +1 -1
  90. package/lib/types/client/effects/header-status.d.ts.map +1 -1
  91. package/lib/types/client/effects/keyboard-guard.d.ts.map +1 -1
  92. package/lib/types/client/effects/model-sheet-extras.d.ts.map +1 -1
  93. package/lib/types/client/effects/permission-sheet.d.ts +39 -0
  94. package/lib/types/client/effects/permission-sheet.d.ts.map +1 -0
  95. package/lib/types/client/effects/remote-session.d.ts +25 -0
  96. package/lib/types/client/effects/remote-session.d.ts.map +1 -0
  97. package/lib/types/client/effects/turn-fold.d.ts +9 -0
  98. package/lib/types/client/effects/turn-fold.d.ts.map +1 -1
  99. package/lib/types/client/effects/workbench-ref-close.d.ts +2 -1
  100. package/lib/types/client/effects/workbench-ref-close.d.ts.map +1 -1
  101. package/lib/types/client/index.d.ts.map +1 -1
  102. package/lib/types/client/locales.d.ts +187 -2
  103. package/lib/types/client/locales.d.ts.map +1 -1
  104. package/lib/types/client/remote-fetch.d.ts +51 -0
  105. package/lib/types/client/remote-fetch.d.ts.map +1 -0
  106. package/lib/types/client/remote-group-expansion.d.ts +41 -0
  107. package/lib/types/client/remote-group-expansion.d.ts.map +1 -0
  108. package/lib/types/client/remote-share-css.d.ts +19 -0
  109. package/lib/types/client/remote-share-css.d.ts.map +1 -0
  110. package/lib/types/client/remote-share-register.d.ts +113 -0
  111. package/lib/types/client/remote-share-register.d.ts.map +1 -0
  112. package/lib/types/client/remote-status-css.d.ts +15 -0
  113. package/lib/types/client/remote-status-css.d.ts.map +1 -0
  114. package/lib/types/client/remote-status-register.d.ts +46 -0
  115. package/lib/types/client/remote-status-register.d.ts.map +1 -0
  116. package/lib/types/client/settings/SettingsSection.d.ts +46 -0
  117. package/lib/types/client/settings/SettingsSection.d.ts.map +1 -0
  118. package/lib/types/client/settings/register-settings.d.ts +58 -0
  119. package/lib/types/client/settings/register-settings.d.ts.map +1 -0
  120. package/lib/types/client/settings/settings-css.d.ts +10 -0
  121. package/lib/types/client/settings/settings-css.d.ts.map +1 -0
  122. package/lib/types/client/share/share-preview.d.ts.map +1 -1
  123. package/lib/types/client/sidebar-panels.d.ts +63 -0
  124. package/lib/types/client/sidebar-panels.d.ts.map +1 -0
  125. package/lib/types/client/styles/compat.css.d.ts +1 -1
  126. package/lib/types/client/styles/compat.css.d.ts.map +1 -1
  127. package/lib/types/client/styles/composer.css.d.ts +1 -1
  128. package/lib/types/client/styles/composer.css.d.ts.map +1 -1
  129. package/lib/types/client/styles/header.css.d.ts +1 -1
  130. package/lib/types/client/styles/header.css.d.ts.map +1 -1
  131. package/lib/types/client/styles/info.css.d.ts +1 -1
  132. package/lib/types/client/styles/info.css.d.ts.map +1 -1
  133. package/lib/types/client/styles/layout.css.d.ts +1 -1
  134. package/lib/types/client/styles/layout.css.d.ts.map +1 -1
  135. package/lib/types/client/styles/remote-session.css.d.ts +2 -0
  136. package/lib/types/client/styles/remote-session.css.d.ts.map +1 -0
  137. package/lib/types/client-data/current-session.d.ts +41 -0
  138. package/lib/types/client-data/current-session.d.ts.map +1 -0
  139. package/lib/types/client-data/remote-group-expansion.d.ts +76 -0
  140. package/lib/types/client-data/remote-group-expansion.d.ts.map +1 -0
  141. package/lib/types/client-data/remote-status.d.ts +148 -0
  142. package/lib/types/client-data/remote-status.d.ts.map +1 -0
  143. package/lib/types/client-data/settings-form.d.ts +774 -0
  144. package/lib/types/client-data/settings-form.d.ts.map +1 -0
  145. package/lib/types/client-data/shares.d.ts +211 -0
  146. package/lib/types/client-data/shares.d.ts.map +1 -0
  147. package/lib/types/client-pairing.d.ts +114 -0
  148. package/lib/types/client-pairing.d.ts.map +1 -0
  149. package/lib/types/client-routes.d.ts +112 -0
  150. package/lib/types/client-routes.d.ts.map +1 -0
  151. package/lib/types/config.d.ts +200 -0
  152. package/lib/types/config.d.ts.map +1 -0
  153. package/lib/types/fetch-route-intercept.d.ts +156 -0
  154. package/lib/types/fetch-route-intercept.d.ts.map +1 -0
  155. package/lib/types/fingerprint.d.ts +164 -0
  156. package/lib/types/fingerprint.d.ts.map +1 -0
  157. package/lib/types/http.d.ts +41 -0
  158. package/lib/types/http.d.ts.map +1 -0
  159. package/lib/types/index.d.ts +56 -24
  160. package/lib/types/index.d.ts.map +1 -1
  161. package/lib/types/intercept-shape.d.ts +32 -0
  162. package/lib/types/intercept-shape.d.ts.map +1 -0
  163. package/lib/types/intercept.d.ts +432 -0
  164. package/lib/types/intercept.d.ts.map +1 -0
  165. package/lib/types/merge-streams.d.ts +393 -0
  166. package/lib/types/merge-streams.d.ts.map +1 -0
  167. package/lib/types/relay-access.d.ts +310 -0
  168. package/lib/types/relay-access.d.ts.map +1 -0
  169. package/lib/types/relay-client.d.ts +274 -0
  170. package/lib/types/relay-client.d.ts.map +1 -0
  171. package/lib/types/relay-filter.d.ts +103 -0
  172. package/lib/types/relay-filter.d.ts.map +1 -0
  173. package/lib/types/relay-server.d.ts +208 -0
  174. package/lib/types/relay-server.d.ts.map +1 -0
  175. package/lib/types/restart-fields.d.ts +22 -0
  176. package/lib/types/restart-fields.d.ts.map +1 -0
  177. package/lib/types/restart-watcher.d.ts +83 -0
  178. package/lib/types/restart-watcher.d.ts.map +1 -0
  179. package/lib/types/session-reference.d.ts +66 -0
  180. package/lib/types/session-reference.d.ts.map +1 -0
  181. package/lib/types/share-export.d.ts +14 -1
  182. package/lib/types/share-export.d.ts.map +1 -1
  183. package/lib/types/share-ops.d.ts +79 -0
  184. package/lib/types/share-ops.d.ts.map +1 -0
  185. package/lib/types/share-store.d.ts +87 -0
  186. package/lib/types/share-store.d.ts.map +1 -0
  187. package/lib/types/virtual-id.d.ts +38 -0
  188. package/lib/types/virtual-id.d.ts.map +1 -0
  189. package/lib/virtual-id.js +52 -0
  190. package/lib/virtual-id.js.map +1 -0
  191. package/llms.txt +17 -14
  192. package/package.json +60 -50
  193. package/scripts/build-client.mjs +5 -3
  194. package/scripts/check-host-shadow.mjs +19 -2
  195. package/scripts/check-settings-form.mjs +933 -0
  196. package/scripts/check-shares.mjs +397 -0
  197. package/scripts/check-upload-endpoint.mjs +59 -3
  198. package/scripts/sync-doc-version.mjs +8 -15
  199. package/scripts/verify-fingerprint-live.mjs +111 -0
  200. package/src/activity.ts +218 -0
  201. package/src/admin-routes.ts +627 -0
  202. package/src/client/MobileAttachButton.tsx +2 -2
  203. package/src/client/MobileAttachChips.tsx +3 -3
  204. package/src/client/MobileDrawerFooter.tsx +7 -4
  205. package/src/client/MobileHome.tsx +20 -16
  206. package/src/client/MobileHomeChips.tsx +25 -21
  207. package/src/client/MobileNavOverlay.tsx +2 -2
  208. package/src/client/MobileNavToggle.tsx +3 -3
  209. package/src/client/MobileSessionHeader.tsx +100 -107
  210. package/src/client/MobileSessionInfo.tsx +118 -17
  211. package/src/client/RemoteComposerBanner.tsx +76 -0
  212. package/src/client/RemoteHeaderIcon.tsx +103 -0
  213. package/src/client/RemoteShareMenu.tsx +57 -0
  214. package/src/client/RemoteStatusIcon.tsx +91 -0
  215. package/src/client/compat/desktop.ts +22 -0
  216. package/src/client/compat/types.ts +62 -0
  217. package/src/client/current-session-report.ts +164 -0
  218. package/src/client/effects/gestures.ts +8 -17
  219. package/src/client/effects/header-status.ts +21 -17
  220. package/src/client/effects/keyboard-avoid.ts +2 -2
  221. package/src/client/effects/keyboard-guard.ts +75 -7
  222. package/src/client/effects/model-sheet-extras.ts +8 -2
  223. package/src/client/effects/permission-sheet.ts +149 -0
  224. package/src/client/effects/remote-session.ts +68 -0
  225. package/src/client/effects/turn-fold.ts +52 -0
  226. package/src/client/effects/workbench-ref-close.ts +16 -23
  227. package/src/client/index.tsx +151 -36
  228. package/src/client/locales.ts +396 -2
  229. package/src/client/remote-fetch.ts +125 -0
  230. package/src/client/remote-group-expansion.ts +190 -0
  231. package/src/client/remote-share-css.ts +54 -0
  232. package/src/client/remote-share-register.ts +244 -0
  233. package/src/client/remote-status-css.ts +64 -0
  234. package/src/client/remote-status-register.ts +107 -0
  235. package/src/client/settings/SettingsSection.tsx +1229 -0
  236. package/src/client/settings/register-settings.ts +116 -0
  237. package/src/client/settings/settings-css.ts +236 -0
  238. package/src/client/share/share-preview.tsx +6 -3
  239. package/src/client/share/share-sheet.tsx +3 -3
  240. package/src/client/sidebar-panels.ts +99 -0
  241. package/src/client/styles/compat.css.ts +10 -0
  242. package/src/client/styles/composer.css.ts +147 -36
  243. package/src/client/styles/header.css.ts +15 -11
  244. package/src/client/styles/info.css.ts +74 -0
  245. package/src/client/styles/layout.css.ts +20 -0
  246. package/src/client/styles/remote-session.css.ts +251 -0
  247. package/src/client-data/current-session.ts +57 -0
  248. package/src/client-data/remote-group-expansion.ts +145 -0
  249. package/src/client-data/remote-status.ts +374 -0
  250. package/src/client-data/settings-form.ts +1371 -0
  251. package/src/client-data/shares.ts +467 -0
  252. package/src/client-pairing.ts +244 -0
  253. package/src/client-routes.ts +732 -0
  254. package/src/config.ts +447 -0
  255. package/src/fetch-route-intercept.ts +510 -0
  256. package/src/fingerprint.ts +408 -0
  257. package/src/http.ts +68 -0
  258. package/src/index.ts +719 -62
  259. package/src/intercept-shape.ts +87 -0
  260. package/src/intercept.ts +3362 -0
  261. package/src/merge-streams.ts +1272 -0
  262. package/src/relay-access.ts +669 -0
  263. package/src/relay-client.ts +1514 -0
  264. package/src/relay-filter.ts +291 -0
  265. package/src/relay-server.ts +2025 -0
  266. package/src/restart-fields.ts +36 -0
  267. package/src/restart-watcher.ts +140 -0
  268. package/src/session-reference.ts +156 -0
  269. package/src/share-export.ts +26 -1
  270. package/src/share-ops.ts +114 -0
  271. package/src/share-store.ts +373 -0
  272. package/src/virtual-id.ts +59 -0
@@ -0,0 +1,774 @@
1
+ /**
2
+ * Staged configuration form for the `dsh-zen-remote` plugin row, in the same
3
+ * shape the dsh-llm-verifier page uses (a control stages what the user types,
4
+ * one save writes every staged edit as a single revision-fenced path mutation
5
+ * through the shared `configForms` form). Browser-import-free so tests drive
6
+ * it directly:
7
+ *
8
+ * - `deriveSettingsView(status)` maps the `GET /_dsh/zen-remote/admin/status`
9
+ * body (T14's same-origin route wrapping the gateway's local admin API) into
10
+ * what the settings block renders: per-field effective value + source layer,
11
+ * the device list, the live pairing code with its remaining seconds, and the
12
+ * `viaGateway` flag that disables every server-local operation.
13
+ * - `deriveClientStatusView(status)` maps the `GET /_dsh/zen-remote/client/status`
14
+ * body (T16's sub-client probe) into the client group's connection line.
15
+ * - `ZenRemoteSettingsForm` stages the row-layer field edits and saves them as
16
+ * one `mutate(ops, expectedRevision)` call, and owns the two DIRECT writes
17
+ * of the client group (pairing write / token clear) plus the device token's
18
+ * configured flag read from the describe view's secrets sidecar. Since T17
19
+ * it also carries the page's two-level role decision — the saved row role
20
+ * first, the client-config probe as fallback — and the `restartPending`
21
+ * flag behind the "saving reloads the plugin" note.
22
+ *
23
+ * The wire contract lives here because the host half that serves it is a
24
+ * separate task; the shapes mirror `lib/lan-gate-server.cjs`'s status payload
25
+ * (devices / pairing) plus the T14 envelope (ok / gateway / config / viaGateway).
26
+ */
27
+ /** Where a resolved field value came from (mirror of src/config.ts's union; the client half cannot import the host module). */
28
+ export type ConfigSource = 'env' | 'row' | 'file' | 'default';
29
+ /** One paired gateway device, as the gateway's status payload lists it. */
30
+ export interface AdminDevice {
31
+ id: string;
32
+ name: string;
33
+ role: 'web' | 'desktop-client';
34
+ kind: 'auto' | 'phone' | 'desktop';
35
+ createdAt: number;
36
+ lastSeen: number;
37
+ ua?: string;
38
+ hasPush: boolean;
39
+ }
40
+ /** The one currently-live pairing code, if any. */
41
+ export interface AdminPairing {
42
+ code: string;
43
+ expiresAt: number;
44
+ role: 'web' | 'desktop-client';
45
+ }
46
+ /**
47
+ * The gateway's `/lan-gate/status` reply, relayed verbatim inside
48
+ * `admin/status`'s `gateway` field (see lib/lan-gate-server.cjs's
49
+ * statusHandler — this mirror keeps only what the settings block reads).
50
+ */
51
+ export interface GatewayStatus {
52
+ state?: string;
53
+ port?: number;
54
+ target?: string;
55
+ devices?: AdminDevice[];
56
+ pairing?: AdminPairing | null;
57
+ pushSubscriptions?: number;
58
+ }
59
+ /**
60
+ * The `GET /_dsh/zen-remote/admin/status` body, exactly as src/admin-routes.ts
61
+ * answers it: the gateway payload lives NESTED under `gateway` (null when the
62
+ * gateway did not answer healthily) and `gatewayStatus` is the HTTP status the
63
+ * route saw (`null` when there was no answer at all).
64
+ */
65
+ export interface AdminStatusBody {
66
+ ok?: boolean;
67
+ gateway?: GatewayStatus | null;
68
+ gatewayReachable?: boolean;
69
+ gatewayStatus?: number | null;
70
+ config?: {
71
+ values?: Record<string, unknown>;
72
+ sources?: Record<string, string>;
73
+ };
74
+ viaGateway?: boolean;
75
+ }
76
+ /** Same-origin admin routes the settings block talks to (host half: T14). */
77
+ export declare const ADMIN_STATUS_ROUTE = "/_dsh/zen-remote/admin/status";
78
+ export declare const ADMIN_PAIR_ROUTE = "/_dsh/zen-remote/admin/pair";
79
+ export declare const ADMIN_ACTION_ROUTE = "/_dsh/zen-remote/admin/action";
80
+ export declare const ADMIN_PUSH_TEST_ROUTE = "/_dsh/zen-remote/admin/push-test";
81
+ /** Same-origin client routes the sub-client block talks to (host half: T16). */
82
+ export declare const CLIENT_CLAIM_ROUTE = "/_dsh/zen-remote/client/claim";
83
+ export declare const CLIENT_STATUS_ROUTE = "/_dsh/zen-remote/client/status";
84
+ /** T43: one immediate reconnect — answered 409 unless the client is offline. */
85
+ export declare const CLIENT_RECONNECT_ROUTE = "/_dsh/zen-remote/client/reconnect";
86
+ /** The lightweight client-facing config route (host half, both roles): the
87
+ * settings page's FALLBACK role probe (T17) — it registers wherever a
88
+ * webServer exists, and since T17 its body carries the effective `role`
89
+ * (resolveConfig's merged value, nothing sensitive). */
90
+ export declare const CLIENT_CONFIG_ROUTE = "/_dsh/mobile-nav/client-config";
91
+ /** The body of `GET /_dsh/mobile-nav/client-config` — the interface-half knobs
92
+ * the client bundle reads, plus (T17) the effective role the settings page
93
+ * falls back to. */
94
+ export interface ClientConfigBody {
95
+ role?: unknown;
96
+ turnFoldDesktop?: boolean;
97
+ keyboardLiftRatio?: number;
98
+ keyboardLiftMaxPx?: number;
99
+ keyboardSafetyPadPx?: number;
100
+ }
101
+ /** Field name of the row secret the pairing flow writes (never echoed back
102
+ * anywhere; the describe view's secrets sidecar is the only "is it set"). */
103
+ export declare const DEVICE_TOKEN_FIELD = "deviceToken";
104
+ /**
105
+ * The `GET /_dsh/zen-remote/client/status` body, exactly as
106
+ * src/client-routes.ts answers it: `serverUrl` is present only once a token
107
+ * exists (the unpaired answer is `{ state: 'unpaired' }` alone). The token
108
+ * itself never rides any status response. The T43 diagnostics fields ride
109
+ * only on a failed live connect of a wired relay client; `intercept` (T23b)
110
+ * and `compat` (T42) are rendered only when the body carries them — earlier
111
+ * servers answer neither.
112
+ */
113
+ export interface ClientStatusBody {
114
+ /** The probe vocabulary (`connected` / `unreachable` / `revoked` /
115
+ * `unexpected`), the row's two own answers (`unpaired` / `invalid-url`),
116
+ * the relay verdicts (`revoked` / `incompatible`) — and, since the
117
+ * relay-wired route reports the relay client's own state verbatim, the
118
+ * `online` / `offline` words too ({@link deriveClientStatusView} maps
119
+ * them; T55). */
120
+ state?: string;
121
+ serverUrl?: string;
122
+ serverName?: unknown;
123
+ /** Epoch ms of the relay client's next automatic reconnect (offline only). */
124
+ nextRetryAt?: unknown;
125
+ /** The error code of the last failure — a code, never a message or URL. */
126
+ lastError?: unknown;
127
+ /**
128
+ * The SERVER's record of THIS device's name (T59, presence-gated like the
129
+ * diagnostics fields — older servers answer without it). The settings page
130
+ * follows it into the row while the user is not editing the field.
131
+ */
132
+ deviceName?: unknown;
133
+ /** T23b request-interceptor diagnostics (provisional shape; presence-gated). */
134
+ intercept?: unknown;
135
+ /** T42 relay compat diagnostics: `{ identical: string[], different:
136
+ * string[], unavailable: string[], incompatibleCalls: { time: number,
137
+ * endpoint: string, code: string }[] }` (presence-gated). */
138
+ compat?: unknown;
139
+ }
140
+ /** One remote-call failure line in the diagnostics lists. `time` is epoch
141
+ * milliseconds — ISO-stamped rings are parsed at derive time; `method`
142
+ * carries the wire's method-or-endpoint name. */
143
+ export interface ClientDiagFailureView {
144
+ time: number;
145
+ method: string;
146
+ code: string;
147
+ }
148
+ /**
149
+ * T23b interceptor diagnostics as the block renders them. The wire field is
150
+ * presence-gated: `undefined` here means the body carried none (an older
151
+ * server), and the whole interceptor group stays hidden.
152
+ */
153
+ export interface ClientInterceptView {
154
+ installed: boolean;
155
+ /** Shape-detection failure reasons — non-empty means remote features are off. */
156
+ reasons: string[];
157
+ /** The most recent remote call failures (at most 10). */
158
+ recentFailures: ClientDiagFailureView[];
159
+ }
160
+ /** T42 compat diagnostics as the block renders them (presence-gated like {@link ClientInterceptView}). */
161
+ export interface ClientCompatView {
162
+ /** Names of the groups whose fingerprints differ. */
163
+ mismatchedGroups: string[];
164
+ /** The most recent incompatible calls (at most 10). */
165
+ recentCalls: ClientDiagFailureView[];
166
+ }
167
+ /** One client connection as the block renders it. */
168
+ export interface ClientConnectionView {
169
+ state: 'unpaired' | 'connected' | 'revoked' | 'unreachable' | 'unexpected' | 'invalid-url' | 'incompatible';
170
+ serverUrl: string;
171
+ serverName: string;
172
+ /** Verbatim from the body when it carried a finite number; the countdown
173
+ * math happens at render time against the live clock. */
174
+ nextRetryAt: number | undefined;
175
+ /** The last failure's code, '' when none is reported. */
176
+ lastError: string;
177
+ /** The SERVER's record of this device's name (T59): '' when the body did
178
+ * not carry one — empty means "nothing to follow". */
179
+ deviceName: string;
180
+ intercept: ClientInterceptView | undefined;
181
+ compat: ClientCompatView | undefined;
182
+ }
183
+ /**
184
+ * Map one `client/status` body into the view the client group renders.
185
+ * Tolerant like {@link deriveSettingsView}: an unexpected shape degrades to
186
+ * "unpaired" instead of throwing into the plugin page.
187
+ */
188
+ export declare function deriveClientStatusView(body: ClientStatusBody): ClientConnectionView;
189
+ /**
190
+ * Which connection line the block renders (T43 diagnostics wording, chosen
191
+ * as a pure descriptor so the copy table and the countdown math stay
192
+ * testable without a browser): connected prefers the handshake's server
193
+ * name; any verdict the body backs with a `nextRetryAt` is OFFLINE first —
194
+ * the relay client is mid-retry and the line counts down to it, whether the
195
+ * probe classified the failure `unreachable` or `unexpected` (T43-fix);
196
+ * without a `nextRetryAt` the two map one plain copy each. The rest map one
197
+ * state each.
198
+ */
199
+ export type ClientStatusLine = {
200
+ kind: 'connectedName';
201
+ serverName: string;
202
+ } | {
203
+ kind: 'connected';
204
+ serverUrl: string;
205
+ } | {
206
+ kind: 'offlineRetry';
207
+ seconds: number;
208
+ } | {
209
+ kind: 'offlineRetrySoon';
210
+ } | {
211
+ kind: 'unreachable';
212
+ } | {
213
+ kind: 'revoked';
214
+ } | {
215
+ kind: 'incompatible';
216
+ } | {
217
+ kind: 'unexpected';
218
+ } | {
219
+ kind: 'invalidUrl';
220
+ } | {
221
+ kind: 'unpaired';
222
+ };
223
+ export declare function clientStatusLineOf(view: ClientConnectionView, now: number): ClientStatusLine;
224
+ /** The `POST /_dsh/zen-remote/client/claim` body (T16's pairing round-trip;
225
+ * on success `token` is the gateway-minted device token — it appears exactly
226
+ * once, on its way into the row's secret field). */
227
+ export interface ClaimRouteBody {
228
+ ok?: boolean;
229
+ token?: unknown;
230
+ deviceId?: unknown;
231
+ deviceName?: unknown;
232
+ /** The normalized address the backend validated — what the form writes. */
233
+ serverUrl?: unknown;
234
+ code?: string;
235
+ message?: string;
236
+ retryAfterMs?: number;
237
+ }
238
+ /** What the pairing-code box keeps as its draft: uppercase, no spaces or
239
+ * hyphens (the gateway strips every other character at claim time anyway). */
240
+ export declare function normalizePairingCode(input: string): string;
241
+ /**
242
+ * Whether the settings page may follow the SERVER's record of this device's
243
+ * name into the row (T59): the body carried a name, it differs from the row
244
+ * (nothing to do otherwise — and this equality is the anti-bounce half: the
245
+ * page's own write echoes back through client/status with both sides equal),
246
+ * and the user has no staged draft in the field (their edit wins until they
247
+ * save — a follow mid-typing would clobber it).
248
+ */
249
+ export declare function shouldFollowServerDeviceName(deviceName: string, rowName: string, hasDraft: boolean): boolean;
250
+ /**
251
+ * Latest-wins sequencing for the status loads: every request takes a ticket,
252
+ * and only the newest ticket may still apply its result. An earlier request
253
+ * that answers LATE is dropped, so a stale body can never overwrite a fresh
254
+ * one (a revoked device reappearing, an old "no pairing code" answer wiping
255
+ * a just-minted code, an unpair racing a refresh).
256
+ */
257
+ export interface LatestGate {
258
+ /** Issue the ticket for one new in-flight request. */
259
+ next(): number;
260
+ /** Whether that ticket is still the newest issued one. */
261
+ isLatest(ticket: number): boolean;
262
+ }
263
+ export declare function createLatestGate(): LatestGate;
264
+ /**
265
+ * The SAVED role of the plugin row, read from the configForms snapshot's row
266
+ * document — the settings page's FIRST role signal (T16-fix 3). The resolved
267
+ * role an admin/status body reports is unusable here: a client deployment has
268
+ * no admin route at all, and a stale kept body pinned the page to the old
269
+ * role after a switch. `value` is the schema-resolved section the Host
270
+ * accepted; a row value that only lives in the raw user layer reads from
271
+ * there. Since T17 every row field is volatile, so the form (and this
272
+ * snapshot) carries every stored field — `role` included, with any volatile
273
+ * wrapper already peeled by the host's plainConfig (the dsh-settings
274
+ * describe path calls `value.get()` recursively before the wire).
275
+ *
276
+ * `undefined` when NEITHER layer carries a role: the field's value lives only
277
+ * in a lower layer (`lan-gate.config.json`) or nowhere. That is not "host" —
278
+ * the caller falls back to the effective role the client-config route
279
+ * reports ({@link settingsPollOf}), because a file-layer client row must
280
+ * still render the client page.
281
+ */
282
+ export declare function savedRowRole(snapshot: {
283
+ value?: unknown;
284
+ user?: unknown;
285
+ }): 'host' | 'client' | undefined;
286
+ /**
287
+ * The EFFECTIVE role for one scope snapshot — the shared two-level decision
288
+ * every role-aware surface uses (the settings page via {@link settingsPollOf},
289
+ * the T33b session-sharing parts through the remote-share registration):
290
+ * while the namespace mirror is still loading the role is not knowable
291
+ * (`'unknown'` — a client deployment must never see a wasted `admin/*` 404);
292
+ * a snapshot whose row carries a role decides from it; a row-silent snapshot
293
+ * falls back to `probed`, the effective role fetched from
294
+ * `/_dsh/mobile-nav/client-config` (T17: the route carries the merged role),
295
+ * and stays `'unknown'` until that probe answers.
296
+ */
297
+ export declare function settingsRoleOf(status: 'loading' | 'ready' | 'unavailable', snapshot: {
298
+ value?: unknown;
299
+ user?: unknown;
300
+ }, probed?: 'host' | 'client'): 'unknown' | 'host' | 'client';
301
+ /**
302
+ * Which status source the page polls for one scope snapshot —
303
+ * {@link settingsRoleOf} mapped onto poll sources (`'unknown'` polls
304
+ * nothing). See there for the decision.
305
+ */
306
+ export declare function settingsPollOf(status: 'loading' | 'ready' | 'unavailable', snapshot: {
307
+ value?: unknown;
308
+ user?: unknown;
309
+ }, probed?: 'host' | 'client'): 'none' | 'admin' | 'client';
310
+ /**
311
+ * The row fields whose changed value reloads the plugin row after a save —
312
+ * re-exported from src/restart-fields.ts, the single list the host half's
313
+ * restart watcher fingerprints too (T17b collapsed the two copies; the leaf
314
+ * module is dependency-free so the client bundler can inline it). Only the
315
+ * fields this page edits can ever stage a draft here — `targetPort` /
316
+ * `pushEvents` are not page fields — but the list is the full shared set so
317
+ * a host-side change surfaces in the diff.
318
+ */
319
+ export { RESTART_FIELDS } from '../restart-fields.ts';
320
+ /** One path-addressed edit a save sends (the wire `SettingsPathOpView` shape). */
321
+ export type SettingsFormOp = {
322
+ op: 'set';
323
+ path: string[];
324
+ value: unknown;
325
+ } | {
326
+ op: 'unset';
327
+ path: string[];
328
+ };
329
+ /** Structural subset of the shared `ConfigForm` snapshot this form reads. */
330
+ export interface SettingsFormScopeSnapshot {
331
+ status: 'loading' | 'ready' | 'unavailable';
332
+ value: Record<string, unknown> | undefined;
333
+ /** Composition layer; what a field reverts to once cleared. */
334
+ base: unknown;
335
+ /** Raw user layer as stored; field PRESENCE here marks an override. */
336
+ user: unknown;
337
+ /** Revision fencing the next write; sent back as `expectedRevision`. */
338
+ revision: number | undefined;
339
+ writable: boolean;
340
+ }
341
+ /**
342
+ * The form face this controller stages over (structural subset of
343
+ * ui-settings' `ConfigForm` — declared locally because this package does not
344
+ * depend on the ui-settings types).
345
+ */
346
+ export interface SettingsFormScope {
347
+ getSnapshot(): SettingsFormScopeSnapshot;
348
+ subscribe(listener: () => void): () => void;
349
+ mutate(ops: readonly SettingsFormOp[], expectedRevision?: number): Promise<boolean>;
350
+ }
351
+ /**
352
+ * One secret path in the describe view's sidecar: the path plus whether it
353
+ * is set — the VALUE itself never rides any describe surface.
354
+ */
355
+ export interface DescribeSecret {
356
+ path: string[];
357
+ set: boolean;
358
+ }
359
+ /**
360
+ * The `configForms.describe()` mirror (structural, like {@link ConfigFormsLike}
361
+ * — the dsh-client-ui-settings package is not a dependency). Read only for
362
+ * the secrets sidecar: whether the row's `deviceToken` is set.
363
+ */
364
+ export interface ConfigFormsDescribe {
365
+ getSnapshot(): {
366
+ view?: {
367
+ namespaces?: ReadonlyArray<{
368
+ ns: string;
369
+ secrets?: ReadonlyArray<DescribeSecret>;
370
+ }>;
371
+ };
372
+ };
373
+ subscribe(listener: () => void): () => void;
374
+ }
375
+ /**
376
+ * The `configForms` service face this plugin uses, mirrored onto the cordis
377
+ * Context. Declared locally (the dsh-client-ui-settings package is not a
378
+ * dependency — same trick as the local `UiWorkspaceLike`): the runtime
379
+ * instance is provided by the host, and the lazy `ctx.inject(['configForms'], …)`
380
+ * in register-settings keeps a composition without the service loadable.
381
+ */
382
+ export interface ConfigFormsLike {
383
+ get(entryId: string): SettingsFormScope;
384
+ describe(): ConfigFormsDescribe;
385
+ whileServed(namespaces: readonly string[], register: (served: ReadonlySet<string>) => () => void): () => void;
386
+ }
387
+ /** The write one staged draft performs when the form is saved. */
388
+ export type SettingsFieldWrite = {
389
+ kind: 'set';
390
+ value: unknown;
391
+ } | {
392
+ kind: 'clear';
393
+ };
394
+ /** How one field converts between its stored value and draft text. */
395
+ export interface SettingsFieldSpec {
396
+ field: string;
397
+ format(value: unknown): string;
398
+ /** The staged write, or undefined when the draft is not a value this field accepts. */
399
+ parse(text: string): SettingsFieldWrite | undefined;
400
+ }
401
+ /** Free-text field: an empty draft clears, so the field re-inherits its default. */
402
+ export declare function textField(field: string): SettingsFieldSpec;
403
+ /** Whole-number field within inclusive bounds; empty clears, anything else out of range blocks the save. */
404
+ export declare function intField(field: string, min: number, max: number): SettingsFieldSpec;
405
+ /** Finite number in (min, max] — the `idleHours` shape: zero excluded, 8760 allowed. */
406
+ export declare function hoursField(field: string, max: number): SettingsFieldSpec;
407
+ /** `serverName`: an empty draft clears; a blank-but-nonempty draft or one over
408
+ * the 40-character cap is invalid and blocks the save (src/config.ts clips at
409
+ * resolve time, so an over-long write would silently lose its tail). */
410
+ export declare function nameField(field: string, max: number): SettingsFieldSpec;
411
+ /**
412
+ * The name one pairing claim registers under (T57, trimmed since T60): the
413
+ * shared 设备名称 field's CURRENT displayed value — a staged draft when one
414
+ * exists — trimmed FIRST and judged on the trimmed text (a 40-character name
415
+ * with padded spaces is legal; whitespace alone is not a name), returned
416
+ * trimmed so what the claim SENDS is exactly what a later save would store.
417
+ * Falls back to the default device-name copy otherwise. The fallback is
418
+ * load-bearing since T55: a client page reads no admin baseline, so an
419
+ * unstored name displays EMPTY and the claim must still carry a usable name.
420
+ */
421
+ export declare function claimDeviceNameOf(displayText: string, fallback: string): string;
422
+ /** One-of field over a fixed vocabulary (rendered as a select). */
423
+ export declare function oneOfField(field: string, options: readonly string[]): SettingsFieldSpec;
424
+ /** Boolean field staged as 'true'/'false' draft text (rendered as a checkbox). */
425
+ export declare function boolField(field: string): SettingsFieldSpec;
426
+ /** Names of the row fields the settings page edits, in group order. */
427
+ export type SettingsFieldName = 'role' | 'host' | 'port' | 'trustedProxies' | 'rateLimit' | 'vapidSubject' | 'pushSummary' | 'pushTurnEnd' | 'pushTool' | 'pushDebounceMs' | 'lang' | 'serverName' | 'idleHours' | 'autoShareNewSessions';
428
+ /** Longest legal `serverName`, mirroring src/config.ts's cap. */
429
+ export declare const SERVER_NAME_MAX = 40;
430
+ /** Upper bound of `idleHours`, mirroring src/config.ts. */
431
+ export declare const IDLE_HOURS_MAX = 8760;
432
+ /** The row fields the settings page edits, in render order. */
433
+ export declare const SETTINGS_FIELDS: readonly SettingsFieldSpec[];
434
+ /**
435
+ * Whether the settings page's server-local operations (pairing, device
436
+ * management, the push probe) are usable: only once a status load answered
437
+ * AND it answered as the local machine (`viaGateway: false`). Everything
438
+ * else — no answer yet, or the page opened through the gateway — keeps them
439
+ * disabled. Pure so the check script pins all three cases.
440
+ */
441
+ export declare function localOpsAllowed(view: SettingsView | undefined): boolean;
442
+ /** Per-field presentation facts the block renders next to each control. */
443
+ export interface SettingsFieldView {
444
+ /** Effective value (the resolved `config.values[field]`). */
445
+ value: unknown;
446
+ /** Layer that supplied the effective value. */
447
+ source: ConfigSource;
448
+ /** `source === 'env'`: shown locked, edits would be shadowed. */
449
+ locked: boolean;
450
+ /** `source === 'file'`: the legacy lan-gate.config.json supplies this. */
451
+ fromFile: boolean;
452
+ /** The row layer stores this field but another layer supplied the value —
453
+ * i.e. the saved value was illegal and resolveConfig skipped it. */
454
+ savedRowInvalid: boolean;
455
+ }
456
+ /** One device row the block renders. */
457
+ export interface SettingsDeviceView {
458
+ id: string;
459
+ name: string;
460
+ role: 'web' | 'desktop-client';
461
+ kind: 'auto' | 'phone' | 'desktop';
462
+ lastSeen: number;
463
+ hasPush: boolean;
464
+ }
465
+ /** The live pairing code with its countdown, or null when none is active. */
466
+ export interface SettingsPairingView {
467
+ code: string;
468
+ role: 'web' | 'desktop-client';
469
+ remainingSeconds: number;
470
+ }
471
+ /** Everything the settings block renders, derived from one status body. */
472
+ export interface SettingsView {
473
+ /** The status route answered ok. */
474
+ available: boolean;
475
+ /** Which half this DSH process runs as (`client` shows only the role group). */
476
+ role: 'host' | 'client';
477
+ /** Opened through the gateway (a remote device): server-local operations disabled. */
478
+ viaGateway: boolean;
479
+ /** Whether the gateway child answers. */
480
+ gatewayReachable: boolean;
481
+ gatewayPort: number | undefined;
482
+ gatewayTarget: string | undefined;
483
+ /** The probe status the admin route saw from the gateway; null = no answer. */
484
+ gatewayStatus: number | null;
485
+ /** `gatewayStatus` is a non-2xx reply: the status line shows the abnormal message instead. */
486
+ gatewayAbnormal: boolean;
487
+ fields: Record<SettingsFieldName, SettingsFieldView>;
488
+ devices: SettingsDeviceView[];
489
+ pairing: SettingsPairingView | null;
490
+ }
491
+ export interface DeriveSettingsOptions {
492
+ /** Clock for the pairing countdown; defaults to `Date.now()`. */
493
+ now?: number;
494
+ /** The shared form snapshot's raw user layer; field presence marks a row-layer override. */
495
+ rowUser?: unknown;
496
+ }
497
+ /**
498
+ * Map one `admin/status` body into the view the settings block renders.
499
+ * Tolerant by design: every wire field is optional, an unexpected shape
500
+ * degrades to the conservative rendering (status unavailable, gateway down,
501
+ * no devices, no pairing) instead of throwing into the plugin page.
502
+ */
503
+ export declare function deriveSettingsView(status: AdminStatusBody, options?: DeriveSettingsOptions): SettingsView;
504
+ /** Card-level state the shared form frame renders. */
505
+ export interface SettingsFormShellState {
506
+ available: boolean;
507
+ writable: boolean;
508
+ dirty: boolean;
509
+ invalid: boolean;
510
+ saving: boolean;
511
+ failed: boolean;
512
+ }
513
+ /** One control's state as its field renders it. */
514
+ export interface SettingsFieldState {
515
+ text: string;
516
+ overridden: boolean;
517
+ invalid: boolean;
518
+ /** A clear is staged: the box keeps showing the CURRENT value — the next
519
+ * layer's value only exists after the save — and the field renders the
520
+ * "reverts on save" hint instead of a fake preview. */
521
+ cleared: boolean;
522
+ /** `admin/status` reported this field's value locked by an environment variable. */
523
+ locked: boolean;
524
+ }
525
+ /** The row secret's face: presence only, never a value. */
526
+ export interface SettingsSecretFieldState {
527
+ /** The describe view's secrets sidecar reports `deviceToken` set. */
528
+ configured: boolean;
529
+ }
530
+ /** The whole staged-form snapshot the page renders. */
531
+ export interface ZenRemoteFormState extends SettingsFormShellState {
532
+ /** The shared form's scope sync state, reactively exposed so the page's
533
+ * role probe can wait out the mirror's loading phase. */
534
+ scopeStatus: 'loading' | 'ready' | 'unavailable';
535
+ role: SettingsFieldState;
536
+ host: SettingsFieldState;
537
+ port: SettingsFieldState;
538
+ trustedProxies: SettingsFieldState;
539
+ rateLimit: SettingsFieldState;
540
+ vapidSubject: SettingsFieldState;
541
+ pushSummary: SettingsFieldState;
542
+ pushTurnEnd: SettingsFieldState;
543
+ pushTool: SettingsFieldState;
544
+ pushDebounceMs: SettingsFieldState;
545
+ lang: SettingsFieldState;
546
+ serverName: SettingsFieldState;
547
+ idleHours: SettingsFieldState;
548
+ autoShareNewSessions: SettingsFieldState;
549
+ deviceToken: SettingsSecretFieldState;
550
+ /** A restart-required field ({@link RESTART_FIELDS}) has a staged change a
551
+ * save would actually write: the save will move the row and the host
552
+ * reloads it, briefly restarting the gateway — the page shows the reload
553
+ * note while this is true. Since T17b it is not merely "a draft exists":
554
+ * a draft equal to the displayed effective value is no change at all, an
555
+ * invalid draft blocks the save instead of saving anything, and an
556
+ * env-locked field never stages (nor counts if locked after staging). */
557
+ restartPending: boolean;
558
+ }
559
+ /**
560
+ * Stages one page's edits over the plugin row's shared form and writes them on
561
+ * save as one atomic, revision-fenced mutation. Fields whose `admin/status`
562
+ * source is `env` are locked via {@link setLockedFields}: they cannot be
563
+ * staged, because a written value would be shadowed by the environment anyway.
564
+ */
565
+ export declare class ZenRemoteSettingsForm {
566
+ private readonly listeners;
567
+ private readonly staged;
568
+ private readonly lockedFields;
569
+ /** Fields whose saved row value `admin/status` reported invalid
570
+ * (savedRowInvalid): those display the RAW stored value instead of the
571
+ * resolved one, so the user can see (and fix) what they actually wrote. */
572
+ private readonly rowInvalidFields;
573
+ private readonly scope;
574
+ /** Reads the describe view's secrets sidecar for `deviceToken`'s
575
+ * configured flag; injectable so tests run without a describe mirror. */
576
+ private readonly secretConfigured;
577
+ /** Effective values from `admin/status`'s `config.values` — what a field
578
+ * displays while the row layer does not carry it. Empty until the page's
579
+ * first status load feeds it via {@link setBaseline}. Consulted only while
580
+ * the page is NOT a client page (T55): see {@link displayValue}. */
581
+ private baseline;
582
+ /** The effective role the page probed from the client-config route (T17):
583
+ * the fallback for {@link savedRoleIsClient} / {@link statusPoll} when the
584
+ * row document cannot answer the saved role (role only in
585
+ * lan-gate.config.json). Undefined until that probe answers. */
586
+ private probedRole;
587
+ private saving;
588
+ private failed;
589
+ /** Revision the drafts started from; the save's `expectedRevision` fence. */
590
+ private stagedRevision;
591
+ private cache;
592
+ private readonly unsubscribe;
593
+ /**
594
+ * @param scope - the shared configuration form for the plugin row entry.
595
+ * (`scope` and `secretConfigured` are assigned in the body rather than as
596
+ * parameter properties: scripts/check-settings-form.mjs imports this module
597
+ * through Node's strip-only type stripping, which rejects that syntax.)
598
+ * @param secretConfigured - whether the row's device token is set, read
599
+ * from the describe view's secrets sidecar; defaults to "not set".
600
+ */
601
+ constructor(scope: SettingsFormScope, secretConfigured?: () => boolean);
602
+ /** @returns the current form snapshot (stable reference until the next change). */
603
+ getSnapshot(): ZenRemoteFormState;
604
+ /** Observe snapshot replacements (the renderer binds this as its store). */
605
+ subscribe(listener: () => void): () => void;
606
+ /** Republish from the current reads (admin/status moved underneath). */
607
+ refresh(): void;
608
+ /** Replace the set of environment-locked fields reported by admin/status. */
609
+ setLockedFields(fields: Iterable<string>): void;
610
+ /**
611
+ * Replace the set of fields whose saved row value resolveConfig rejected
612
+ * (admin/status's savedRowInvalid). Those keep showing the RAW stored
613
+ * value — the resolved value belongs to another layer and would hide the
614
+ * mistake the user needs to fix.
615
+ */
616
+ setRowInvalidFields(fields: Iterable<string>): void;
617
+ /**
618
+ * The entry document's stored value for one field (the shared form
619
+ * snapshot's `value` — the redacted section: secrets never ride it). What
620
+ * the client group prefills the server address from, and what the page
621
+ * reads the SAVED role from (the role field's display text also carries
622
+ * staged drafts, which must not flip the page's mode).
623
+ */
624
+ rowValue(field: string): unknown;
625
+ /**
626
+ * The scope sync state as the shared form sees it. The page waits it out
627
+ * before choosing a status source: a 'loading' snapshot cannot answer the
628
+ * role yet (T16-fix 3).
629
+ */
630
+ scopeStatus(): 'loading' | 'ready' | 'unavailable';
631
+ /**
632
+ * Which status source the page should poll right now ({@link settingsPollOf}
633
+ * over the live snapshot): `none` while the mirror loads — and while a row
634
+ * without a stored role waits for the client-config probe — `client` for a
635
+ * client row — never admin there — and `admin` otherwise.
636
+ */
637
+ statusPoll(): 'none' | 'admin' | 'client';
638
+ /**
639
+ * Feed the fallback role the page probed from the client-config route
640
+ * (T17). Only consulted when the row document carries no `role` — a stored
641
+ * row role always wins, so a staged-then-saved switch is never masked by a
642
+ * stale probe.
643
+ */
644
+ setProbedRole(role: 'host' | 'client' | undefined): void;
645
+ /**
646
+ * Whether the SAVED row role is client ({@link savedRowRole} over the live
647
+ * snapshot, falling back to the probed role) — the page mode's single
648
+ * source of truth.
649
+ */
650
+ savedRoleIsClient(): boolean;
651
+ /**
652
+ * Whether the row document answers the saved role at all (T17): false when
653
+ * neither snapshot layer carries one — the page then probes the
654
+ * client-config route for the effective role instead of assuming host.
655
+ */
656
+ rowRoleKnown(): boolean;
657
+ /**
658
+ * One direct write for the pairing flow (T16): the normalized server
659
+ * address, the token just redeemed from the server, and (T60) the name
660
+ * actually registered with — the SERVER's confirmed echo when the claim
661
+ * carried one, else the name the claim sent — ride ONE revision-fenced
662
+ * mutate. The name lands in the row's `serverName` (the role card's 设备
663
+ * 名称 field), so a pairing that used an UNSAVED draft can never leave the
664
+ * local row empty while the server records the name. Not a staged edit —
665
+ * all three fields are volatile row settings and apply immediately. The
666
+ * caller drops the field's stale draft afterwards (see
667
+ * {@link discardField}). @returns whether the write landed.
668
+ */
669
+ writeClientPairing(serverUrl: string, token: string, deviceName: string): Promise<boolean>;
670
+ /**
671
+ * One direct write for unpairing: forget the token, keep the address so
672
+ * the next pairing only needs a fresh code.
673
+ */
674
+ clearDeviceToken(): Promise<boolean>;
675
+ /**
676
+ * Drop ONE field's staged draft (T60): after the pairing write landed the
677
+ * name in the row, the draft that produced it is stale — it now equals the
678
+ * displayed value, and keeping it would show a phantom "overridden" badge.
679
+ * The revision fence goes with the last draft (there is nothing staged
680
+ * left to fence); an in-flight save-failure flag is not touched — that is
681
+ * the staged-save surface's own story.
682
+ */
683
+ discardField(field: string): void;
684
+ /**
685
+ * Whether one field currently has a STAGED edit (a typed draft or a staged
686
+ * clear) — the "the user is mid-edit" fact the T59 server-name follow
687
+ * reads before it may write. Reads the live draft map, so it answers per
688
+ * call; the page re-runs its effect on every republished snapshot.
689
+ */
690
+ hasDraft(field: string): boolean;
691
+ /**
692
+ * One direct write for the T59 name follow: the SERVER's record of this
693
+ * device's name landing in the row's `serverName` (the role card's 设备
694
+ * 名称 field). The page only calls it when the user is not mid-edit —
695
+ * see {@link shouldFollowServerDeviceName} — and the write's own volatile
696
+ * update echoes back to the backend with both sides now equal, so the
697
+ * push path stays silent (no overwrite loop).
698
+ */
699
+ writeDeviceName(name: string): Promise<boolean>;
700
+ /**
701
+ * The shared write path of the two pairing flows. Deliberately does NOT
702
+ * touch the frame's `failed` flag — a refused pairing write surfaces in
703
+ * the client group's own copy, not as a staged-save failure.
704
+ */
705
+ private directWrite;
706
+ /**
707
+ * Feed the effective values (`admin/status`'s `config.values`) the fields
708
+ * display while the row layer does not carry them; also the baseline the
709
+ * "did the user change anything" comparison reads. The page clears it
710
+ * (`undefined`) whenever the poll source is not the admin one (T55): a
711
+ * stale host baseline must not survive a role switch on a client page.
712
+ */
713
+ setBaseline(values: unknown): void;
714
+ /** Stage draft text for one row field; ignored for env-locked fields. */
715
+ stage(field: string, text: string): void;
716
+ /** Stage a clear so the field re-inherits the composition layer; ignored for env-locked fields. */
717
+ resetField(field: string): void;
718
+ /** Drop every staged edit. */
719
+ discard(): void;
720
+ /**
721
+ * The raw user layer as the shared form stores it; a field's presence here
722
+ * marks a row-layer override, which is what `savedRowInvalid` compares
723
+ * against.
724
+ */
725
+ rowUser(): Record<string, unknown>;
726
+ /** Whether a save would do anything and is allowed to run right now. */
727
+ canSave(): boolean;
728
+ /**
729
+ * Write every staged edit as one mutation fenced by the revision the drafts
730
+ * started from. The Host is the only authority on acceptance: a refused
731
+ * save keeps its drafts for correction.
732
+ * @returns whether the save landed.
733
+ */
734
+ save(): Promise<boolean>;
735
+ /** Release the scope subscription. */
736
+ dispose(): void;
737
+ private noteStage;
738
+ /** Whether the user layer currently carries an entry for one field. */
739
+ private stored;
740
+ /**
741
+ * The value one field DISPLAYS, which is also the baseline a draft must
742
+ * differ from to count as a change: an env-locked field shows the effective
743
+ * value (a written one would be shadowed anyway); a field the row layer
744
+ * stores shows the RESOLVED value from admin/status — resolveConfig
745
+ * normalizes stored shapes (a trustedProxies array becomes the comma
746
+ * string, pushTool 1 becomes true) and the box must match what a save
747
+ * writes — EXCEPT when the saved row value was rejected (savedRowInvalid):
748
+ * then the raw stored value shows, so the user sees the mistake; before
749
+ * the first status load the stored raw value stands in, and otherwise the
750
+ * shared form's own effective layer does, so drafts behave sensibly even
751
+ * with no admin/status yet.
752
+ *
753
+ * T55: the admin/status baseline counts only while the page is not a CLIENT
754
+ * page. A role switch leaves the host page's kept status load (and with it
755
+ * the last-fed baseline) in place forever — a client page never refreshes
756
+ * admin/status again — so reading the baseline there would pin every field,
757
+ * the role dropdown included, to the old role. Under a client poll the
758
+ * fields fall back to the row's stored values (the page stops feeding the
759
+ * baseline too, {@link setBaseline}).
760
+ */
761
+ private displayValue;
762
+ private anyInvalid;
763
+ /**
764
+ * Every staged edit a save would send, in field order, mirroring the
765
+ * upstream form model's plan: a draft equal to the field's current value is
766
+ * no change at all; a reset only writes when the user layer actually
767
+ * carries the field; an invalid draft produces none — the save refuses
768
+ * rather than dropping the edit; a locked field never produces one.
769
+ */
770
+ private plan;
771
+ private project;
772
+ private publish;
773
+ }
774
+ //# sourceMappingURL=settings-form.d.ts.map