dsh-zen-remote 2.0.3 → 2.2.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/AGENTS.md +15 -1
- package/README.md +11 -5
- package/README.zh-CN.md +11 -5
- package/docs/adr/0001-plugin-bridge-forwards-plugin-routes.md +15 -0
- package/docs/adr/0002-relay-emulates-websocket.md +8 -0
- package/docs/interface.md +3 -1
- package/docs/plugin-bridge.md +140 -0
- package/docs/remote-access.en.md +57 -0
- package/docs/remote-access.md +57 -0
- package/lib/activity.js +25 -0
- package/lib/activity.js.map +1 -1
- package/lib/adapters/dsh-better-sidebar/index.js +144 -0
- package/lib/adapters/dsh-better-sidebar/index.js.map +1 -0
- package/lib/adapters/dsh-better-sidebar/methods.js +221 -0
- package/lib/adapters/dsh-better-sidebar/methods.js.map +1 -0
- package/lib/adapters/index.js +17 -0
- package/lib/adapters/index.js.map +1 -0
- package/lib/adapters/types.js +17 -0
- package/lib/adapters/types.js.map +1 -0
- package/lib/admin-routes.js +51 -0
- package/lib/admin-routes.js.map +1 -1
- package/lib/client-routes.js +18 -1
- package/lib/client-routes.js.map +1 -1
- package/lib/client.js +449 -45
- package/lib/config.js +27 -0
- package/lib/config.js.map +1 -1
- package/lib/http.js +10 -0
- package/lib/http.js.map +1 -1
- package/lib/index.js +273 -1
- package/lib/index.js.map +1 -1
- package/lib/plugin-adapter-core.js +385 -0
- package/lib/plugin-adapter-core.js.map +1 -0
- package/lib/plugin-bridge.js +318 -0
- package/lib/plugin-bridge.js.map +1 -0
- package/lib/plugin-loopback.js +207 -0
- package/lib/plugin-loopback.js.map +1 -0
- package/lib/plugin-route-intercept.js +1103 -0
- package/lib/plugin-route-intercept.js.map +1 -0
- package/lib/plugin-tunnel.js +416 -0
- package/lib/plugin-tunnel.js.map +1 -0
- package/lib/relay-client.js +319 -2
- package/lib/relay-client.js.map +1 -1
- package/lib/relay-server.js +633 -0
- package/lib/relay-server.js.map +1 -1
- package/lib/types/activity.d.ts +22 -0
- package/lib/types/activity.d.ts.map +1 -1
- package/lib/types/adapters/dsh-better-sidebar/index.d.ts +27 -0
- package/lib/types/adapters/dsh-better-sidebar/index.d.ts.map +1 -0
- package/lib/types/adapters/dsh-better-sidebar/methods.d.ts +58 -0
- package/lib/types/adapters/dsh-better-sidebar/methods.d.ts.map +1 -0
- package/lib/types/adapters/index.d.ts +17 -0
- package/lib/types/adapters/index.d.ts.map +1 -0
- package/lib/types/adapters/types.d.ts +42 -0
- package/lib/types/adapters/types.d.ts.map +1 -0
- package/lib/types/admin-routes.d.ts +16 -0
- package/lib/types/admin-routes.d.ts.map +1 -1
- package/lib/types/client/effects/remote-session.d.ts +0 -22
- package/lib/types/client/effects/remote-session.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +16 -0
- 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/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-data/plugin-bridge.d.ts +79 -0
- package/lib/types/client-data/plugin-bridge.d.ts.map +1 -0
- package/lib/types/client-data/remote-status.d.ts +20 -0
- package/lib/types/client-data/remote-status.d.ts.map +1 -1
- package/lib/types/client-data/settings-form.d.ts +37 -1
- package/lib/types/client-data/settings-form.d.ts.map +1 -1
- package/lib/types/client-routes.d.ts +13 -0
- package/lib/types/client-routes.d.ts.map +1 -1
- package/lib/types/config.d.ts +8 -0
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/http.d.ts +7 -0
- package/lib/types/http.d.ts.map +1 -1
- package/lib/types/index.d.ts +4 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/plugin-adapter-core.d.ts +263 -0
- package/lib/types/plugin-adapter-core.d.ts.map +1 -0
- package/lib/types/plugin-bridge.d.ts +187 -0
- package/lib/types/plugin-bridge.d.ts.map +1 -0
- package/lib/types/plugin-loopback.d.ts +104 -0
- package/lib/types/plugin-loopback.d.ts.map +1 -0
- package/lib/types/plugin-route-intercept.d.ts +114 -0
- package/lib/types/plugin-route-intercept.d.ts.map +1 -0
- package/lib/types/plugin-tunnel.d.ts +96 -0
- package/lib/types/plugin-tunnel.d.ts.map +1 -0
- package/lib/types/relay-client.d.ts +76 -0
- package/lib/types/relay-client.d.ts.map +1 -1
- package/lib/types/relay-server.d.ts +32 -1
- package/lib/types/relay-server.d.ts.map +1 -1
- package/llms.txt +1 -0
- package/package.json +2 -2
- package/scripts/check-plugin-bridge-ui.mjs +168 -0
- package/scripts/check-settings-form.mjs +3 -1
- package/src/activity.ts +32 -0
- package/src/adapters/dsh-better-sidebar/index.ts +144 -0
- package/src/adapters/dsh-better-sidebar/methods.ts +244 -0
- package/src/adapters/index.ts +19 -0
- package/src/adapters/types.ts +44 -0
- package/src/admin-routes.ts +64 -0
- package/src/client/effects/remote-session.ts +82 -10
- package/src/client/locales.ts +36 -0
- package/src/client/settings/SettingsSection.tsx +107 -3
- package/src/client/styles/remote-session.css.ts +70 -31
- package/src/client-data/plugin-bridge.ts +150 -0
- package/src/client-data/remote-status.ts +30 -0
- package/src/client-data/settings-form.ts +78 -0
- package/src/client-routes.ts +29 -1
- package/src/config.ts +32 -0
- package/src/http.ts +12 -0
- package/src/index.ts +279 -1
- package/src/plugin-adapter-core.ts +530 -0
- package/src/plugin-bridge.ts +435 -0
- package/src/plugin-loopback.ts +299 -0
- package/src/plugin-route-intercept.ts +1193 -0
- package/src/plugin-tunnel.ts +470 -0
- package/src/relay-client.ts +394 -2
- package/src/relay-server.ts +656 -1
|
@@ -51,6 +51,11 @@ export interface RemoteStatusView {
|
|
|
51
51
|
versionMismatch: boolean
|
|
52
52
|
serverName: string
|
|
53
53
|
closed: Readonly<Record<string, RemoteClosedReason>>
|
|
54
|
+
/** The plugins whose bridge verdict is bridged RIGHT NOW (PB-2; sorted
|
|
55
|
+
* server-side). A garbage or missing field degrades to the empty list —
|
|
56
|
+
* nothing is ever considered bridged on a shape the parse does not
|
|
57
|
+
* trust. */
|
|
58
|
+
bridgedPlugins: readonly string[]
|
|
54
59
|
/** When this view was parsed off a GET body. */
|
|
55
60
|
asOf: number
|
|
56
61
|
}
|
|
@@ -80,10 +85,35 @@ export function parseRemoteStatusBody(body: unknown, now: number): RemoteStatusV
|
|
|
80
85
|
versionMismatch: record.versionMismatch === true,
|
|
81
86
|
serverName: typeof record.serverName === 'string' ? record.serverName : '',
|
|
82
87
|
closed,
|
|
88
|
+
bridgedPlugins: Array.isArray(record.bridgedPlugins)
|
|
89
|
+
? record.bridgedPlugins.filter((name): name is string => typeof name === 'string' && name !== '')
|
|
90
|
+
: [],
|
|
83
91
|
asOf: now,
|
|
84
92
|
}
|
|
85
93
|
}
|
|
86
94
|
|
|
95
|
+
// --- the bridge stamp (PB-14) -----------------------------------------------
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Whether ONE plugin's bridge verdict is bridged RIGHT NOW, judged off the
|
|
99
|
+
* `bridgedPlugins` list the remote-status body carries. Tolerant by the
|
|
100
|
+
* same discipline as the parse: a garbage list (an older server that never
|
|
101
|
+
* learned the field, a broken body) reads as "not bridged" — the hiding
|
|
102
|
+
* rules stay in force unless the server positively says otherwise, never
|
|
103
|
+
* the reverse. This is the pure core behind the
|
|
104
|
+
* `html[data-zr-bridge-<plugin>]` stamp the remote-session stylesheet
|
|
105
|
+
* spends.
|
|
106
|
+
*/
|
|
107
|
+
export function shouldStampBridgeAttribute(bridgedPlugins: unknown, plugin: string): boolean {
|
|
108
|
+
return Array.isArray(bridgedPlugins) && bridgedPlugins.includes(plugin)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The plugin the bridge stamp names. */
|
|
112
|
+
export const BRIDGE_STAMP_PLUGIN = 'dsh-better-sidebar'
|
|
113
|
+
|
|
114
|
+
/** The html attribute the stamp writes (`data-zr-bridge-dsh-better-sidebar`). */
|
|
115
|
+
export const BRIDGE_STAMP_ATTRIBUTE = `data-zr-bridge-${BRIDGE_STAMP_PLUGIN}`
|
|
116
|
+
|
|
87
117
|
// --- the icon's states -------------------------------------------------------
|
|
88
118
|
|
|
89
119
|
/** The locale keys `describeRemoteStatus` needs. */
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
import { RESTART_FIELDS } from '../restart-fields.ts'
|
|
29
|
+
import { derivePluginBridgeStatuses, type PluginBridgeStatusView } from './plugin-bridge.ts'
|
|
29
30
|
|
|
30
31
|
// --- wire shapes -----------------------------------------------------------
|
|
31
32
|
|
|
@@ -88,6 +89,8 @@ export const ADMIN_STATUS_ROUTE = '/_dsh/zen-remote/admin/status'
|
|
|
88
89
|
export const ADMIN_PAIR_ROUTE = '/_dsh/zen-remote/admin/pair'
|
|
89
90
|
export const ADMIN_ACTION_ROUTE = '/_dsh/zen-remote/admin/action'
|
|
90
91
|
export const ADMIN_PUSH_TEST_ROUTE = '/_dsh/zen-remote/admin/push-test'
|
|
92
|
+
/** PB-5: the plugin bridge's registered-plugin listing (GET, server-local only). */
|
|
93
|
+
export const ADMIN_PLUGIN_BRIDGE_ROUTE = '/_dsh/zen-remote/admin/plugin-bridge'
|
|
91
94
|
|
|
92
95
|
/** Same-origin client routes the sub-client block talks to (host half: T16). */
|
|
93
96
|
export const CLIENT_CLAIM_ROUTE = '/_dsh/zen-remote/client/claim'
|
|
@@ -153,6 +156,13 @@ export interface ClientStatusBody {
|
|
|
153
156
|
* string[], unavailable: string[], incompatibleCalls: { time: number,
|
|
154
157
|
* endpoint: string, code: string }[] }` (presence-gated). */
|
|
155
158
|
compat?: unknown
|
|
159
|
+
/** PB-2: the per-plugin bridge verdicts (`BridgeStatus[]` verbatim,
|
|
160
|
+
* presence-gated — older servers answer without it). */
|
|
161
|
+
pluginBridge?: unknown
|
|
162
|
+
/** PB-4: the plugin-route interception's diagnostics (presence-gated —
|
|
163
|
+
* a host role or a refused install may still answer it; a server without
|
|
164
|
+
* the field never does). */
|
|
165
|
+
pluginRouteIntercept?: unknown
|
|
156
166
|
}
|
|
157
167
|
|
|
158
168
|
/** One remote-call failure line in the diagnostics lists. `time` is epoch
|
|
@@ -200,6 +210,13 @@ export interface ClientConnectionView {
|
|
|
200
210
|
deviceName: string
|
|
201
211
|
intercept: ClientInterceptView | undefined
|
|
202
212
|
compat: ClientCompatView | undefined
|
|
213
|
+
/** PB-5: the bridge verdicts as the 诊断区 renders them, input order kept
|
|
214
|
+
* (undefined when the body carried none — the group stays hidden). */
|
|
215
|
+
pluginBridge: PluginBridgeStatusView[] | undefined
|
|
216
|
+
/** PB-5: the route interception's install state (`installed: false`
|
|
217
|
+
* renders the 本机改道未启用 line with the shape reasons; undefined when
|
|
218
|
+
* the body carried none). */
|
|
219
|
+
pluginRouteIntercept: { installed: boolean; reasons: string[] } | undefined
|
|
203
220
|
}
|
|
204
221
|
|
|
205
222
|
const CLIENT_STATES: readonly ClientConnectionView['state'][] = ['unpaired', 'connected', 'revoked', 'unreachable', 'unexpected', 'invalid-url', 'incompatible']
|
|
@@ -258,6 +275,18 @@ function deriveInterceptView(value: unknown): ClientInterceptView | undefined {
|
|
|
258
275
|
}
|
|
259
276
|
}
|
|
260
277
|
|
|
278
|
+
/** PB-4 route-interception block, presence-gated the same way: only the
|
|
279
|
+
* install flag and the shape reasons (the 本机改道未启用 line) — the
|
|
280
|
+
* counters belong to the status surface, not this copy. */
|
|
281
|
+
function derivePluginRouteInterceptView(value: unknown): { installed: boolean; reasons: string[] } | undefined {
|
|
282
|
+
if (value === undefined || value === null) return undefined
|
|
283
|
+
const record = asRecord(value)
|
|
284
|
+
return {
|
|
285
|
+
installed: record.installed === true,
|
|
286
|
+
reasons: stringListOf(record.reasons, 20),
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
261
290
|
/** T42 compat block, only when the body carried the field at all. The wire
|
|
262
291
|
* names are the status route's (`different` / `incompatibleCalls`); the view
|
|
263
292
|
* keeps the render-side names the block was written against. */
|
|
@@ -302,6 +331,8 @@ export function deriveClientStatusView(body: ClientStatusBody): ClientConnection
|
|
|
302
331
|
deviceName: typeof safe.deviceName === 'string' ? safe.deviceName : '',
|
|
303
332
|
intercept: deriveInterceptView(safe.intercept),
|
|
304
333
|
compat: deriveCompatView(safe.compat),
|
|
334
|
+
pluginBridge: derivePluginBridgeStatuses(safe.pluginBridge),
|
|
335
|
+
pluginRouteIntercept: derivePluginRouteInterceptView(safe.pluginRouteIntercept),
|
|
305
336
|
}
|
|
306
337
|
}
|
|
307
338
|
|
|
@@ -641,6 +672,47 @@ export function boolField(field: string): SettingsFieldSpec {
|
|
|
641
672
|
}
|
|
642
673
|
}
|
|
643
674
|
|
|
675
|
+
/**
|
|
676
|
+
* `pluginBridgeConsent` (PB-5): a name → boolean map staged as its canonical
|
|
677
|
+
* JSON draft. Nothing renders it as text — the 插件桥 toggles stage the NEXT
|
|
678
|
+
* map wholesale (`togglePluginConsent` builds it, one key per click) — but
|
|
679
|
+
* riding the shared spec table is what puts the field on the SAME save path
|
|
680
|
+
* as `autoShareNewSessions`: one staged revision, one save button, one
|
|
681
|
+
* atomic mutation. `format` keeps the value's own key order so a staged
|
|
682
|
+
* draft equal to the displayed value reads as "no change"; an illegal stored
|
|
683
|
+
* map formats as `{}` (the toggle then writes from scratch, and the junk
|
|
684
|
+
* stays refused by the row validator, never laundered through).
|
|
685
|
+
*/
|
|
686
|
+
export function consentField(field: string): SettingsFieldSpec {
|
|
687
|
+
const format = (value: unknown): string => {
|
|
688
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return '{}'
|
|
689
|
+
const out: Record<string, boolean> = {}
|
|
690
|
+
for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
|
|
691
|
+
if (typeof entry === 'boolean') out[key] = entry
|
|
692
|
+
}
|
|
693
|
+
return JSON.stringify(out)
|
|
694
|
+
}
|
|
695
|
+
return {
|
|
696
|
+
field,
|
|
697
|
+
format,
|
|
698
|
+
parse: (text) => {
|
|
699
|
+
let parsed: unknown
|
|
700
|
+
try {
|
|
701
|
+
parsed = JSON.parse(text)
|
|
702
|
+
} catch {
|
|
703
|
+
return undefined
|
|
704
|
+
}
|
|
705
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return undefined
|
|
706
|
+
const out: Record<string, boolean> = {}
|
|
707
|
+
for (const [key, entry] of Object.entries(parsed as Record<string, unknown>)) {
|
|
708
|
+
if (typeof entry !== 'boolean') return undefined
|
|
709
|
+
out[key] = entry
|
|
710
|
+
}
|
|
711
|
+
return { kind: 'set', value: out }
|
|
712
|
+
},
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
644
716
|
/** Names of the row fields the settings page edits, in group order. */
|
|
645
717
|
export type SettingsFieldName =
|
|
646
718
|
| 'role'
|
|
@@ -657,6 +729,7 @@ export type SettingsFieldName =
|
|
|
657
729
|
| 'serverName'
|
|
658
730
|
| 'idleHours'
|
|
659
731
|
| 'autoShareNewSessions'
|
|
732
|
+
| 'pluginBridgeConsent'
|
|
660
733
|
|
|
661
734
|
/** Longest legal `serverName`, mirroring src/config.ts's cap. */
|
|
662
735
|
export const SERVER_NAME_MAX = 40
|
|
@@ -680,6 +753,7 @@ export const SETTINGS_FIELDS: readonly SettingsFieldSpec[] = [
|
|
|
680
753
|
nameField('serverName', SERVER_NAME_MAX),
|
|
681
754
|
hoursField('idleHours', IDLE_HOURS_MAX),
|
|
682
755
|
boolField('autoShareNewSessions'),
|
|
756
|
+
consentField('pluginBridgeConsent'),
|
|
683
757
|
]
|
|
684
758
|
|
|
685
759
|
// --- the status -> view mapping ---------------------------------------------
|
|
@@ -894,6 +968,10 @@ export interface ZenRemoteFormState extends SettingsFormShellState {
|
|
|
894
968
|
serverName: SettingsFieldState
|
|
895
969
|
idleHours: SettingsFieldState
|
|
896
970
|
autoShareNewSessions: SettingsFieldState
|
|
971
|
+
/** The consent map's staged state (PB-5). No text control renders it — the
|
|
972
|
+
* 插件桥 toggles stage the next map wholesale — but its `invalid` flag
|
|
973
|
+
* (a malformed draft) still gates the save like any field's. */
|
|
974
|
+
pluginBridgeConsent: SettingsFieldState
|
|
897
975
|
deviceToken: SettingsSecretFieldState
|
|
898
976
|
/** A restart-required field ({@link RESTART_FIELDS}) has a staged change a
|
|
899
977
|
* save would actually write: the save will move the row and the host
|
package/src/client-routes.ts
CHANGED
|
@@ -31,7 +31,9 @@ import type { ClaimOutcome, ProbeState } from './client-pairing.js'
|
|
|
31
31
|
import { unwrapVolatile } from './config.js'
|
|
32
32
|
import type { FetchRouteInterceptDiagnostics } from './fetch-route-intercept.js'
|
|
33
33
|
import type { CurrentSessionRead, InterceptDiagnostics } from './intercept.js'
|
|
34
|
+
import type { PluginRouteInterceptDiagnostics } from './plugin-route-intercept.js'
|
|
34
35
|
import { responseJson, sameOriginPost } from './http.js'
|
|
36
|
+
import type { BridgeStatus } from './plugin-bridge.js'
|
|
35
37
|
import { RELAY_HTTP_ROUTES } from './relay-access.js'
|
|
36
38
|
import { relayCredentialsDigest, RelayError } from './relay-client.js'
|
|
37
39
|
import type { RelayClient, RelayHttpResult } from './relay-client.js'
|
|
@@ -118,6 +120,17 @@ export interface ClientHandlerOptions {
|
|
|
118
120
|
* its refusal counters. Undefined when this composition never attempted
|
|
119
121
|
* one (no connection service, or no fileUploads service to inject). */
|
|
120
122
|
getFetchRouteIntercept?: () => FetchRouteInterceptDiagnostics | undefined
|
|
123
|
+
/** The plugin-route interception's diagnostics (PB-4), read live per
|
|
124
|
+
* request: the local webServer `match` wrap that forwards bridged-plugin
|
|
125
|
+
* requests for remote sessions. Undefined when this composition never
|
|
126
|
+
* attempted one (a host role, or no webServer). */
|
|
127
|
+
getPluginRouteIntercept?: () => PluginRouteInterceptDiagnostics | undefined
|
|
128
|
+
/** The plugin bridge's live verdict (PB-2), computed PER CALL off the local
|
|
129
|
+
* registry, the last handshake's `plugins` table and the connection state —
|
|
130
|
+
* never a stored conclusion. Wired only by the CLIENT role's mount; absent
|
|
131
|
+
* (the host mount, old tests) both routes answer the empty shape: status's
|
|
132
|
+
* `pluginBridge: []` and remote-status's `bridgedPlugins: []`. */
|
|
133
|
+
getPluginBridgeStatus?: () => BridgeStatus[]
|
|
121
134
|
/** Serve ONLY the remote-status route (T41a-fix2, the host role's mount):
|
|
122
135
|
* every other route under the prefix answers the same 404 an unknown path
|
|
123
136
|
* would. The claim / status / reconnect / unshare routes exist for a
|
|
@@ -456,6 +469,7 @@ export function createClientHandler(options: ClientHandlerOptions): ClientHandle
|
|
|
456
469
|
// that.
|
|
457
470
|
const intercept = options.getIntercept?.()
|
|
458
471
|
const fetchRouteIntercept = options.getFetchRouteIntercept?.()
|
|
472
|
+
const pluginRouteIntercept = options.getPluginRouteIntercept?.()
|
|
459
473
|
const compatRelay = options.getRelayClient?.()
|
|
460
474
|
const compat =
|
|
461
475
|
compatRelay?.compat !== undefined || (intercept?.incompatibleCalls.length ?? 0) > 0
|
|
@@ -467,8 +481,13 @@ export function createClientHandler(options: ClientHandlerOptions): ClientHandle
|
|
|
467
481
|
}
|
|
468
482
|
: undefined
|
|
469
483
|
const withDiagnostics = (body: Record<string, unknown>): Record<string, unknown> => {
|
|
470
|
-
|
|
484
|
+
// PB-2: the per-plugin bridge verdict rides EVERY status answer —
|
|
485
|
+
// the settings surface explains a not-bridged plugin whatever the
|
|
486
|
+
// connection state (unpaired reads all-offline, the honest verdict).
|
|
487
|
+
let next: Record<string, unknown> = { ...body, pluginBridge: options.getPluginBridgeStatus?.() ?? [] }
|
|
488
|
+
if (intercept !== undefined) next = { ...next, intercept }
|
|
471
489
|
if (fetchRouteIntercept !== undefined) next = { ...next, fetchRouteIntercept }
|
|
490
|
+
if (pluginRouteIntercept !== undefined) next = { ...next, pluginRouteIntercept }
|
|
472
491
|
return compat === undefined ? next : { ...next, compat }
|
|
473
492
|
}
|
|
474
493
|
// The ROW decides its own two shapes first, per request (both fields
|
|
@@ -571,6 +590,11 @@ export function createClientHandler(options: ClientHandlerOptions): ClientHandle
|
|
|
571
590
|
// T34: the T34 client parts' data source — no token, no server
|
|
572
591
|
// address, nothing but the serving state, the compat verdict's
|
|
573
592
|
// difference flag, the display name and the closed-session map.
|
|
593
|
+
// PB-2 adds `bridgedPlugins`: the names whose bridge verdict is
|
|
594
|
+
// bridged RIGHT NOW, sorted (the list arrives plugin-sorted, but the
|
|
595
|
+
// sort is pinned here so the field's shape never depends on the
|
|
596
|
+
// registry's internal order). The host mount — no verdict source —
|
|
597
|
+
// answers [].
|
|
574
598
|
const relay = options.getRelayClient?.()
|
|
575
599
|
const intercept = options.getIntercept?.()
|
|
576
600
|
responseJson(res, 200, {
|
|
@@ -578,6 +602,10 @@ export function createClientHandler(options: ClientHandlerOptions): ClientHandle
|
|
|
578
602
|
versionMismatch: (relay?.compat?.different?.length ?? 0) > 0,
|
|
579
603
|
serverName: relay?.handshakeInfo?.serverName ?? '',
|
|
580
604
|
closed: Object.fromEntries((intercept?.closedSessions ?? []).map((record) => [record.sessionId, record.reason])),
|
|
605
|
+
bridgedPlugins: (options.getPluginBridgeStatus?.() ?? [])
|
|
606
|
+
.filter((status) => status.bridged)
|
|
607
|
+
.map((status) => status.plugin)
|
|
608
|
+
.sort(),
|
|
581
609
|
})
|
|
582
610
|
return
|
|
583
611
|
}
|
package/src/config.ts
CHANGED
|
@@ -77,6 +77,12 @@ export interface ZenRemoteConfig {
|
|
|
77
77
|
idleHours: number
|
|
78
78
|
/** Auto-share every newly created session to the paired phone (T16). */
|
|
79
79
|
autoShareNewSessions: boolean
|
|
80
|
+
/** Per-plugin consent for bridging whole-machine data (plugin bridge,
|
|
81
|
+
* PB-1): plugin name → whether the server owner lets that plugin's
|
|
82
|
+
* `scope:'server'` routes be forwarded inside remote sessions. Absent or
|
|
83
|
+
* false means NO — default `{}` keeps every plugin unconsented. Volatile:
|
|
84
|
+
* read at compare time, deliberately NOT in RESTART_FIELDS. */
|
|
85
|
+
pluginBridgeConsent: Record<string, boolean>
|
|
80
86
|
}
|
|
81
87
|
|
|
82
88
|
function defaultServerName(): string {
|
|
@@ -101,6 +107,7 @@ export const DEFAULTS: ZenRemoteConfig = {
|
|
|
101
107
|
serverName: defaultServerName(),
|
|
102
108
|
idleHours: 48,
|
|
103
109
|
autoShareNewSessions: false,
|
|
110
|
+
pluginBridgeConsent: {},
|
|
104
111
|
}
|
|
105
112
|
|
|
106
113
|
// --- per-field validators -------------------------------------------------
|
|
@@ -176,6 +183,22 @@ function serverNameOf(value: unknown): string | undefined {
|
|
|
176
183
|
return name === undefined ? undefined : name.slice(0, SERVER_NAME_MAX)
|
|
177
184
|
}
|
|
178
185
|
|
|
186
|
+
/** `pluginBridgeConsent`: a plain object mapping plugin names to STRICT
|
|
187
|
+
* booleans. Anything else — null, an array, a non-boolean value (`"yes"`,
|
|
188
|
+
* `1`; unlike the scalar booleans there is no historic `1`/`"1"` convention
|
|
189
|
+
* to honor, this map only ever came from the settings surface or a
|
|
190
|
+
* hand-edited row/file) — makes the layer transparent, because a half-valid
|
|
191
|
+
* consent map must never read as "consented". Object keys are strings by
|
|
192
|
+
* construction in both JSON and YAML; the per-key string check stays as the
|
|
193
|
+
* cheap belt over the iteration. */
|
|
194
|
+
function consentMap(value: unknown): Record<string, boolean> | undefined {
|
|
195
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return undefined
|
|
196
|
+
for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
|
|
197
|
+
if (typeof key !== 'string' || typeof entry !== 'boolean') return undefined
|
|
198
|
+
}
|
|
199
|
+
return value as Record<string, boolean>
|
|
200
|
+
}
|
|
201
|
+
|
|
179
202
|
/** Env validator applied to `Number(env[name])`, so `LAN_GATE_PORT='abc'`
|
|
180
203
|
* lands in the same "unset" bucket as a bad row value. */
|
|
181
204
|
function envNumber(
|
|
@@ -263,6 +286,10 @@ const FIELDS: { [K in keyof ZenRemoteConfig]: FieldResolver<ZenRemoteConfig[K]>
|
|
|
263
286
|
serverName: { row: serverNameOf, file: serverNameOf },
|
|
264
287
|
idleHours: { row: inRange(0, 8760), file: inRange(0, 8760) },
|
|
265
288
|
autoShareNewSessions: { row: boolLike, file: boolLike },
|
|
289
|
+
// No env layer on purpose (PB-1): a per-plugin consent map has no historic
|
|
290
|
+
// flat variable to stay compatible with, and a consent decision is a
|
|
291
|
+
// settings-page action, not something set through the environment.
|
|
292
|
+
pluginBridgeConsent: { row: consentMap, file: consentMap },
|
|
266
293
|
}
|
|
267
294
|
|
|
268
295
|
/**
|
|
@@ -346,6 +373,7 @@ export function resolveConfig(
|
|
|
346
373
|
serverName: resolveField('serverName', safeRow, safeFile, env, sources),
|
|
347
374
|
idleHours: resolveField('idleHours', safeRow, safeFile, env, sources),
|
|
348
375
|
autoShareNewSessions: resolveField('autoShareNewSessions', safeRow, safeFile, env, sources),
|
|
376
|
+
pluginBridgeConsent: resolveField('pluginBridgeConsent', safeRow, safeFile, env, sources),
|
|
349
377
|
}
|
|
350
378
|
return { values, sources }
|
|
351
379
|
}
|
|
@@ -432,6 +460,10 @@ export const Config = z.object({
|
|
|
432
460
|
serverName: z.any().volatile(),
|
|
433
461
|
idleHours: z.any().volatile(),
|
|
434
462
|
autoShareNewSessions: z.any().volatile(),
|
|
463
|
+
// Plugin-bridge consent (PB-1): a name → boolean map, live-read at compare
|
|
464
|
+
// time — a consent flip must apply without a row restart, so it is
|
|
465
|
+
// deliberately NOT in RESTART_FIELDS.
|
|
466
|
+
pluginBridgeConsent: z.any().volatile(),
|
|
435
467
|
// Client-half knobs (T16): where the phone finds the gateway, and the
|
|
436
468
|
// pairing token it presents. `role('secret')` keeps the token out of every
|
|
437
469
|
// settings wire surface, like the verifier plugin's API key.
|
package/src/http.ts
CHANGED
|
@@ -66,3 +66,15 @@ export function sameOriginPost(req: IncomingMessage): boolean {
|
|
|
66
66
|
return false
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* One plugin-bridge refusal (PB-3): the standard JSON envelope plus the
|
|
73
|
+
* `x-zen-remote-error` marker header — the sub-client's passthrough layer
|
|
74
|
+
* classifies a refusal off the header alone, because the success path
|
|
75
|
+
* streams opaque plugin bytes and must never have to parse a body.
|
|
76
|
+
*/
|
|
77
|
+
export function pluginBridgeError(res: ServerResponse, status: number, code: string): void {
|
|
78
|
+
res.setHeader('x-zen-remote-error', code)
|
|
79
|
+
responseJson(res, status, { ok: false, error: { code } })
|
|
80
|
+
}
|