dsh-zen-remote 2.3.11 → 2.4.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/CHANGELOG.md +45 -78
- package/README.md +94 -84
- package/README.zh-CN.md +90 -81
- package/bin/zen-remote.mjs +5 -0
- package/docs/interface.md +6 -6
- package/docs/remote-access.en.md +6 -4
- package/docs/remote-access.md +6 -4
- package/dsh-push.mjs +96 -10
- package/lib/adapters/dsh-better-sidebar/index.js +7 -3
- package/lib/adapters/dsh-better-sidebar/index.js.map +1 -1
- package/lib/admin-routes.js +10 -2
- package/lib/admin-routes.js.map +1 -1
- package/lib/cli/args.js +251 -0
- package/lib/cli/args.js.map +1 -0
- package/lib/cli/bootstrap.js +449 -0
- package/lib/cli/bootstrap.js.map +1 -0
- package/lib/cli/command-setup.js +484 -0
- package/lib/cli/command-setup.js.map +1 -0
- package/lib/cli/commands-client.js +359 -0
- package/lib/cli/commands-client.js.map +1 -0
- package/lib/cli/commands-host.js +1108 -0
- package/lib/cli/commands-host.js.map +1 -0
- package/lib/cli/commands-status.js +279 -0
- package/lib/cli/commands-status.js.map +1 -0
- package/lib/cli/config-writer.js +334 -0
- package/lib/cli/config-writer.js.map +1 -0
- package/lib/cli/context.js +10 -0
- package/lib/cli/context.js.map +1 -0
- package/lib/cli/i18n.js +620 -0
- package/lib/cli/i18n.js.map +1 -0
- package/lib/cli/ipc-client.js +160 -0
- package/lib/cli/ipc-client.js.map +1 -0
- package/lib/cli/ipc-server.js +464 -0
- package/lib/cli/ipc-server.js.map +1 -0
- package/lib/cli/main.js +245 -0
- package/lib/cli/main.js.map +1 -0
- package/lib/cli/qr-terminal.js +48 -0
- package/lib/cli/qr-terminal.js.map +1 -0
- package/lib/client-data/qr.js +286 -0
- package/lib/client-data/qr.js.map +1 -0
- package/lib/client-routes.js +308 -25
- package/lib/client-routes.js.map +1 -1
- package/lib/client.js +1013 -229
- package/lib/config.js +50 -12
- package/lib/config.js.map +1 -1
- package/lib/debug-report.js +28 -1
- package/lib/debug-report.js.map +1 -1
- package/lib/fetch-route-intercept.js +334 -26
- package/lib/fetch-route-intercept.js.map +1 -1
- package/lib/index.js +608 -375
- package/lib/index.js.map +1 -1
- package/lib/intercept.js +1833 -280
- package/lib/intercept.js.map +1 -1
- package/lib/lan-gate-server.cjs +36 -10
- package/lib/merge-streams.js +103 -11
- package/lib/merge-streams.js.map +1 -1
- package/lib/plugin-route-intercept.js +93 -63
- package/lib/plugin-route-intercept.js.map +1 -1
- package/lib/relay-access.js +8 -1
- package/lib/relay-access.js.map +1 -1
- package/lib/relay-client.js +133 -4
- package/lib/relay-client.js.map +1 -1
- package/lib/relay-pool.js +275 -0
- package/lib/relay-pool.js.map +1 -0
- package/lib/relay-server.js +269 -17
- package/lib/relay-server.js.map +1 -1
- package/lib/restart-fields.js +6 -4
- package/lib/restart-fields.js.map +1 -1
- package/lib/share-ops.js +68 -0
- package/lib/share-ops.js.map +1 -1
- package/lib/share-store.js +65 -27
- package/lib/share-store.js.map +1 -1
- package/lib/state-store.js +400 -0
- package/lib/state-store.js.map +1 -0
- package/lib/types/adapters/dsh-better-sidebar/index.d.ts +3 -2
- package/lib/types/adapters/dsh-better-sidebar/index.d.ts.map +1 -1
- package/lib/types/admin-routes.d.ts.map +1 -1
- package/lib/types/cli/args.d.ts +78 -0
- package/lib/types/cli/args.d.ts.map +1 -0
- package/lib/types/cli/bootstrap.d.ts +170 -0
- package/lib/types/cli/bootstrap.d.ts.map +1 -0
- package/lib/types/cli/command-setup.d.ts +72 -0
- package/lib/types/cli/command-setup.d.ts.map +1 -0
- package/lib/types/cli/commands-client.d.ts +36 -0
- package/lib/types/cli/commands-client.d.ts.map +1 -0
- package/lib/types/cli/commands-host.d.ts +82 -0
- package/lib/types/cli/commands-host.d.ts.map +1 -0
- package/lib/types/cli/commands-status.d.ts +46 -0
- package/lib/types/cli/commands-status.d.ts.map +1 -0
- package/lib/types/cli/config-writer.d.ts +92 -0
- package/lib/types/cli/config-writer.d.ts.map +1 -0
- package/lib/types/cli/context.d.ts +39 -0
- package/lib/types/cli/context.d.ts.map +1 -0
- package/lib/types/cli/i18n.d.ts +32 -0
- package/lib/types/cli/i18n.d.ts.map +1 -0
- package/lib/types/cli/ipc-client.d.ts +83 -0
- package/lib/types/cli/ipc-client.d.ts.map +1 -0
- package/lib/types/cli/ipc-server.d.ts +113 -0
- package/lib/types/cli/ipc-server.d.ts.map +1 -0
- package/lib/types/cli/main.d.ts +57 -0
- package/lib/types/cli/main.d.ts.map +1 -0
- package/lib/types/cli/qr-terminal.d.ts +17 -0
- package/lib/types/cli/qr-terminal.d.ts.map +1 -0
- package/lib/types/client/RemoteStatusIcon.d.ts.map +1 -1
- package/lib/types/client/current-session-report.d.ts +4 -0
- package/lib/types/client/current-session-report.d.ts.map +1 -1
- package/lib/types/client/effects/model-group-side.d.ts.map +1 -1
- package/lib/types/client/effects/remote-session.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +47 -3
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/settings/SettingsSection.d.ts.map +1 -1
- package/lib/types/client/settings/settings-css.d.ts +1 -1
- package/lib/types/client/settings/settings-css.d.ts.map +1 -1
- package/lib/types/client/styles/remote-session.css.d.ts +1 -1
- package/lib/types/client/styles/remote-session.css.d.ts.map +1 -1
- package/lib/types/client/wizard/GatewayWizard.d.ts.map +1 -1
- package/lib/types/client/wizard/cards/ProxyCard.d.ts.map +1 -1
- package/lib/types/client/wizard/wizard-css.d.ts +1 -1
- package/lib/types/client/wizard/wizard-css.d.ts.map +1 -1
- package/lib/types/client-data/model-group-side.d.ts +23 -2
- package/lib/types/client-data/model-group-side.d.ts.map +1 -1
- package/lib/types/client-data/remote-status.d.ts +47 -1
- package/lib/types/client-data/remote-status.d.ts.map +1 -1
- package/lib/types/client-data/settings-form.d.ts +85 -1
- package/lib/types/client-data/settings-form.d.ts.map +1 -1
- package/lib/types/client-data/wizard.d.ts +56 -1
- package/lib/types/client-data/wizard.d.ts.map +1 -1
- package/lib/types/client-routes.d.ts +25 -1
- package/lib/types/client-routes.d.ts.map +1 -1
- package/lib/types/config.d.ts +40 -14
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/debug-report.d.ts.map +1 -1
- package/lib/types/fetch-route-intercept.d.ts +66 -8
- package/lib/types/fetch-route-intercept.d.ts.map +1 -1
- package/lib/types/index.d.ts +9 -2
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/intercept.d.ts +72 -10
- package/lib/types/intercept.d.ts.map +1 -1
- package/lib/types/merge-streams.d.ts +79 -1
- package/lib/types/merge-streams.d.ts.map +1 -1
- package/lib/types/plugin-route-intercept.d.ts +25 -9
- package/lib/types/plugin-route-intercept.d.ts.map +1 -1
- package/lib/types/relay-access.d.ts +3 -1
- package/lib/types/relay-access.d.ts.map +1 -1
- package/lib/types/relay-client.d.ts +23 -2
- package/lib/types/relay-client.d.ts.map +1 -1
- package/lib/types/relay-pool.d.ts +148 -0
- package/lib/types/relay-pool.d.ts.map +1 -0
- package/lib/types/relay-server.d.ts +30 -3
- package/lib/types/relay-server.d.ts.map +1 -1
- package/lib/types/restart-fields.d.ts +5 -5
- package/lib/types/restart-fields.d.ts.map +1 -1
- package/lib/types/share-ops.d.ts +47 -1
- package/lib/types/share-ops.d.ts.map +1 -1
- package/lib/types/share-store.d.ts.map +1 -1
- package/lib/types/state-store.d.ts +179 -0
- package/lib/types/state-store.d.ts.map +1 -0
- package/lib/types/upload-limits.d.ts +15 -0
- package/lib/types/upload-limits.d.ts.map +1 -1
- package/lib/types/virtual-id.d.ts +21 -0
- package/lib/types/virtual-id.d.ts.map +1 -1
- package/lib/types/wizard/probe.d.ts.map +1 -1
- package/lib/types/wizard/proxy-snippets.d.ts +3 -0
- package/lib/types/wizard/proxy-snippets.d.ts.map +1 -1
- package/lib/types/wizard/routes.d.ts +1 -0
- package/lib/types/wizard/routes.d.ts.map +1 -1
- package/lib/types/wizard/status.d.ts +9 -0
- package/lib/types/wizard/status.d.ts.map +1 -1
- package/lib/types/wizard/verify.d.ts +3 -0
- package/lib/types/wizard/verify.d.ts.map +1 -1
- package/lib/upload-limits.js +15 -0
- package/lib/upload-limits.js.map +1 -1
- package/lib/virtual-id.js +60 -0
- package/lib/virtual-id.js.map +1 -1
- package/lib/wizard/probe.js +46 -1
- package/lib/wizard/probe.js.map +1 -1
- package/lib/wizard/proxy-snippets.js +14 -1
- package/lib/wizard/proxy-snippets.js.map +1 -1
- package/lib/wizard/routes.js +23 -4
- package/lib/wizard/routes.js.map +1 -1
- package/lib/wizard/status.js +21 -0
- package/lib/wizard/status.js.map +1 -1
- package/lib/wizard/verify.js +31 -8
- package/lib/wizard/verify.js.map +1 -1
- package/package.json +8 -4
- package/pwa/icons/badge-96.png +0 -0
- package/pwa/icons/badge.svg +1 -0
- package/pwa/manifest.json +2 -1
- package/pwa/sw.js +3 -1
- package/scripts/check-debug-report.mjs +41 -1
- package/scripts/check-settings-form.mjs +14 -3
- package/scripts/check-settings-view.mjs +247 -0
- package/scripts/check-wizard-ui.mjs +37 -0
- package/src/adapters/dsh-better-sidebar/index.ts +7 -3
- package/src/admin-routes.ts +10 -2
- package/src/cli/args.ts +289 -0
- package/src/cli/bootstrap.ts +569 -0
- package/src/cli/command-setup.ts +540 -0
- package/src/cli/commands-client.ts +369 -0
- package/src/cli/commands-host.ts +1127 -0
- package/src/cli/commands-status.ts +296 -0
- package/src/cli/config-writer.ts +354 -0
- package/src/cli/context.ts +38 -0
- package/src/cli/i18n.ts +645 -0
- package/src/cli/ipc-client.ts +216 -0
- package/src/cli/ipc-server.ts +531 -0
- package/src/cli/main.ts +285 -0
- package/src/cli/qr-terminal.ts +49 -0
- package/src/client/RemoteStatusIcon.tsx +4 -1
- package/src/client/current-session-report.ts +28 -4
- package/src/client/effects/model-group-side.ts +19 -5
- package/src/client/effects/remote-session.ts +26 -6
- package/src/client/locales.ts +96 -6
- package/src/client/settings/SettingsSection.tsx +287 -92
- package/src/client/settings/settings-css.ts +18 -0
- package/src/client/styles/remote-session.css.ts +15 -1
- package/src/client/wizard/GatewayWizard.tsx +470 -109
- package/src/client/wizard/cards/ProxyCard.tsx +2 -1
- package/src/client/wizard/wizard-css.ts +23 -0
- package/src/client-data/model-group-side.ts +37 -10
- package/src/client-data/remote-status.ts +112 -17
- package/src/client-data/settings-form.ts +164 -8
- package/src/client-data/wizard.ts +112 -2
- package/src/client-routes.ts +334 -26
- package/src/config.ts +62 -16
- package/src/debug-report.ts +27 -1
- package/src/fetch-route-intercept.ts +381 -32
- package/src/index.ts +608 -380
- package/src/intercept.ts +1866 -286
- package/src/merge-streams.ts +155 -10
- package/src/plugin-route-intercept.ts +120 -71
- package/src/relay-access.ts +14 -3
- package/src/relay-client.ts +163 -5
- package/src/relay-pool.ts +387 -0
- package/src/relay-server.ts +312 -16
- package/src/restart-fields.ts +6 -4
- package/src/share-ops.ts +82 -1
- package/src/share-store.ts +59 -26
- package/src/state-store.ts +428 -0
- package/src/upload-limits.ts +16 -0
- package/src/virtual-id.ts +57 -0
- package/src/wizard/probe.ts +54 -2
- package/src/wizard/proxy-snippets.ts +17 -2
- package/src/wizard/routes.ts +24 -5
- package/src/wizard/status.ts +28 -0
- package/src/wizard/verify.ts +33 -11
- package/AGENTS.md +0 -185
- package/docs/adr/0001-plugin-bridge-forwards-plugin-routes.md +0 -15
- package/docs/adr/0002-relay-emulates-websocket.md +0 -8
|
@@ -23,7 +23,8 @@ export interface ProxyCardProps {
|
|
|
23
23
|
|
|
24
24
|
export function ProxyCard({ t, domain, upstreamPort, onCheck, onClose }: ProxyCardProps): ReactNode {
|
|
25
25
|
const [tab, setTab] = useState<SnippetKind>('nginx')
|
|
26
|
-
const
|
|
26
|
+
const lang: 'zh' | 'en' = t('wizard.card.close') === 'Close' ? 'en' : 'zh'
|
|
27
|
+
const input = { domain, upstreamHost: '127.0.0.1', upstreamPort, lang }
|
|
27
28
|
const text = tab === 'nginx'
|
|
28
29
|
? nginxSnippetText(input)
|
|
29
30
|
: tab === 'caddy' ? caddySnippetText(input) : (luckyStepTexts(input) ?? []).join('\n')
|
|
@@ -138,6 +138,18 @@ export const WIZARD_CSS = `
|
|
|
138
138
|
overflow-wrap: anywhere;
|
|
139
139
|
word-break: break-word;
|
|
140
140
|
}
|
|
141
|
+
/* MS-8: the pool cap's loud form — the 已配对服务端 cell's value and sub
|
|
142
|
+
wear data-limit="true" at 8/8 and turn bold red (the same loudness the
|
|
143
|
+
settings page's limit alert uses). */
|
|
144
|
+
[data-zen-remote="wizard"] .zr-wizard-overviewValue[data-limit="true"] {
|
|
145
|
+
color: var(--dsw-alias-state-error-primary, #d5304f);
|
|
146
|
+
font-weight: 700;
|
|
147
|
+
}
|
|
148
|
+
[data-zen-remote="wizard"] .zr-wizard-overviewSub[data-limit="true"],
|
|
149
|
+
[data-zen-remote="wizard-modal"] .zr-wizard-subtitle[data-limit="true"] {
|
|
150
|
+
color: var(--dsw-alias-state-error-primary, #d5304f);
|
|
151
|
+
font-weight: 600;
|
|
152
|
+
}
|
|
141
153
|
[data-zen-remote="wizard"] .zr-wizard-emptyBanner {
|
|
142
154
|
display: flex;
|
|
143
155
|
align-items: center;
|
|
@@ -773,6 +785,17 @@ export const WIZARD_CSS = `
|
|
|
773
785
|
background: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-module-platform));
|
|
774
786
|
color: var(--dsw-alias-label-secondary);
|
|
775
787
|
}
|
|
788
|
+
/* MS-8: the servers-modal state badge tones — green online, amber offline,
|
|
789
|
+
red for a verdict only a re-pair or an upgrade fixes. */
|
|
790
|
+
[data-zen-remote="wizard"] .zr-wizard-deviceBadge[data-tone="live"] {
|
|
791
|
+
color: var(--dsw-alias-state-success-primary, #16a34a);
|
|
792
|
+
}
|
|
793
|
+
[data-zen-remote="wizard"] .zr-wizard-deviceBadge[data-tone="warn"] {
|
|
794
|
+
color: var(--dsw-alias-state-warning-primary, #b8860b);
|
|
795
|
+
}
|
|
796
|
+
[data-zen-remote="wizard"] .zr-wizard-deviceBadge[data-tone="invalid"] {
|
|
797
|
+
color: var(--dsw-alias-state-error-primary, #d5304f);
|
|
798
|
+
}
|
|
776
799
|
[data-zen-remote="wizard"] .zr-wizard-deviceActions {
|
|
777
800
|
display: flex;
|
|
778
801
|
align-items: center;
|
|
@@ -28,7 +28,13 @@
|
|
|
28
28
|
* counts as a mark, so it can never decide a container by accident.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
import { VIRTUAL_GROUP_NAME_MARK } from '../virtual-id.ts'
|
|
31
|
+
import { VIRTUAL_GROUP_NAME_MARK, decodeServerIdZeroWidth } from '../virtual-id.ts'
|
|
32
|
+
|
|
33
|
+
/** MS-4: the <html> attribute carrying the open REMOTE session's server id
|
|
34
|
+
* (`zr~<serverId>~…`'s middle half). Written by effects/remote-session.ts
|
|
35
|
+
* together with `data-zr-remote-session`, read by
|
|
36
|
+
* effects/model-group-side.ts as the `activeRemoteServerId`. */
|
|
37
|
+
export const REMOTE_SERVER_ATTRIBUTE = 'data-zr-remote-server'
|
|
32
38
|
|
|
33
39
|
/** One group's display description, as the DOM shows it. */
|
|
34
40
|
export interface ModelGroupHeading {
|
|
@@ -42,10 +48,29 @@ export interface ModelGroupHeading {
|
|
|
42
48
|
/** Per-group verdict, in input order: `remote` for marked headings, `local`
|
|
43
49
|
* for the rest of a container that has at least one, and `undefined` —
|
|
44
50
|
* "stamp nothing" — for every group of a container that holds no mark at
|
|
45
|
-
* all.
|
|
46
|
-
|
|
51
|
+
* all. MS-4 adds `remote-other`: with a session's own server id known
|
|
52
|
+
* ({@link classifyModelGroupsForSession}'s `activeRemoteServerId`), a
|
|
53
|
+
* marked group whose DECODED server id is a DIFFERENT paired server — the
|
|
54
|
+
* model menu must show only the models of the server the open session
|
|
55
|
+
* lives on. */
|
|
56
|
+
export type ModelGroupSide = 'remote' | 'local' | 'remote-other' | undefined
|
|
47
57
|
|
|
48
58
|
export function classifyModelGroups(groups: readonly ModelGroupHeading[]): ModelGroupSide[] {
|
|
59
|
+
return classifyModelGroupsForSession(groups)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The MS-4 extension: with `activeRemoteServerId` set (the `zr~<serverId>~`
|
|
64
|
+
* half of the session the browser has open, effects/remote-session.ts),
|
|
65
|
+
* each marked heading's embedded server id (decodeServerIdZeroWidth)
|
|
66
|
+
* decides between plain `remote` (the open session's own server, or a
|
|
67
|
+
* heading whose server cannot be decoded — shown, never hidden) and
|
|
68
|
+
* `remote-other` (a DIFFERENT paired server's group — hidden in a remote
|
|
69
|
+
* session by remote-session.css.ts). Without the argument the verdicts
|
|
70
|
+
* are exactly {@link classifyModelGroups}'s — the single-paired-server
|
|
71
|
+
* behavior, byte for byte.
|
|
72
|
+
*/
|
|
73
|
+
export function classifyModelGroupsForSession(groups: readonly ModelGroupHeading[], activeRemoteServerId?: string): ModelGroupSide[] {
|
|
49
74
|
// Per container: did any heading carry the mark? Containers are decided
|
|
50
75
|
// independently — one mixed model menu must not turn an all-local
|
|
51
76
|
// container (a second menu elsewhere on the page) into "hide everything
|
|
@@ -55,11 +80,13 @@ export function classifyModelGroups(groups: readonly ModelGroupHeading[]): Model
|
|
|
55
80
|
const had = marked.get(group.containerKey) ?? false
|
|
56
81
|
marked.set(group.containerKey, had || group.headingText.startsWith(VIRTUAL_GROUP_NAME_MARK))
|
|
57
82
|
}
|
|
58
|
-
return groups.map((group) =>
|
|
59
|
-
marked.get(group.containerKey)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
83
|
+
return groups.map((group) => {
|
|
84
|
+
if (!marked.get(group.containerKey)) return undefined
|
|
85
|
+
if (!group.headingText.startsWith(VIRTUAL_GROUP_NAME_MARK)) return 'local'
|
|
86
|
+
if (activeRemoteServerId !== undefined) {
|
|
87
|
+
const groupServerId = decodeServerIdZeroWidth(group.headingText)
|
|
88
|
+
if (groupServerId !== undefined && groupServerId !== activeRemoteServerId) return 'remote-other'
|
|
89
|
+
}
|
|
90
|
+
return 'remote'
|
|
91
|
+
})
|
|
65
92
|
}
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
* - `parseRemoteStatusBody(body, now)` maps the
|
|
9
9
|
* `GET /_dsh/zen-remote/client/remote-status` body
|
|
10
10
|
* (`{ state, versionMismatch, serverName, closed }`) into a tolerant view;
|
|
11
|
+
* since MS-6 the body's `servers` dict (per handshake serverId) parses
|
|
12
|
+
* alongside, and `resolveServerStatusView(view, sessionId)` picks the
|
|
13
|
+
* session's OWN server entry — falling back to the top-level mirror (the
|
|
14
|
+
* FIRST pool entry) whenever the dict cannot answer;
|
|
11
15
|
* - `describeRemoteStatus(view)` maps one view to the icon's three states —
|
|
12
16
|
* online (lit) / offline (grey; `revoked` / `unpaired` count as offline —
|
|
13
17
|
* the link is not serving either way) / mismatch (yellow, only ever while
|
|
@@ -29,7 +33,7 @@
|
|
|
29
33
|
*/
|
|
30
34
|
|
|
31
35
|
import { createLatestGate } from './settings-form.ts'
|
|
32
|
-
import { isVirtual } from '../virtual-id.ts'
|
|
36
|
+
import { fromVirtual, isVirtual } from '../virtual-id.ts'
|
|
33
37
|
import { zh } from '../client/locales.ts'
|
|
34
38
|
|
|
35
39
|
/** Same-origin client route feeding this store (host half: T34,
|
|
@@ -47,6 +51,21 @@ export type RemoteStatusState = 'online' | 'offline' | 'revoked' | 'unpaired'
|
|
|
47
51
|
/** Why the server closed a session's remote access. */
|
|
48
52
|
export type RemoteClosedReason = 'manual' | 'client' | 'idle'
|
|
49
53
|
|
|
54
|
+
/**
|
|
55
|
+
* ONE paired server's serving state (MS-6): the same four facts the top
|
|
56
|
+
* level has always carried, scoped to the server whose handshake minted the
|
|
57
|
+
* `serverId`. The route (src/client-routes.ts, MS-5) serves these under
|
|
58
|
+
* `servers`, keyed by serverId; the top level mirrors the FIRST pool entry
|
|
59
|
+
* for the pre-MS-6 page.
|
|
60
|
+
*/
|
|
61
|
+
export interface RemoteServerStatusView {
|
|
62
|
+
state: RemoteStatusState
|
|
63
|
+
versionMismatch: boolean
|
|
64
|
+
serverName: string
|
|
65
|
+
/** The plugins whose bridge verdict is bridged RIGHT NOW on THIS server. */
|
|
66
|
+
bridgedPlugins: readonly string[]
|
|
67
|
+
}
|
|
68
|
+
|
|
50
69
|
/** One parsed status body, stamped with the parse time for future decay. */
|
|
51
70
|
export interface RemoteStatusView {
|
|
52
71
|
state: RemoteStatusState
|
|
@@ -58,6 +77,10 @@ export interface RemoteStatusView {
|
|
|
58
77
|
* nothing is ever considered bridged on a shape the parse does not
|
|
59
78
|
* trust. */
|
|
60
79
|
bridgedPlugins: readonly string[]
|
|
80
|
+
/** Per-server views keyed by the handshake serverId (MS-6). A legacy body
|
|
81
|
+
* without the field parses to the empty dict — the parts then fall back to
|
|
82
|
+
* the top-level mirror. */
|
|
83
|
+
servers: Readonly<Record<string, RemoteServerStatusView>>
|
|
61
84
|
/** When this view was parsed off a GET body. */
|
|
62
85
|
asOf: number
|
|
63
86
|
}
|
|
@@ -69,12 +92,26 @@ export interface RemoteStatusSnapshot {
|
|
|
69
92
|
view: RemoteStatusView | undefined
|
|
70
93
|
}
|
|
71
94
|
|
|
95
|
+
/** The tolerant per-entry parse the top level and the `servers` dict share:
|
|
96
|
+
* a garbage shape is "not online yet", a garbage field degrades to its
|
|
97
|
+
* default — never a throw into the polling loop. */
|
|
98
|
+
function parseServerStatusEntry(value: unknown): RemoteServerStatusView {
|
|
99
|
+
const record = value !== null && typeof value === 'object' ? (value as Record<string, unknown>) : {}
|
|
100
|
+
return {
|
|
101
|
+
state: record.state === 'online' || record.state === 'revoked' || record.state === 'unpaired' ? record.state : 'offline',
|
|
102
|
+
versionMismatch: record.versionMismatch === true,
|
|
103
|
+
serverName: typeof record.serverName === 'string' ? record.serverName : '',
|
|
104
|
+
bridgedPlugins: Array.isArray(record.bridgedPlugins)
|
|
105
|
+
? record.bridgedPlugins.filter((name): name is string => typeof name === 'string' && name !== '')
|
|
106
|
+
: [],
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
72
110
|
/** Parse one GET body tolerantly: a garbage shape is "not online yet", a
|
|
73
111
|
* garbage field degrades to its default — never a throw into the polling
|
|
74
112
|
* loop. */
|
|
75
113
|
export function parseRemoteStatusBody(body: unknown, now: number): RemoteStatusView {
|
|
76
114
|
const record = body !== null && typeof body === 'object' ? (body as Record<string, unknown>) : {}
|
|
77
|
-
const state = record.state === 'online' || record.state === 'revoked' || record.state === 'unpaired' ? record.state : 'offline'
|
|
78
115
|
const closed: Record<string, RemoteClosedReason> = {}
|
|
79
116
|
if (record.closed !== null && typeof record.closed === 'object') {
|
|
80
117
|
for (const [sessionId, reason] of Object.entries(record.closed as Record<string, unknown>)) {
|
|
@@ -82,14 +119,20 @@ export function parseRemoteStatusBody(body: unknown, now: number): RemoteStatusV
|
|
|
82
119
|
closed[sessionId] = reason === 'client' || reason === 'idle' ? reason : 'manual'
|
|
83
120
|
}
|
|
84
121
|
}
|
|
122
|
+
// MS-6: the per-server dict, keyed by the handshake serverId. A legacy
|
|
123
|
+
// body (or a broken one) parses to the EMPTY dict — the parts then read
|
|
124
|
+
// the top-level mirror, the pre-MS-6 answer.
|
|
125
|
+
const servers: Record<string, RemoteServerStatusView> = {}
|
|
126
|
+
if (record.servers !== null && typeof record.servers === 'object' && !Array.isArray(record.servers)) {
|
|
127
|
+
for (const [serverId, entry] of Object.entries(record.servers as Record<string, unknown>)) {
|
|
128
|
+
if (serverId === '') continue
|
|
129
|
+
servers[serverId] = parseServerStatusEntry(entry)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
85
132
|
return {
|
|
86
|
-
|
|
87
|
-
versionMismatch: record.versionMismatch === true,
|
|
88
|
-
serverName: typeof record.serverName === 'string' ? record.serverName : '',
|
|
133
|
+
...parseServerStatusEntry(record),
|
|
89
134
|
closed,
|
|
90
|
-
|
|
91
|
-
? record.bridgedPlugins.filter((name): name is string => typeof name === 'string' && name !== '')
|
|
92
|
-
: [],
|
|
135
|
+
servers,
|
|
93
136
|
asOf: now,
|
|
94
137
|
}
|
|
95
138
|
}
|
|
@@ -110,6 +153,48 @@ export function shouldStampBridgeAttribute(bridgedPlugins: unknown, plugin: stri
|
|
|
110
153
|
return Array.isArray(bridgedPlugins) && bridgedPlugins.includes(plugin)
|
|
111
154
|
}
|
|
112
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Which server's facts ONE session reads (MS-6): given the session id on
|
|
158
|
+
* screen, its virtual form names the owning serverId, and THAT server's
|
|
159
|
+
* entry answers — per-server isolation for the icon, the banner and the
|
|
160
|
+
* bridge stamp. Everything else falls back to the TOP-LEVEL view, the
|
|
161
|
+
* pre-MS-6 answer: a session id that is not virtual (a local session), a
|
|
162
|
+
* serverId the dict never answered (its entry never handshook), a legacy
|
|
163
|
+
* body without a dict, or no session id at all. Tolerant the same way as
|
|
164
|
+
* the parse: hand-rolled views without `servers` never throw here.
|
|
165
|
+
*/
|
|
166
|
+
export function resolveServerStatusView(
|
|
167
|
+
view: RemoteStatusView | undefined | null,
|
|
168
|
+
sessionId?: string,
|
|
169
|
+
): RemoteServerStatusView | undefined {
|
|
170
|
+
if (view === undefined || view === null) return undefined
|
|
171
|
+
if (sessionId !== undefined) {
|
|
172
|
+
const parts = fromVirtual(sessionId)
|
|
173
|
+
if (parts !== undefined) {
|
|
174
|
+
const server = view.servers?.[parts.serverId]
|
|
175
|
+
if (server !== undefined) return server
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// RemoteStatusView structurally carries every RemoteServerStatusView field.
|
|
179
|
+
return view
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Whether the remote session CURRENTLY ON SCREEN has its own server's bridge
|
|
184
|
+
* verdict going its way (MS-6): the verdict comes from the session's server
|
|
185
|
+
* entry ({@link resolveServerStatusView}), not the page-wide mirror of the
|
|
186
|
+
* FIRST pool entry — with several servers paired, A bridging a plugin must
|
|
187
|
+
* not un-hide it over B's sessions. Tolerant like the 2-argument core it
|
|
188
|
+
* wraps: a garbage or missing list reads as "not bridged".
|
|
189
|
+
*/
|
|
190
|
+
export function shouldStampBridgeForSession(
|
|
191
|
+
view: RemoteStatusView | undefined,
|
|
192
|
+
sessionId: string | undefined,
|
|
193
|
+
plugin: string,
|
|
194
|
+
): boolean {
|
|
195
|
+
return shouldStampBridgeAttribute(resolveServerStatusView(view, sessionId)?.bridgedPlugins, plugin)
|
|
196
|
+
}
|
|
197
|
+
|
|
113
198
|
/** The plugin the bridge stamp names. */
|
|
114
199
|
export const BRIDGE_STAMP_PLUGIN = 'dsh-better-sidebar'
|
|
115
200
|
|
|
@@ -149,12 +234,17 @@ export interface RemoteStatusDescription {
|
|
|
149
234
|
export function describeRemoteStatus(
|
|
150
235
|
view: RemoteStatusView | undefined | null,
|
|
151
236
|
t: RemoteStatusFormatter = createZhRemoteStatusFormatter(),
|
|
237
|
+
/** MS-6: the session on screen — its virtual id picks its OWN server's
|
|
238
|
+
* entry; omitted (or unanswered by the dict) keeps the pre-MS-6 top-level
|
|
239
|
+
* read. */
|
|
240
|
+
sessionId?: string,
|
|
152
241
|
): RemoteStatusDescription {
|
|
153
|
-
|
|
154
|
-
if (
|
|
155
|
-
if (
|
|
156
|
-
if (
|
|
157
|
-
|
|
242
|
+
const status = resolveServerStatusView(view, sessionId)
|
|
243
|
+
if (status === undefined) return { state: 'offline', hoverText: t('remoteStatusOffline') }
|
|
244
|
+
if (status.state === 'revoked') return { state: 'revoked', hoverText: t('remoteStatusRevoked') }
|
|
245
|
+
if (status.state === 'unpaired') return { state: 'unpaired', hoverText: t('remoteStatusUnpaired') }
|
|
246
|
+
if (status.state === 'online') {
|
|
247
|
+
return status.versionMismatch
|
|
158
248
|
? { state: 'mismatch', hoverText: t('remoteStatusMismatch') }
|
|
159
249
|
: { state: 'online', hoverText: t('remoteStatusOnline') }
|
|
160
250
|
}
|
|
@@ -190,18 +280,23 @@ export function bannerText(
|
|
|
190
280
|
sessionId: string,
|
|
191
281
|
t: RemoteBannerFormatter = (key) => zh[key],
|
|
192
282
|
): string | undefined {
|
|
283
|
+
// The closed map is SESSION-keyed (virtual ids embed their server, so one
|
|
284
|
+
// global dict cannot collide) and outranks every link-level line.
|
|
193
285
|
const reason = view?.closed[sessionId]
|
|
194
286
|
if (reason === 'idle') return t('remoteBannerClosedIdle')
|
|
195
287
|
if (reason === 'client') return t('remoteBannerClosedClient')
|
|
196
288
|
// 'manual' — and any word this build does not know (a newer server's
|
|
197
289
|
// vocabulary): a closure with no certain reason reads as the manual close.
|
|
198
290
|
if (reason !== undefined) return t('remoteBannerClosedManual')
|
|
199
|
-
|
|
200
|
-
|
|
291
|
+
// MS-6: the link-level line reads the SESSION'S OWN server, not the page's
|
|
292
|
+
// first-entry mirror.
|
|
293
|
+
const status = resolveServerStatusView(view, sessionId)
|
|
294
|
+
if (status !== undefined) {
|
|
295
|
+
if (status.state === 'offline') return t('remoteBannerOffline')
|
|
201
296
|
// T41a-fix2: a revoked token / an unpaired client is not "reconnecting"
|
|
202
297
|
// — the composer must not sit disabled on a promise no reconnect keeps.
|
|
203
|
-
if (
|
|
204
|
-
if (
|
|
298
|
+
if (status.state === 'revoked') return t('remoteBannerRevoked')
|
|
299
|
+
if (status.state === 'unpaired') return t('remoteBannerUnpaired')
|
|
205
300
|
}
|
|
206
301
|
return undefined
|
|
207
302
|
}
|
|
@@ -97,10 +97,22 @@ export const CLIENT_CLAIM_ROUTE = '/_dsh/zen-remote/client/claim'
|
|
|
97
97
|
export const CLIENT_STATUS_ROUTE = '/_dsh/zen-remote/client/status'
|
|
98
98
|
/** T43: one immediate reconnect — answered 409 unless the client is offline. */
|
|
99
99
|
export const CLIENT_RECONNECT_ROUTE = '/_dsh/zen-remote/client/reconnect'
|
|
100
|
+
/** MS-6: POST `{ entryId }` (or `{ all: true }`) removes one / all paired
|
|
101
|
+
* servers from the pool (host half: src/client-routes.ts, mirrored here
|
|
102
|
+
* because the browser half cannot import the host module). */
|
|
103
|
+
export const CLIENT_UNPAIR_ROUTE = '/_dsh/zen-remote/client/unpair'
|
|
100
104
|
/** CU-4: the redacted diagnostic report (GET, both roles, machine-local only
|
|
101
105
|
* — a gateway-forwarded fetch is refused server-side). */
|
|
102
106
|
export const CLIENT_DEBUG_REPORT_ROUTE = '/_dsh/zen-remote/client/debug-report'
|
|
103
107
|
|
|
108
|
+
/**
|
|
109
|
+
* MS-6: the paired-server pool's cap — the browser mirror of
|
|
110
|
+
* src/state-store.ts's constant of the same name (the host module cannot be
|
|
111
|
+
* imported here). The settings page and the wizard read it for the
|
|
112
|
+
* `已配对 N / 8 台` counters and the limit gating.
|
|
113
|
+
*/
|
|
114
|
+
export const MAX_PAIRED_SERVERS = 8
|
|
115
|
+
|
|
104
116
|
/** The lightweight client-facing config route (host half, both roles): the
|
|
105
117
|
* settings page's FALLBACK role probe (T17) — it registers wherever a
|
|
106
118
|
* webServer exists, and since T17 its body carries the effective `role`
|
|
@@ -169,6 +181,12 @@ export interface ClientStatusBody {
|
|
|
169
181
|
/** RD-6: the upload-redirect service probe (presence-gated — an older
|
|
170
182
|
* plugin answers without it). */
|
|
171
183
|
serviceProbe?: unknown
|
|
184
|
+
/** MS-6: the pool list, one row per paired server (`poolStatusBodyOf`'s
|
|
185
|
+
* `servers`), presence-gated like every diagnostic field — a pre-MS-5
|
|
186
|
+
* server (or the row-probe fallback) answers without it. */
|
|
187
|
+
servers?: unknown
|
|
188
|
+
/** MS-6: the pool cap as the body reports it (8). */
|
|
189
|
+
maxServers?: unknown
|
|
172
190
|
}
|
|
173
191
|
|
|
174
192
|
/** One remote-call failure line in the diagnostics lists. `time` is epoch
|
|
@@ -201,6 +219,28 @@ export interface ClientCompatView {
|
|
|
201
219
|
recentCalls: ClientDiagFailureView[]
|
|
202
220
|
}
|
|
203
221
|
|
|
222
|
+
/**
|
|
223
|
+
* MS-6: ONE paired server of the pool, as the per-server list renders it —
|
|
224
|
+
* the parsed form of one `client/status` `servers[]` row (no token ever
|
|
225
|
+
* rides any status answer). `state` speaks the same view vocabulary as the
|
|
226
|
+
* top level (the wire aliases mapped); every field degrades to its default
|
|
227
|
+
* on a junk row, and a row without an `entryId` (the route's stable key)
|
|
228
|
+
* drops whole.
|
|
229
|
+
*/
|
|
230
|
+
export interface ClientServerView {
|
|
231
|
+
entryId: string
|
|
232
|
+
serverId: string
|
|
233
|
+
serverUrl: string
|
|
234
|
+
serverName: string
|
|
235
|
+
/** The pool's deduplicated display name (`-1` / `-2` suffixes included). */
|
|
236
|
+
displayName: string
|
|
237
|
+
state: ClientConnectionView['state']
|
|
238
|
+
nextRetryAt: number | undefined
|
|
239
|
+
lastError: string
|
|
240
|
+
compat: ClientCompatView | undefined
|
|
241
|
+
pluginBridge: PluginBridgeStatusView[]
|
|
242
|
+
}
|
|
243
|
+
|
|
204
244
|
/** One client connection as the block renders it. */
|
|
205
245
|
export interface ClientConnectionView {
|
|
206
246
|
state: 'unpaired' | 'connected' | 'revoked' | 'unreachable' | 'unexpected' | 'invalid-url' | 'incompatible'
|
|
@@ -227,6 +267,15 @@ export interface ClientConnectionView {
|
|
|
227
267
|
* through tolerantly (undefined when the body carried none — an older
|
|
228
268
|
* plugin — and the diagnostics row stays hidden). */
|
|
229
269
|
uploadProbe: Readonly<Record<string, unknown>> | undefined
|
|
270
|
+
/** MS-6: every paired server of the pool, in pairing order. Empty on a
|
|
271
|
+
* legacy body (or a row-less unpaired answer) — the pre-MS-6 page shape. */
|
|
272
|
+
servers: readonly ClientServerView[]
|
|
273
|
+
/** MS-6: the pool's cap, from the body when it carried a sane number,
|
|
274
|
+
* {@link MAX_PAIRED_SERVERS} otherwise. */
|
|
275
|
+
maxServers: number
|
|
276
|
+
/** MS-6: `servers.length >= maxServers` — the red 已配满 note and the
|
|
277
|
+
* new-address submit gate both read this. */
|
|
278
|
+
limitReached: boolean
|
|
230
279
|
}
|
|
231
280
|
|
|
232
281
|
const CLIENT_STATES: readonly ClientConnectionView['state'][] = ['unpaired', 'connected', 'revoked', 'unreachable', 'unexpected', 'invalid-url', 'incompatible']
|
|
@@ -240,6 +289,42 @@ const WIRE_STATE_ALIASES: Readonly<Record<string, ClientConnectionView['state']>
|
|
|
240
289
|
offline: 'unreachable',
|
|
241
290
|
}
|
|
242
291
|
|
|
292
|
+
/** The view `state` for one raw wire word: the vocabulary verbatim, the T55
|
|
293
|
+
* aliases mapped, anything else the conservative `unpaired`. Shared by the
|
|
294
|
+
* top level and every `servers[]` row. */
|
|
295
|
+
function clientStateOf(raw: unknown): ClientConnectionView['state'] {
|
|
296
|
+
const word = typeof raw === 'string' ? raw : ''
|
|
297
|
+
return CLIENT_STATES.includes(word as ClientConnectionView['state'])
|
|
298
|
+
? word as ClientConnectionView['state']
|
|
299
|
+
: WIRE_STATE_ALIASES[word] ?? 'unpaired'
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** The finite-number gate every epoch-ms wire field passes. */
|
|
303
|
+
function finiteOrUndefined(value: unknown): number | undefined {
|
|
304
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** MS-6: one `servers[]` row into its view. Tolerant like the top level —
|
|
308
|
+
* a junk field degrades, a row without the route's stable `entryId` key
|
|
309
|
+
* drops whole. */
|
|
310
|
+
function deriveClientServerView(value: unknown): ClientServerView | undefined {
|
|
311
|
+
const record = asRecord(value)
|
|
312
|
+
const entryId = asStringSet(record.entryId)
|
|
313
|
+
if (entryId === '') return undefined
|
|
314
|
+
return {
|
|
315
|
+
entryId,
|
|
316
|
+
serverId: asStringSet(record.serverId),
|
|
317
|
+
serverUrl: asStringSet(record.serverUrl),
|
|
318
|
+
serverName: asStringSet(record.serverName),
|
|
319
|
+
displayName: asStringSet(record.displayName),
|
|
320
|
+
state: clientStateOf(record.state),
|
|
321
|
+
nextRetryAt: finiteOrUndefined(record.nextRetryAt),
|
|
322
|
+
lastError: asStringSet(record.lastError),
|
|
323
|
+
compat: deriveCompatView(record.compat),
|
|
324
|
+
pluginBridge: derivePluginBridgeStatuses(record.pluginBridge),
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
243
328
|
/** At most `cap` strings out of an array-shaped value; anything else is none. */
|
|
244
329
|
function stringListOf(value: unknown, cap: number): string[] {
|
|
245
330
|
if (!Array.isArray(value)) return []
|
|
@@ -325,18 +410,20 @@ export function deriveClientStatusView(body: ClientStatusBody): ClientConnection
|
|
|
325
410
|
// diagnostics below it showed live relay calls. `offline` rides the same
|
|
326
411
|
// mapping to the probe word for it; anything else unknown still degrades
|
|
327
412
|
// to `unpaired`.
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
: undefined
|
|
413
|
+
const state = clientStateOf(safe.state)
|
|
414
|
+
// MS-6: the pool list rides as `servers[]` (one row per paired entry); a
|
|
415
|
+
// legacy single-server body answers the empty list and the pre-MS-6 page
|
|
416
|
+
// shape otherwise.
|
|
417
|
+
const servers = (Array.isArray(safe.servers) ? safe.servers : [])
|
|
418
|
+
.map(deriveClientServerView)
|
|
419
|
+
.filter((row): row is ClientServerView => row !== undefined)
|
|
420
|
+
const maxServersRaw = finiteOrUndefined(safe.maxServers)
|
|
421
|
+
const maxServers = maxServersRaw !== undefined && maxServersRaw >= 1 ? maxServersRaw : MAX_PAIRED_SERVERS
|
|
335
422
|
return {
|
|
336
423
|
state,
|
|
337
424
|
serverUrl: typeof safe.serverUrl === 'string' ? safe.serverUrl : '',
|
|
338
425
|
serverName: typeof safe.serverName === 'string' ? safe.serverName : '',
|
|
339
|
-
nextRetryAt,
|
|
426
|
+
nextRetryAt: finiteOrUndefined(safe.nextRetryAt),
|
|
340
427
|
lastError: typeof safe.lastError === 'string' ? safe.lastError : '',
|
|
341
428
|
deviceName: typeof safe.deviceName === 'string' ? safe.deviceName : '',
|
|
342
429
|
intercept: deriveInterceptView(safe.intercept),
|
|
@@ -346,6 +433,9 @@ export function deriveClientStatusView(body: ClientStatusBody): ClientConnection
|
|
|
346
433
|
uploadProbe: safe.serviceProbe !== null && typeof safe.serviceProbe === 'object'
|
|
347
434
|
? Object.freeze({ ...(safe.serviceProbe as Record<string, unknown>) })
|
|
348
435
|
: undefined,
|
|
436
|
+
servers,
|
|
437
|
+
maxServers,
|
|
438
|
+
limitReached: servers.length >= maxServers,
|
|
349
439
|
}
|
|
350
440
|
}
|
|
351
441
|
|
|
@@ -399,6 +489,11 @@ export interface ClaimRouteBody {
|
|
|
399
489
|
token?: unknown
|
|
400
490
|
deviceId?: unknown
|
|
401
491
|
deviceName?: unknown
|
|
492
|
+
/** MS-8: the pool entry the pairing landed in (pool-mode answers carry it;
|
|
493
|
+
* the legacy single-server answer carries `token` instead and no
|
|
494
|
+
* `entryId` — the token never reaches the browser in pool mode). The
|
|
495
|
+
* wizard records it as the step-3 selected server. */
|
|
496
|
+
entryId?: unknown
|
|
402
497
|
/** The normalized address the backend validated — what the form writes. */
|
|
403
498
|
serverUrl?: unknown
|
|
404
499
|
code?: string
|
|
@@ -421,6 +516,61 @@ export function normalizePairingCode(input: string): string {
|
|
|
421
516
|
.slice(0, PAIRING_CODE_LENGTH)
|
|
422
517
|
}
|
|
423
518
|
|
|
519
|
+
/**
|
|
520
|
+
* The address comparison the overwrite judgment makes (MS-6): trim padding,
|
|
521
|
+
* drop trailing slashes. Light on purpose — the backend re-normalizes the
|
|
522
|
+
* address authoritatively on claim; here it only decides whether the typed
|
|
523
|
+
* address NAMES an already-paired server, and `http://x.local:3088/` must
|
|
524
|
+
* count as that server while `http://x.local:3089` must not.
|
|
525
|
+
*/
|
|
526
|
+
function normalizedPairAddress(url: string): string {
|
|
527
|
+
return url.trim().replace(/\/+$/u, '')
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** What {@link canSubmitClientPairing} decides. */
|
|
531
|
+
export interface CanSubmitClientPairingVerdict {
|
|
532
|
+
/** The 配对 button may act: everything locally checkable passes. */
|
|
533
|
+
allowed: boolean
|
|
534
|
+
/** The typed address names an already-paired server — the claim will
|
|
535
|
+
* UPDATE that entry's token in place, not add a ninth one. */
|
|
536
|
+
isOverwrite: boolean
|
|
537
|
+
/** A NEW address while the pool is full (`servers.length >= maxServers`):
|
|
538
|
+
* the claim must not even leave the page (the route would answer
|
|
539
|
+
* `limit-reached`). An overwrite never sets this. */
|
|
540
|
+
blockedByLimit: boolean
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* The 配对 button's whole gate, as one pure truth table (MS-6): the claim
|
|
545
|
+
* needs an address and a COMPLETE 8-character pairing code (the input box
|
|
546
|
+
* normalizes as the user types; this judge is the same bar the route's code
|
|
547
|
+
* grammar enforces), a quiet page (`busy`), and a valid device-name draft
|
|
548
|
+
* (`deviceNameInvalid`, T60 — a red-boxed name would silently pair under the
|
|
549
|
+
* fallback copy). The pool cap blocks only NEW addresses: a full pool still
|
|
550
|
+
* accepts re-pairing an existing one in place, which is how a user whose
|
|
551
|
+
* token was revoked at 8/8 gets back in.
|
|
552
|
+
*/
|
|
553
|
+
export function canSubmitClientPairing(input: {
|
|
554
|
+
pairUrl: string
|
|
555
|
+
pairCode: string
|
|
556
|
+
servers: readonly ClientServerView[]
|
|
557
|
+
maxServers?: number
|
|
558
|
+
busy?: boolean
|
|
559
|
+
deviceNameInvalid?: boolean
|
|
560
|
+
}): CanSubmitClientPairingVerdict {
|
|
561
|
+
const maxServers = input.maxServers ?? MAX_PAIRED_SERVERS
|
|
562
|
+
const address = normalizedPairAddress(input.pairUrl)
|
|
563
|
+
const isOverwrite = address !== ''
|
|
564
|
+
&& input.servers.some((server) => normalizedPairAddress(server.serverUrl) === address)
|
|
565
|
+
const blockedByLimit = !isOverwrite && input.servers.length >= maxServers
|
|
566
|
+
const allowed = address !== ''
|
|
567
|
+
&& normalizePairingCode(input.pairCode).length === PAIRING_CODE_LENGTH
|
|
568
|
+
&& !blockedByLimit
|
|
569
|
+
&& input.busy !== true
|
|
570
|
+
&& input.deviceNameInvalid !== true
|
|
571
|
+
return { allowed, isOverwrite, blockedByLimit }
|
|
572
|
+
}
|
|
573
|
+
|
|
424
574
|
/**
|
|
425
575
|
* Whether the settings page may follow the SERVER's record of this device's
|
|
426
576
|
* name into the row (T59): the body carried a name, it differs from the row
|
|
@@ -803,6 +953,8 @@ export type SettingsFieldName =
|
|
|
803
953
|
| 'pushSummary'
|
|
804
954
|
| 'pushTurnEnd'
|
|
805
955
|
| 'pushTool'
|
|
956
|
+
| 'pushEnabled'
|
|
957
|
+
| 'pushSessionName'
|
|
806
958
|
| 'pushDebounceMs'
|
|
807
959
|
| 'lang'
|
|
808
960
|
| 'serverName'
|
|
@@ -824,6 +976,8 @@ export const SETTINGS_FIELDS: readonly SettingsFieldSpec[] = [
|
|
|
824
976
|
textField('trustedProxies'),
|
|
825
977
|
intField('rateLimit', 1, Number.MAX_SAFE_INTEGER),
|
|
826
978
|
textField('vapidSubject'),
|
|
979
|
+
boolField('pushEnabled'),
|
|
980
|
+
boolField('pushSessionName'),
|
|
827
981
|
boolField('pushSummary'),
|
|
828
982
|
boolField('pushTurnEnd'),
|
|
829
983
|
boolField('pushTool'),
|
|
@@ -1042,6 +1196,8 @@ export interface ZenRemoteFormState extends SettingsFormShellState {
|
|
|
1042
1196
|
pushSummary: SettingsFieldState
|
|
1043
1197
|
pushTurnEnd: SettingsFieldState
|
|
1044
1198
|
pushTool: SettingsFieldState
|
|
1199
|
+
pushEnabled: SettingsFieldState
|
|
1200
|
+
pushSessionName: SettingsFieldState
|
|
1045
1201
|
pushDebounceMs: SettingsFieldState
|
|
1046
1202
|
lang: SettingsFieldState
|
|
1047
1203
|
serverName: SettingsFieldState
|