dsh-zen-remote 2.0.3 → 2.2.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 (114) hide show
  1. package/AGENTS.md +15 -1
  2. package/README.md +11 -5
  3. package/README.zh-CN.md +11 -5
  4. package/docs/adr/0001-plugin-bridge-forwards-plugin-routes.md +15 -0
  5. package/docs/adr/0002-relay-emulates-websocket.md +8 -0
  6. package/docs/interface.md +3 -1
  7. package/docs/plugin-bridge.md +140 -0
  8. package/docs/remote-access.en.md +57 -0
  9. package/docs/remote-access.md +57 -0
  10. package/lib/adapters/dsh-better-sidebar/index.js +144 -0
  11. package/lib/adapters/dsh-better-sidebar/index.js.map +1 -0
  12. package/lib/adapters/dsh-better-sidebar/methods.js +198 -0
  13. package/lib/adapters/dsh-better-sidebar/methods.js.map +1 -0
  14. package/lib/adapters/index.js +17 -0
  15. package/lib/adapters/index.js.map +1 -0
  16. package/lib/adapters/types.js +17 -0
  17. package/lib/adapters/types.js.map +1 -0
  18. package/lib/admin-routes.js +51 -0
  19. package/lib/admin-routes.js.map +1 -1
  20. package/lib/client-routes.js +18 -1
  21. package/lib/client-routes.js.map +1 -1
  22. package/lib/client.js +449 -45
  23. package/lib/config.js +27 -0
  24. package/lib/config.js.map +1 -1
  25. package/lib/http.js +10 -0
  26. package/lib/http.js.map +1 -1
  27. package/lib/index.js +246 -0
  28. package/lib/index.js.map +1 -1
  29. package/lib/plugin-adapter-core.js +385 -0
  30. package/lib/plugin-adapter-core.js.map +1 -0
  31. package/lib/plugin-bridge.js +318 -0
  32. package/lib/plugin-bridge.js.map +1 -0
  33. package/lib/plugin-loopback.js +207 -0
  34. package/lib/plugin-loopback.js.map +1 -0
  35. package/lib/plugin-route-intercept.js +1103 -0
  36. package/lib/plugin-route-intercept.js.map +1 -0
  37. package/lib/plugin-tunnel.js +416 -0
  38. package/lib/plugin-tunnel.js.map +1 -0
  39. package/lib/relay-client.js +319 -2
  40. package/lib/relay-client.js.map +1 -1
  41. package/lib/relay-server.js +633 -0
  42. package/lib/relay-server.js.map +1 -1
  43. package/lib/types/adapters/dsh-better-sidebar/index.d.ts +27 -0
  44. package/lib/types/adapters/dsh-better-sidebar/index.d.ts.map +1 -0
  45. package/lib/types/adapters/dsh-better-sidebar/methods.d.ts +51 -0
  46. package/lib/types/adapters/dsh-better-sidebar/methods.d.ts.map +1 -0
  47. package/lib/types/adapters/index.d.ts +17 -0
  48. package/lib/types/adapters/index.d.ts.map +1 -0
  49. package/lib/types/adapters/types.d.ts +42 -0
  50. package/lib/types/adapters/types.d.ts.map +1 -0
  51. package/lib/types/admin-routes.d.ts +16 -0
  52. package/lib/types/admin-routes.d.ts.map +1 -1
  53. package/lib/types/client/effects/remote-session.d.ts +0 -22
  54. package/lib/types/client/effects/remote-session.d.ts.map +1 -1
  55. package/lib/types/client/locales.d.ts +16 -0
  56. package/lib/types/client/locales.d.ts.map +1 -1
  57. package/lib/types/client/settings/SettingsSection.d.ts.map +1 -1
  58. package/lib/types/client/styles/remote-session.css.d.ts +1 -1
  59. package/lib/types/client/styles/remote-session.css.d.ts.map +1 -1
  60. package/lib/types/client-data/plugin-bridge.d.ts +79 -0
  61. package/lib/types/client-data/plugin-bridge.d.ts.map +1 -0
  62. package/lib/types/client-data/remote-status.d.ts +20 -0
  63. package/lib/types/client-data/remote-status.d.ts.map +1 -1
  64. package/lib/types/client-data/settings-form.d.ts +37 -1
  65. package/lib/types/client-data/settings-form.d.ts.map +1 -1
  66. package/lib/types/client-routes.d.ts +13 -0
  67. package/lib/types/client-routes.d.ts.map +1 -1
  68. package/lib/types/config.d.ts +8 -0
  69. package/lib/types/config.d.ts.map +1 -1
  70. package/lib/types/http.d.ts +7 -0
  71. package/lib/types/http.d.ts.map +1 -1
  72. package/lib/types/index.d.ts +4 -0
  73. package/lib/types/index.d.ts.map +1 -1
  74. package/lib/types/plugin-adapter-core.d.ts +263 -0
  75. package/lib/types/plugin-adapter-core.d.ts.map +1 -0
  76. package/lib/types/plugin-bridge.d.ts +187 -0
  77. package/lib/types/plugin-bridge.d.ts.map +1 -0
  78. package/lib/types/plugin-loopback.d.ts +104 -0
  79. package/lib/types/plugin-loopback.d.ts.map +1 -0
  80. package/lib/types/plugin-route-intercept.d.ts +114 -0
  81. package/lib/types/plugin-route-intercept.d.ts.map +1 -0
  82. package/lib/types/plugin-tunnel.d.ts +96 -0
  83. package/lib/types/plugin-tunnel.d.ts.map +1 -0
  84. package/lib/types/relay-client.d.ts +76 -0
  85. package/lib/types/relay-client.d.ts.map +1 -1
  86. package/lib/types/relay-server.d.ts +32 -1
  87. package/lib/types/relay-server.d.ts.map +1 -1
  88. package/llms.txt +1 -0
  89. package/package.json +2 -2
  90. package/scripts/check-plugin-bridge-ui.mjs +168 -0
  91. package/scripts/check-settings-form.mjs +3 -1
  92. package/src/adapters/dsh-better-sidebar/index.ts +144 -0
  93. package/src/adapters/dsh-better-sidebar/methods.ts +215 -0
  94. package/src/adapters/index.ts +19 -0
  95. package/src/adapters/types.ts +44 -0
  96. package/src/admin-routes.ts +64 -0
  97. package/src/client/effects/remote-session.ts +82 -10
  98. package/src/client/locales.ts +36 -0
  99. package/src/client/settings/SettingsSection.tsx +107 -3
  100. package/src/client/styles/remote-session.css.ts +70 -31
  101. package/src/client-data/plugin-bridge.ts +150 -0
  102. package/src/client-data/remote-status.ts +30 -0
  103. package/src/client-data/settings-form.ts +78 -0
  104. package/src/client-routes.ts +29 -1
  105. package/src/config.ts +32 -0
  106. package/src/http.ts +12 -0
  107. package/src/index.ts +252 -0
  108. package/src/plugin-adapter-core.ts +530 -0
  109. package/src/plugin-bridge.ts +435 -0
  110. package/src/plugin-loopback.ts +299 -0
  111. package/src/plugin-route-intercept.ts +1193 -0
  112. package/src/plugin-tunnel.ts +470 -0
  113. package/src/relay-client.ts +394 -2
  114. package/src/relay-server.ts +656 -1
@@ -0,0 +1,114 @@
1
+ /**
2
+ * The plugin bridge's route interception (PB-4): the sub-client's local
3
+ * DSH process owns a webServer whose `match(pathname)` every request goes
4
+ * through (RT dsh-host-webserver lib/index.js:350-364 — `handle` computes
5
+ * the RAW, undecoded pathname and calls `this.match`). Overriding that one
6
+ * method intercepts every route lookup — including routes registered AFTER
7
+ * us, because the host re-reads the instance method per request. A request
8
+ * aimed at a BRIDGED plugin's declared route, carrying a VIRTUAL session id,
9
+ * cannot be served locally (the session lives on the server): the surrogate
10
+ * handler forwards it through `relay.pluginRequest` (PB-3's
11
+ * `relay/v1/plugin`) with the real id restored, and streams the answer
12
+ * back. Everything else — undeclared routes, local sessions — flows to the
13
+ * original route untouched.
14
+ *
15
+ * The un-bridged refusals answer a UNIFORM 503 shape (header
16
+ * `x-zen-remote: unbridged`, JSON body naming the reason) — and, decisive
17
+ * for the security posture, they answer WITHOUT the local plugin handler
18
+ * ever running: a remote session must never execute a plugin route against
19
+ * local state, not even when the bridge is down (docs/adr/0001).
20
+ *
21
+ * The wrap is gated on a structural shape check like the typert and
22
+ * fetch-route ones; uninstall restores `match` only while the instance
23
+ * still holds OUR wrapper — a later wrapper chained over us keeps working,
24
+ * and ours goes inert instead (the same posture as intercept.ts and
25
+ * fetch-route-intercept.ts).
26
+ */
27
+ import type { BridgeRegistry, BridgeStatus } from './plugin-bridge.js';
28
+ import type { RelayClient } from './relay-client.js';
29
+ import type { NormalizedAdapter } from './plugin-adapter-core.js';
30
+ /** Structural verdict of {@link checkWebServerShape}: the facts the wrap
31
+ * reads per request — the `match` function the host's handle calls, the two
32
+ * route tables, and (PB-10) the `upgrades` table the host's upgrade handler
33
+ * reads `this.upgrades.get(pathname)` from (both Maps, RT
34
+ * lib/index.js:467-478 and 298-308; the probe verified them on real
35
+ * 0.2.0-rc.2 / 0.2.1-alpha.2 instances). */
36
+ export type WebServerShapeCheck = {
37
+ ok: true;
38
+ } | {
39
+ ok: false;
40
+ reasons: string[];
41
+ };
42
+ /** Pure inspection of the RAW webServer instance — nothing is mutated, so a
43
+ * failed check leaves every route exactly as it was. */
44
+ export declare function checkWebServerShape(webServer: unknown): WebServerShapeCheck;
45
+ /** One failure of the bridge answered to a browser, in the diagnostics
46
+ * ring. The query NEVER travels — only the plugin name and the reason. */
47
+ export interface PluginRouteFailureRecord {
48
+ time: string;
49
+ plugin: string;
50
+ reason: string;
51
+ }
52
+ /** What the client status route surfaces about this interception. */
53
+ export interface PluginRouteInterceptDiagnostics {
54
+ installed: boolean;
55
+ shape: WebServerShapeCheck;
56
+ /** Requests forwarded through the relay (virtual ids on declared routes,
57
+ * everything green). */
58
+ forwarded: number;
59
+ /** Requests answered with the uniform unbridged 503. */
60
+ refused: number;
61
+ /** The refusals keyed by reason. */
62
+ refusalsByReason: Record<string, number>;
63
+ /** The most recent refusals, oldest first, capped — time, plugin, reason
64
+ * only, never a query, never a session id. */
65
+ recentFailures: PluginRouteFailureRecord[];
66
+ /** PB-10: tunnels live RIGHT NOW. */
67
+ tunnelsCurrent: number;
68
+ /** PB-10: tunnels opened / torn down since the install. */
69
+ tunnelsOpened: number;
70
+ tunnelsClosed: number;
71
+ /** PB-11: adapter requests forwarded through the relay. */
72
+ adapterForwarded: number;
73
+ /** PB-11: adapter JSON answers rewritten by a `client.response` hook, and
74
+ * answers that passed through UNTOUCHED because the buffer cap (8 MB) or
75
+ * a parse failure made a rewrite impossible. */
76
+ adapterResponseRewrites: number;
77
+ adapterResponsePassthroughs: number;
78
+ }
79
+ export interface InstallPluginRouteInterceptOptions {
80
+ /** The RAW webServer instance (`ctx.webServer[symbols.original]`). */
81
+ webServer: object;
82
+ /** The local declaration registry (the `zenRemote` service's table). */
83
+ registry: BridgeRegistry;
84
+ /** The client relay client; `pluginRequest` is the forwarding door. */
85
+ relay: RelayClient;
86
+ /** The CURRENT handshake's server id, read live per call; a virtual id
87
+ * minted for another server (or before any handshake) refuses
88
+ * `server-changed`. */
89
+ getServerId: () => string | undefined;
90
+ /** The PB-2 bridge verdicts, computed live per call; a plugin that is
91
+ * not bridged right now refuses with the status's own reason. */
92
+ getStatus: () => BridgeStatus[];
93
+ /** The validated adapters (PB-11), as the wiring registered them.
94
+ * Absent/empty — the pluggable default — means no adapter routes exist
95
+ * and this interception behaves exactly as before. */
96
+ adapters?: readonly NormalizedAdapter[];
97
+ /** The session id the browser is CURRENTLY looking at (T62's report,
98
+ * read live per call). A VIRTUAL id means a remote session's panel is
99
+ * the request's origin — the signal an adapter route's `deny` hook uses
100
+ * to refuse requests (like better-sidebar's `open.external`) that carry
101
+ * no session field of their own. Absent → deny hooks see `undefined`
102
+ * and judge conservatively. */
103
+ getCurrentSession?: () => string | undefined;
104
+ /** Progress logging, wired to the context logger by index.ts. */
105
+ log?: (format: string, ...args: unknown[]) => void;
106
+ }
107
+ export interface PluginRouteInterceptHandle {
108
+ /** Restore `match` — ONLY while the instance still holds our wrapper
109
+ * (chained-over wraps go inert instead). Idempotent. */
110
+ uninstall(): void;
111
+ diagnostics(): PluginRouteInterceptDiagnostics;
112
+ }
113
+ export declare function installPluginRouteIntercept(options: InstallPluginRouteInterceptOptions): PluginRouteInterceptHandle;
114
+ //# sourceMappingURL=plugin-route-intercept.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-route-intercept.d.ts","sourceRoot":"","sources":["../../src/plugin-route-intercept.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAItE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAEpD,OAAO,KAAK,EAAE,iBAAiB,EAA8E,MAAM,0BAA0B,CAAA;AAG7I;;;;;4CAK4C;AAC5C,MAAM,MAAM,mBAAmB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAA;AAMjF;wDACwD;AACxD,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,OAAO,GAAG,mBAAmB,CAS3E;AAED;0EAC0E;AAC1E,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;CACf;AAED,qEAAqE;AACrE,MAAM,WAAW,+BAA+B;IAC9C,SAAS,EAAE,OAAO,CAAA;IAClB,KAAK,EAAE,mBAAmB,CAAA;IAC1B;4BACwB;IACxB,SAAS,EAAE,MAAM,CAAA;IACjB,wDAAwD;IACxD,OAAO,EAAE,MAAM,CAAA;IACf,oCAAoC;IACpC,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACxC;kDAC8C;IAC9C,cAAc,EAAE,wBAAwB,EAAE,CAAA;IAC1C,qCAAqC;IACrC,cAAc,EAAE,MAAM,CAAA;IACtB,2DAA2D;IAC3D,aAAa,EAAE,MAAM,CAAA;IACrB,aAAa,EAAE,MAAM,CAAA;IACrB,2DAA2D;IAC3D,gBAAgB,EAAE,MAAM,CAAA;IACxB;;oDAEgD;IAChD,uBAAuB,EAAE,MAAM,CAAA;IAC/B,2BAA2B,EAAE,MAAM,CAAA;CACpC;AAED,MAAM,WAAW,kCAAkC;IACjD,sEAAsE;IACtE,SAAS,EAAE,MAAM,CAAA;IACjB,wEAAwE;IACxE,QAAQ,EAAE,cAAc,CAAA;IACxB,uEAAuE;IACvE,KAAK,EAAE,WAAW,CAAA;IAClB;;2BAEuB;IACvB,WAAW,EAAE,MAAM,MAAM,GAAG,SAAS,CAAA;IACrC;qEACiE;IACjE,SAAS,EAAE,MAAM,YAAY,EAAE,CAAA;IAC/B;;0DAEsD;IACtD,QAAQ,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAA;IACvC;;;;;mCAK+B;IAC/B,iBAAiB,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAA;IAC5C,iEAAiE;IACjE,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAA;CACnD;AAED,MAAM,WAAW,0BAA0B;IACzC;4DACwD;IACxD,SAAS,IAAI,IAAI,CAAA;IACjB,WAAW,IAAI,+BAA+B,CAAA;CAC/C;AAqBD,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,kCAAkC,GAAG,0BAA0B,CA+gCnH"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * The plugin bridge's WebSocket byte tunnel (PB-10), server half. Per
3
+ * docs/adr/0002 the relay NEVER speaks WebSocket: a bridged upgrade route is
4
+ * carried as ONE NDJSON streaming response (server → client bytes) plus
5
+ * ordinary POSTs per chunk (client → server bytes), both ends treating the
6
+ * payload as OPAQUE BYTES — the WebSocket handshake and every frame are
7
+ * produced by the real plugin route on the server machine and arrive at the
8
+ * browser untouched.
9
+ *
10
+ * Open: `POST relay/v1/plugin-tunnel/open` — the same coordinate headers and
11
+ * the SAME validation chain as `v1/plugin` (shared {@link validatePluginCall};
12
+ * the route must additionally be declared `upgrade: true`), plus a small
13
+ * JSON body carrying exactly the four `sec-websocket-*` headers and the
14
+ * optional early head bytes. On success the relay dials the local web server
15
+ * itself (net/tls, loopback only), writes a hand-built upgrade request with
16
+ * the operator's cookie, and pumps socket bytes downstream line by line.
17
+ * Send/close: `POST relay/v1/plugin-tunnel/{send,close}` — ordered chunks
18
+ * upstream (one in-flight per tunnel, sequence-gated) and idempotent
19
+ * teardown.
20
+ *
21
+ * Every tunnel belongs to the device that opened it; another device's
22
+ * send/close is 404. Budgets, heartbeats and the slow-reader discipline
23
+ * mirror the routes this module sits beside.
24
+ */
25
+ import type { IncomingMessage, ServerResponse } from 'node:http';
26
+ import type { BridgeRegistry, NormalizedBridgeRoute, NormalizedDeclaration } from './plugin-bridge.js';
27
+ import type { PluginLoopback } from './plugin-loopback.js';
28
+ /** The shared validation verdict for the two plugin routes (PB-3, reused by
29
+ * the tunnel open in PB-10): the whole chain from the coordinate headers to
30
+ * the consent wall, minus each route's own extras (the method check for
31
+ * `v1/plugin`, the upgrade mark here). First failure wins, in this order. */
32
+ export type PluginCallValidation = {
33
+ ok: true;
34
+ declaration: NormalizedDeclaration;
35
+ route: NormalizedBridgeRoute;
36
+ params: URLSearchParams;
37
+ sessionId: string;
38
+ normalizedQuery: string;
39
+ } | {
40
+ ok: false;
41
+ status: number;
42
+ code: string;
43
+ };
44
+ /** Validate one plugin-bridge call's coordinates — the parse-once query
45
+ * discipline (the re-serialization of THIS parse is the only string the
46
+ * tunnel's upgrade request may carry), the share table with its ancestor
47
+ * walk, and the live consent wall. `bridge.getConsent()` throwing reads as
48
+ * unknown consent, and unknown consent stays home. */
49
+ export declare function validatePluginCall(bridge: {
50
+ registry: BridgeRegistry;
51
+ getConsent: () => Record<string, boolean>;
52
+ }, coords: {
53
+ pluginName: string;
54
+ pluginVersion: string;
55
+ pluginDigest: string;
56
+ routePath: string;
57
+ rawQuery: string;
58
+ }, isAccessible: (sessionId: string) => boolean): PluginCallValidation;
59
+ /** Largest single downstream data LINE (base64 will inflate by 4/3 — the cap
60
+ * is on the RAW bytes carried, and the line stays well under typical proxy
61
+ * line limits this way). */
62
+ export declare const TUNNEL_DOWN_CHUNK_BYTES: number;
63
+ /** Largest upstream send chunk (the wire body of one `send`). */
64
+ export declare const TUNNEL_UP_CHUNK_BYTES: number;
65
+ /** Concurrent tunnels per device. */
66
+ export declare const MAX_TUNNELS_PER_DEVICE = 8;
67
+ export interface CreatePluginTunnelOptions {
68
+ bridge: {
69
+ registry: BridgeRegistry;
70
+ getConsent: () => Record<string, boolean>;
71
+ loopback: PluginLoopback;
72
+ };
73
+ /** The loopback web server bind, re-read per tunnel (same live discipline
74
+ * as every target read in this plugin). */
75
+ getTarget: () => {
76
+ protocol: 'http:' | 'https:';
77
+ port: number;
78
+ };
79
+ isAccessible: (sessionId: string) => boolean;
80
+ /** Heartbeat interval for the open stream (the ping lines). */
81
+ heartbeatMs: number;
82
+ }
83
+ export interface PluginTunnelRoutes {
84
+ /** `POST relay/v1/plugin-tunnel/open`. */
85
+ open(req: IncomingMessage, res: ServerResponse): Promise<void>;
86
+ /** `POST relay/v1/plugin-tunnel/send`. */
87
+ send(req: IncomingMessage, res: ServerResponse): Promise<void>;
88
+ /** `POST relay/v1/plugin-tunnel/close`. */
89
+ close(req: IncomingMessage, res: ServerResponse): Promise<void>;
90
+ /** Live tunnel count (the settings diagnostics). */
91
+ tunnelCount(): number;
92
+ }
93
+ /** The tunnel table: id → the live entry. Every teardown path removes its
94
+ * own entry exactly once. */
95
+ export declare function createPluginTunnel(options: CreatePluginTunnelOptions): PluginTunnelRoutes;
96
+ //# sourceMappingURL=plugin-tunnel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-tunnel.d.ts","sourceRoot":"","sources":["../../src/plugin-tunnel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAIhE,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AACtG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAE1D;;;6EAG6E;AAC7E,MAAM,MAAM,oBAAoB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,WAAW,EAAE,qBAAqB,CAAC;IAAC,KAAK,EAAE,qBAAqB,CAAC;IAAC,MAAM,EAAE,eAAe,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,MAAM,CAAA;CAAE,GACnJ;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AAE/C;;;;sDAIsD;AACtD,wBAAgB,kBAAkB,CAChC,MAAM,EAAE;IAAE,QAAQ,EAAE,cAAc,CAAC;IAAC,UAAU,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,EAC/E,MAAM,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,EAChH,YAAY,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,GAC3C,oBAAoB,CA4BtB;AAED;;4BAE4B;AAC5B,eAAO,MAAM,uBAAuB,QAAY,CAAA;AAEhD,iEAAiE;AACjE,eAAO,MAAM,qBAAqB,QAAY,CAAA;AAE9C,qCAAqC;AACrC,eAAO,MAAM,sBAAsB,IAAI,CAAA;AA2BvC,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE;QAAE,QAAQ,EAAE,cAAc,CAAC;QAAC,UAAU,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAAC,QAAQ,EAAE,cAAc,CAAA;KAAE,CAAA;IACzG;+CAC2C;IAC3C,SAAS,EAAE,MAAM;QAAE,QAAQ,EAAE,OAAO,GAAG,QAAQ,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IAC/D,YAAY,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAA;IAC5C,+DAA+D;IAC/D,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,0CAA0C;IAC1C,IAAI,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9D,0CAA0C;IAC1C,IAAI,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9D,2CAA2C;IAC3C,KAAK,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/D,oDAAoD;IACpD,WAAW,IAAI,MAAM,CAAA;CACtB;AAED;6BAC6B;AAC7B,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,kBAAkB,CAyUzF"}
@@ -45,6 +45,7 @@
45
45
  * notification channel the state changes use.
46
46
  */
47
47
  import type { RelayCompatVerdict } from './fingerprint.js';
48
+ import type { ServerPluginEntry } from './plugin-bridge.js';
48
49
  /**
49
50
  * The clock face the reconnect machinery runs on: wall time, timers and the
50
51
  * jitter source. Injectable so tests drive the whole backoff sequence
@@ -82,6 +83,15 @@ export interface RelayHandshake {
82
83
  * has and pushes nothing.
83
84
  */
84
85
  deviceName?: string;
86
+ /**
87
+ * The server's per-plugin bridge table (PB-2), verbatim from the
88
+ * handshake. `undefined` from an OLDER server whose handshake carries no
89
+ * `plugins` field at all — and that distinction is load-bearing:
90
+ * {@link compareBridges} reads `undefined` as `server-too-old`, while an
91
+ * empty table means "this server speaks the field, nothing registered"
92
+ * (`not-on-server`). Never normalized into `{}` here.
93
+ */
94
+ plugins?: Record<string, ServerPluginEntry>;
85
95
  }
86
96
  /**
87
97
  * One relay failure. `code` is the mapped reason (the contract's state
@@ -230,6 +240,72 @@ export interface RelayClient {
230
240
  * `Content-Length`, when it had one) and a timeout there never moves the
231
241
  * connection state — the invoke route's rules (T31-fix). */
232
242
  upload(options: RelayUploadOptions, signal?: AbortSignal): Promise<RelayHttpResult>;
243
+ /** One bridged-plugin round-trip (PB-4): the coordinates travel in the
244
+ * x-zr-* headers (PB-3's protocol), no body. Resolves with the UNREAD
245
+ * fetch Response the moment the headers arrive — the caller streams the
246
+ * body itself — and EVERY HTTP status is carried verbatim (a refusal's
247
+ * `x-zen-remote-error` header is the caller's classifier, never an
248
+ * exception here). The header wait gets the plain request timeout; the
249
+ * BODY carries no deadline of our own (a stream takes as long as it
250
+ * takes) — the caller's signal aborts it. Transport deaths judge the
251
+ * link like every exchange (offline); `requireCredentials` throws the
252
+ * usual RelayError when nothing is configured. */
253
+ pluginRequest(request: RelayPluginRequest, signal?: AbortSignal): Promise<Response>;
254
+ /** Open one WebSocket byte tunnel (PB-10): the coordinates travel like a
255
+ * pluginRequest's, plus the browser handshake's `sec-websocket-*` headers
256
+ * and any early head bytes (base64). Resolves once the NDJSON stream's
257
+ * FIRST line named the tunnel id; `frames` then yields the upstream's
258
+ * raw bytes (each `data` line decoded) and ENDS when the server writes
259
+ * the `end` line (upstream closed or errored). A refusal (any non-200 or
260
+ * a missing open line) throws RelayError with the wire's
261
+ * `x-zen-remote-error` code. */
262
+ pluginTunnelOpen(request: RelayPluginRequest & {
263
+ wsHeaders: {
264
+ 'sec-websocket-key': string;
265
+ 'sec-websocket-version': string;
266
+ 'sec-websocket-protocol'?: string;
267
+ 'sec-websocket-extensions'?: string;
268
+ };
269
+ head?: string;
270
+ }, signal?: AbortSignal): Promise<{
271
+ tunnel: string;
272
+ frames: AsyncIterable<Uint8Array>;
273
+ }>;
274
+ /** One ordered upstream chunk (PB-10): `seq` must count from 0 without
275
+ * gaps — the server answers 409 `tunnel-out-of-order` and closes the
276
+ * tunnel otherwise. Resolves after the server took the bytes (drain
277
+ * included). */
278
+ pluginTunnelSend(tunnel: string, seq: number, bytes: Uint8Array, signal?: AbortSignal): Promise<void>;
279
+ /** Idempotent teardown (PB-10). */
280
+ pluginTunnelClose(tunnel: string, signal?: AbortSignal): Promise<void>;
281
+ }
282
+ /** The inputs of one {@link RelayClient.pluginRequest} round-trip (PB-4) —
283
+ * the PB-3 wire protocol's coordinates, already validated and virtual-id-
284
+ * restored by the caller. */
285
+ export interface RelayPluginRequest {
286
+ plugin: string;
287
+ version: string;
288
+ digest: string;
289
+ /** The declared route path, verbatim (never decoded). */
290
+ path: string;
291
+ /** The re-serialized query string, session id already restored to its
292
+ * ORIGINAL form. */
293
+ query: string;
294
+ /** The request method (PB-9); defaults to GET. Must be one the route
295
+ * declared — the server answers 405 `method-undeclared` otherwise. */
296
+ method?: string;
297
+ /** The request BODY (PB-9), forwarded verbatim as a stream. `null` means
298
+ * no body. A streaming body rides chunked (undici forbids hand-set length
299
+ * headers), so the server's streaming byte count is the size gate. */
300
+ body?: ReadableStream<Uint8Array> | null;
301
+ /** The body's content type, passed through to the plugin when set. */
302
+ contentType?: string;
303
+ /** The declared body size, when the local request carried one — sizes the
304
+ * header-wait budget and nothing else (a streaming body goes chunked). */
305
+ contentLength?: number;
306
+ /** The one request header passed through to the plugin beside `range`. */
307
+ accept?: string;
308
+ range?: string;
233
309
  }
234
310
  /** The inputs of one {@link RelayClient.upload} round-trip. */
235
311
  export interface RelayUploadOptions {
@@ -1 +1 @@
1
- {"version":3,"file":"relay-client.d.ts","sourceRoot":"","sources":["../../src/relay-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"relay-client.d.ts","sourceRoot":"","sources":["../../src/relay-client.ts"],"names":[],"mappings":"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"}
@@ -27,6 +27,10 @@
27
27
  */
28
28
  import type { IncomingMessage, ServerResponse } from 'node:http';
29
29
  import type { ShareStore } from './share-store.js';
30
+ import type { ServerPluginEntry, BridgeRegistry } from './plugin-bridge.js';
31
+ import { type PluginTunnelRoutes } from './plugin-tunnel.js';
32
+ import type { PluginLoopback } from './plugin-loopback.js';
33
+ import type { NormalizedAdapter } from './plugin-adapter-core.js';
30
34
  /**
31
35
  * Registration prefix on the host webServer. Deliberately WITHOUT the
32
36
  * trailing slash: the webserver matches a prefix route as `pathname === p ||
@@ -108,12 +112,17 @@ export interface RelayGateway {
108
112
  * callback too (T42), awaited per handshake so a just-restarted server never
109
113
  * serves a stale map; a THROWN compute degrades to the empty map here so a
110
114
  * broken fingerprint source can never fail the handshake itself — the
111
- * client's comparison judges every group `unavailable`, never `different`. */
115
+ * client's comparison judges every group `unavailable`, never `different`.
116
+ * `plugins` is the same discipline for the plugin bridge (PB-2): computed
117
+ * FRESH per handshake (the local registry and the row's consent map are both
118
+ * live), a THROWN compute degrades to the empty table — the handshake still
119
+ * succeeds and the client reads every local plugin `not-on-server`. */
112
120
  export interface RelayServerInfo {
113
121
  serverId: string;
114
122
  serverName: () => string;
115
123
  dshVersion: string;
116
124
  fingerprints?: () => Record<string, string> | Promise<Record<string, string>>;
125
+ plugins?: () => Record<string, ServerPluginEntry>;
117
126
  }
118
127
  export interface RelayHandlerOptions {
119
128
  /** The per-apply shared secret; an empty one refuses every request. */
@@ -151,6 +160,28 @@ export interface RelayHandlerOptions {
151
160
  * destroyed (the {@link DRAIN_DEADLINE_MS} bound); defaults to 10000.
152
161
  * Tests inject a small value so the deadline path stays cheap to drive. */
153
162
  drainDeadlineMs?: number;
163
+ /** The plugin bridge's server half (PB-3). Absent — a wiring that never
164
+ * mounted it, old tests — and `POST relay/v1/plugin` answers 501
165
+ * `unsupported`, exactly like the `/api` passthrough without a dispatcher. */
166
+ pluginBridge?: {
167
+ /** THIS process's bridge declaration registry (the `zenRemote` service's
168
+ * backing table) — the source of truth for "which plugin, which version,
169
+ * which routes". */
170
+ registry: BridgeRegistry;
171
+ /** The row's per-plugin whole-machine-data consent, read LIVE per request
172
+ * (a volatile row field — never snapshotted). */
173
+ getConsent: () => Record<string, boolean>;
174
+ /** The loopback executor performing the actual plugin GET. */
175
+ loopback: PluginLoopback;
176
+ /** The WebSocket byte tunnel's server half (PB-10). Absent — a wiring
177
+ * that predates the tunnels — and the three `plugin-tunnel` routes
178
+ * answer 501 `unsupported` like an unwired bridge. */
179
+ tunnel?: PluginTunnelRoutes;
180
+ /** The validated adapters (PB-11), as the wiring registered them.
181
+ * Absent/empty — the pluggable default — and `v1/plugin` serves
182
+ * cooperative declarations only, exactly as before. */
183
+ adapters?: readonly NormalizedAdapter[];
184
+ };
154
185
  }
155
186
  /**
156
187
  * The relay route handler plus its introspection surface. A function WITH
@@ -1 +1 @@
1
- {"version":3,"file":"relay-server.d.ts","sourceRoot":"","sources":["../../src/relay-server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"relay-server.d.ts","sourceRoot":"","sources":["../../src/relay-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAOH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAChE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAWlD,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAE3E,OAAO,EAA0C,KAAK,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACpG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAE1D,OAAO,KAAK,EAAE,iBAAiB,EAAkB,MAAM,0BAA0B,CAAA;AAGjF;;;;;GAKG;AACH,eAAO,MAAM,YAAY,2BAA2B,CAAA;AAQpD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,gBAAgB,QAAoB,CAAA;AAEjD;0EAC0E;AAC1E,eAAO,MAAM,gBAAgB,kCAAkC,CAAA;AA+F/D;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC1G,MAAM,CAAC,IAAI,EAAE;QACX,SAAS,EAAE,MAAM,CAAA;QACjB,MAAM,EAAE,MAAM,CAAA;QACd,IAAI,EAAE,OAAO,CAAA;QACb,MAAM,CAAC,EAAE,WAAW,CAAA;KACrB,GAAG,OAAO,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAA;IACnC;uDACmD;IACnD,UAAU,CAAC,EAAE;QACX,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,OAAO,CAAA;KACnH,CAAA;IACD,4EAA4E;IAC5E,WAAW,CAAC,CACV,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,IAAI,EAAE,OAAO,GACZ,OAAO,CAAC,OAAO,CAAC,CAAA;CACpB;AAED;;;;;;;;;;;uEAWuE;AACvE,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,MAAM,CAAA;IACxB,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAC7E,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAA;CAClD;AAED,MAAM,WAAW,mBAAmB;IAClC,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAA;IACd,sDAAsD;IACtD,KAAK,EAAE,UAAU,CAAA;IACjB,+DAA+D;IAC/D,OAAO,EAAE,YAAY,CAAA;IACrB,uBAAuB;IACvB,UAAU,EAAE,eAAe,CAAA;IAC3B,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;IAC7C;;;;;;mEAM+D;IAC/D,WAAW,CAAC,EAAE,MAAM,CAAC,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC,GAAG,SAAS,CAAA;IACzE;;qCAEiC;IACjC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;uDAGmD;IACnD,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B;;0EAEsE;IACtE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;+EAE2E;IAC3E,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB;;kFAE8E;IAC9E,YAAY,CAAC,EAAE;QACb;;4BAEoB;QACpB,QAAQ,EAAE,cAAc,CAAA;QACxB;yDACiD;QACjD,UAAU,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;QACzC,8DAA8D;QAC9D,QAAQ,EAAE,cAAc,CAAA;QACxB;;8DAEsD;QACtD,MAAM,CAAC,EAAE,kBAAkB,CAAA;QAC3B;;+DAEuD;QACvD,QAAQ,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAA;KACxC,CAAA;CACF;AAED;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1D;+BAC2B;IAC3B,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAA;IACtC;;;;;;;yEAOqE;IACrE,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B;AASD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAoBjD;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAW1C;AAsWD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,YAAY,CAq3D7E;AAaD,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,yBAAyB,EACzB,yBAAyB,EACzB,iBAAiB,GAClB,MAAM,wBAAwB,CAAA"}
package/llms.txt CHANGED
@@ -9,6 +9,7 @@
9
9
  - Host sharing: per-session remote toggle from four places (sidebar session "…" menu, title-row icon with confirm, settings list, the phone session info card's remote access switch); idle sleep after 48h without session activity (running/waiting sessions exempt, viewers don't count); auto-share new sessions (opt-in; relay-created sessions always auto-share); subagent/fork sessions follow the parent.
10
10
  - Pairing codes are role-bound: web app devices (phones/browsers, see all sessions) redeem on the pairing page; desktop app devices pair from the client's settings page via `/lan-gate/pair/claim-desktop` and only ever reach the relay prefix (everything else 403 `relay-only`).
11
11
  - Relay protocol (prefix `/_dsh/zen-remote/relay`): `GET ping`, `POST v1/handshake` (relay protocol version + DSH interface fingerprints — version mismatch refuses; fingerprint mismatch annotates and degrades; the answer carries `serverName`/`deviceName`), `POST v1/invoke`, `POST v1/stream` (NDJSON, ping heartbeats that carry the current `serverName`/`deviceName`, `unshared` error line on close), `POST v1/event-result`, `POST v1/http` (the plain-HTTP long tail: only `changes.summary` / `changes.diff`, GET only, query normalized to `sessionId`/`seq`/`index`, dispatched in-process through the host's shared `/api` handler), `POST v1/upload` (a remote session's non-image attachments: raw byte stream, share-checked, query normalized to `sessionId`/`name`, 100 MiB cap answers 413, 8 concurrent per device answers 429, dispatched in-process through the same shared handler), `POST v1/unshare`, `POST v1/device/name` (gateway-handled rename of the CALLING device's own record — paired desktop clients only, never forwarded to DSH; 1–40 chars). Access control is a per-method allowlist (`src/relay-access.ts` RELAY_METHODS, mirrored by intercept.ts CLIENT_METHOD_FIELDS, pinned identical by test); global reads (`session/control`, `session/list`, `workspace/follow`) and the @-mention candidates are output-filtered (candidates list only shared sessions); `session/modelCatalog` is a parameter-less global read whose `failures` array the server empties; `dsh-session:` references are scanned wherever DSH would inject them — prompt content, queue-edit content, every string of a `commands/execute` call — and a reference naming an unshared session refuses the call. Approval/question events forward first come, first served; the relay never answers on the client's behalf (an unshared session's approval waits for the server UI or the next subscription), and unshared sessions' notification-type events are never forwarded.
12
+ - Plugin bridge (2.1): another plugin registers a bridge declaration (`ctx.inject(['zenRemote'], …)`, types from `dsh-zen-remote`); when the plugin name, exact version and declaration digest agree on BOTH ends, the sub-client reroutes the declared GET routes to the server — `POST relay/v1/plugin` (coordinates in x-zr-* headers, no body; validation: registry → version+digest → exact path → parse-once query with the route's sessionParam → share table → per-plugin consent for scope:'server' routes → 8 per device), the server re-issues it over loopback HTTP with the operator's cookie and streams the answer back; unbridged answers are a uniform 503 (`x-zen-remote: unbridged`, reason: not-on-server / version-mismatch / declaration-mismatch / consent-off / offline / server-too-old / server-changed / bad-query / method-not-bridged, or the relay's own codes). 2.1 forwards GET on exact paths only; plugin WebSockets will ride streamed responses in 2.2 (docs/adr/0002). Docs: docs/plugin-bridge.md
12
13
  - Remote extras: a remote session can attach non-image files — the sub-client's own backend wraps the host's `fetchRoutes` upload entry and streams the bytes through `v1/upload` (the 100 MiB cap is the relay's own, an over-limit pick is refused locally on the client, slow links are bounded by a 300 s client-side timeout; export is disabled remotely instead — menu item hidden, backend 403 for virtual ids). The model picker in a remote session lists the SERVER's catalog (groups named "server name · group name"); local sessions list only local models, the last server groups survive relay offline, and virtual groups never appear in local settings such as the vision router (boundary: a server-created session that never sent a message shows the local default model).
13
14
  - Gateway auth chain: device token (Bearer) → gateway marker headers `x-zen-remote-via/-role/-device` + `x-zen-remote-secret` (per-load shared secret, `LAN_GATE_RELAY_SECRET`, not externally settable, re-verified with timingSafeEqual) → per-method share-table check. Admin API stays loopback-only; forwarded requests may read status but never mutate.
14
15
  - Admin surface: the dsh-zen-remote settings block (pairing with role picker, device list with rename/set-role/revoke, push test, shares list). `/lan-gate/admin` is now just a notice pointing there.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-zen-remote",
3
- "version": "2.0.3",
3
+ "version": "2.2.0",
4
4
  "description": "一个插件、两种角色:主服务端把 DeepSeek Harness 变成手机能安全访问的 PWA(移动端界面、配对码网关、Web Push),并把选定会话共享给已配对的桌面端;子客户端在另一台 DSH 桌面端的侧边栏里远程使用这些会话。One DSH plugin, two roles: the host turns DeepSeek Harness into a phone-reachable PWA (mobile UI, pairing-code gateway behind your own reverse proxy, Web Push) and shares chosen sessions to paired desktops; the client uses those sessions remotely from another DSH desktop app's sidebar.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -160,7 +160,7 @@
160
160
  "scripts": {
161
161
  "build": "tsc -p tsconfig.json && tsc -p tsconfig.client.json && node scripts/build-client.mjs",
162
162
  "verify": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json --noEmit",
163
- "test": "node --test \"test/*.test.cjs\" && node scripts/check-sunk-viewport.mjs && node scripts/check-keyboard-avoid.mjs && node scripts/check-attach-upload.mjs && node scripts/check-share-image.mjs && node scripts/check-upload-endpoint.mjs && node scripts/check-host-shadow.mjs && node scripts/check-compat-store.mjs && node scripts/check-settings-form.mjs && node scripts/check-shares.mjs && node scripts/check-client-externals.mjs && node scripts/check-info-sheet-stacking.mjs && node scripts/sync-doc-version.mjs --check",
163
+ "test": "node --test \"test/*.test.cjs\" && node scripts/check-sunk-viewport.mjs && node scripts/check-keyboard-avoid.mjs && node scripts/check-attach-upload.mjs && node scripts/check-share-image.mjs && node scripts/check-upload-endpoint.mjs && node scripts/check-host-shadow.mjs && node scripts/check-compat-store.mjs && node scripts/check-settings-form.mjs && node scripts/check-shares.mjs && node scripts/check-plugin-bridge-ui.mjs && node scripts/check-client-externals.mjs && node scripts/check-info-sheet-stacking.mjs && node scripts/sync-doc-version.mjs --check",
164
164
  "version": "node scripts/sync-doc-version.mjs && git add README.md README.zh-CN.md"
165
165
  }
166
166
  }
@@ -0,0 +1,168 @@
1
+ // Behaviour check for the plugin bridge's settings-page pure pieces
2
+ // (src/client-data/plugin-bridge.ts, PB-5), driven against the REAL
3
+ // settings-form pieces the toggles stage through (Node >= 23.6 type
4
+ // stripping, no build needed):
5
+ //
6
+ // - derivePluginBridgeStatuses: every BridgeReason's copy key, the bridged
7
+ // case, the unknown-reason fallback, malformed rows skipped, non-arrays
8
+ // answered with [].
9
+ // - togglePluginConsent: one key per click, every other key carried over
10
+ // (including entries for plugins no longer registered), an illegal
11
+ // current map read as empty.
12
+ // - consentField (settings-form.ts): the staged-form spec the toggles ride
13
+ // — format/parse round-trip, an illegal draft blocks the save, and the
14
+ // full toggle → parse → plan shape a save would send.
15
+ //
16
+ // Run: node scripts/check-plugin-bridge-ui.mjs
17
+ import assert from 'node:assert/strict'
18
+ import { bridgeSectionState, derivePluginBridgeStatuses, readAdminBridgeRow, togglePluginConsent } from '../src/client-data/plugin-bridge.ts'
19
+ import { consentField, SETTINGS_FIELDS } from '../src/client-data/settings-form.ts'
20
+
21
+ let testChain = Promise.resolve()
22
+ function test(name, fn) {
23
+ testChain = testChain
24
+ .then(fn)
25
+ .then(() => { console.log(`ok - ${name}`) })
26
+ .catch((error) => {
27
+ console.error(`not ok - ${name}`)
28
+ throw error
29
+ })
30
+ return testChain
31
+ }
32
+
33
+ const STATUS = (over = {}) => ({ plugin: 'test-panel', bridged: false, reason: 'offline', localVersion: '1.0.0', ...over })
34
+
35
+ // ---- derivePluginBridgeStatuses ---------------------------------------------
36
+
37
+ await test('every reason maps to its copy key, bridged to 已桥接', () => {
38
+ const rows = derivePluginBridgeStatuses([
39
+ STATUS({ bridged: true, reason: undefined }),
40
+ STATUS({ reason: 'not-on-server' }),
41
+ STATUS({ reason: 'version-mismatch', serverVersion: '2.0.0' }),
42
+ STATUS({ reason: 'declaration-mismatch' }),
43
+ STATUS({ reason: 'consent-off' }),
44
+ STATUS({ reason: 'offline' }),
45
+ STATUS({ reason: 'server-too-old' }),
46
+ ])
47
+ assert.deepEqual(rows.map((row) => row.stateKey), [
48
+ 'settings.bridge.stateBridged',
49
+ 'settings.bridge.stateNotOnServer',
50
+ 'settings.bridge.stateVersionMismatch',
51
+ 'settings.bridge.stateDeclarationMismatch',
52
+ 'settings.bridge.stateConsentOff',
53
+ 'settings.bridge.stateOffline',
54
+ 'settings.bridge.stateServerTooOld',
55
+ ])
56
+ assert.equal(rows[0].plugin, 'test-panel')
57
+ assert.equal(rows[0].localVersion, '1.0.0')
58
+ // 版本不同的文案 carries the server version for the {version} interpolation.
59
+ assert.equal(rows[2].serverVersion, '2.0.0')
60
+ })
61
+
62
+ await test('an unknown reason falls to the offline copy; malformed rows and shapes never throw', () => {
63
+ const rows = derivePluginBridgeStatuses([
64
+ STATUS({ reason: 'some-future-reason' }),
65
+ STATUS({ bridged: true, reason: 'offline' }), // bridged outranks any reason
66
+ null,
67
+ 42,
68
+ { noPlugin: true },
69
+ ])
70
+ assert.deepEqual(rows.map((row) => row.stateKey), [
71
+ 'settings.bridge.stateOffline',
72
+ 'settings.bridge.stateBridged',
73
+ ])
74
+ assert.deepEqual(derivePluginBridgeStatuses(undefined), [])
75
+ assert.deepEqual(derivePluginBridgeStatuses('x'), [])
76
+ assert.deepEqual(derivePluginBridgeStatuses(42), [])
77
+ })
78
+
79
+ // ---- togglePluginConsent ----------------------------------------------------
80
+
81
+ await test('a toggle changes exactly ONE key and keeps every other — including unregistered leftovers', () => {
82
+ const current = { 'old-plugin': true, 'another': false, 'gone-plugin': true }
83
+ const next = togglePluginConsent(current, 'new-plugin', true)
84
+ assert.deepEqual(next, { 'old-plugin': true, another: false, 'gone-plugin': true, 'new-plugin': true })
85
+ // The INPUT is never mutated (staging reads it again for the next toggle).
86
+ assert.deepEqual(current, { 'old-plugin': true, 'another': false, 'gone-plugin': true })
87
+
88
+ const off = togglePluginConsent(next, 'new-plugin', false)
89
+ assert.deepEqual(off, { 'old-plugin': true, another: false, 'gone-plugin': true, 'new-plugin': false })
90
+ })
91
+
92
+ await test('an illegal current map reads as empty; the toggle writes from scratch', () => {
93
+ for (const bad of [undefined, null, 'x', 42, [], { plugin: 'yes' }]) {
94
+ assert.deepEqual(togglePluginConsent(bad, 'p', true), { p: true }, `current ${JSON.stringify(bad)} must read as empty`)
95
+ }
96
+ })
97
+
98
+ // ---- bridgeSectionState: the three-state listing (PB-9-fix) -----------------
99
+
100
+ await test('the listing distinguishes loading / failed from a confirmed empty registry', () => {
101
+ assert.equal(bridgeSectionState('loading', []), 'loading', 'no answer yet — never reads as empty')
102
+ assert.equal(bridgeSectionState('error', []), 'error', 'a failed or refused fetch is an error, not "nothing registered"')
103
+ assert.equal(bridgeSectionState('ready', []), 'empty', 'an ANSWER with zero rows is the honest empty')
104
+ assert.equal(bridgeSectionState('ready', [{ plugin: 'p' }]), 'plugins')
105
+ })
106
+
107
+ // ---- consentField: the staged-form spec the toggles ride ---------------------
108
+
109
+ await test('consentField round-trips a map through format/parse; an illegal draft blocks the save', () => {
110
+ const spec = consentField('pluginBridgeConsent')
111
+ assert.equal(spec.field, 'pluginBridgeConsent')
112
+ const map = { 'a': true, 'b': false }
113
+ const draft = spec.format(map)
114
+ const write = spec.parse(draft)
115
+ assert.deepEqual(write, { kind: 'set', value: map })
116
+
117
+ // format tolerates an illegal stored value (the row validator's job to refuse it).
118
+ assert.equal(spec.format('junk'), '{}')
119
+ assert.equal(spec.format(null), '{}')
120
+ assert.equal(spec.format(['x']), '{}')
121
+ assert.equal(spec.format({ a: 'yes' }), '{}')
122
+
123
+ // Every malformed draft is undefined — the save refuses rather than writing junk.
124
+ for (const bad of ['', 'not json', '"string"', '42', '[]', 'null', '{"a":"yes"}', '{"a":1}']) {
125
+ assert.equal(spec.parse(bad), undefined, `draft ${bad} must not parse`)
126
+ }
127
+ })
128
+
129
+ await test('the toggle lands on the SHARED field table and a save would send exactly one set op', () => {
130
+ // The field the toggles stage is on SETTINGS_FIELDS — the same table
131
+ // autoShareNewSessions rides, hence the same save button and fence.
132
+ assert.ok(SETTINGS_FIELDS.some((spec) => spec.field === 'pluginBridgeConsent'), 'pluginBridgeConsent is a settings field')
133
+
134
+ const spec = SETTINGS_FIELDS.find((entry) => entry.field === 'pluginBridgeConsent')
135
+ const current = { 'old': true }
136
+ const next = togglePluginConsent(current, 'new-plugin', true)
137
+ const write = spec.parse(JSON.stringify(next))
138
+ assert.deepEqual(write, { kind: 'set', value: { old: true, 'new-plugin': true } })
139
+
140
+ // And the no-change detection: a draft equal to the displayed value
141
+ // produces no write at all (plan() compares against format(displayValue)).
142
+ const unchanged = spec.parse(spec.format(current))
143
+ assert.deepEqual(unchanged, { kind: 'set', value: { old: true } })
144
+ assert.equal(JSON.stringify(next) === spec.format(next), true, 'an unchanged map formats identical to its draft')
145
+ })
146
+
147
+ // ---- readAdminBridgeRow (PB-11) ----------------------------------------------
148
+
149
+ await test('the adapter mark rides the admin listing row and reads tolerantly', () => {
150
+ // A full row: adapted true rides through; the tolerable defaults hold.
151
+ assert.deepEqual(
152
+ readAdminBridgeRow({ plugin: 'p', version: '1.0.0', hasServerScope: true, consent: false, adapted: true }),
153
+ { plugin: 'p', version: '1.0.0', hasServerScope: true, consent: false, adapted: true },
154
+ )
155
+ // An older server without the field reads as NOT adapted.
156
+ assert.equal(readAdminBridgeRow({ plugin: 'p', version: '1', hasServerScope: false, consent: true }).adapted, false)
157
+ // A junk field value is not "adapted" — only the exact true is.
158
+ assert.equal(readAdminBridgeRow({ plugin: 'p', version: '1', hasServerScope: false, consent: true, adapted: 'yes' }).adapted, false)
159
+ // A malformed row is undefined, never a throw.
160
+ for (const bad of [undefined, null, 42, 'x', [], {}, { version: '1' }]) {
161
+ assert.equal(readAdminBridgeRow(bad), undefined, `row ${JSON.stringify(bad)} must not parse`)
162
+ }
163
+ })
164
+
165
+ let failed = 0
166
+ await testChain.catch(() => { failed = 1 })
167
+ if (failed !== 0) process.exit(1)
168
+ console.log('check-plugin-bridge-ui: ok')
@@ -456,7 +456,9 @@ test('the latest-wins gate drops tickets that are no longer newest (T15-fix 4)',
456
456
 
457
457
  test('deriveClientStatusView maps every probe state and tolerates garbage', () => {
458
458
  // T59: the view carries the device's own name ('' when the body had none).
459
- const bare = { state: 'unpaired', serverUrl: '', serverName: '', deviceName: '', nextRetryAt: undefined, lastError: '', intercept: undefined, compat: undefined }
459
+ // PB-5: the bridge rows ([] — the body carried none) and the rerouting
460
+ // note (undefined) ride the same tolerant derivation.
461
+ const bare = { state: 'unpaired', serverUrl: '', serverName: '', deviceName: '', nextRetryAt: undefined, lastError: '', intercept: undefined, compat: undefined, pluginBridge: [], pluginRouteIntercept: undefined }
460
462
  assert.deepEqual(deriveClientStatusView({ state: 'unpaired' }), bare)
461
463
  assert.deepEqual(deriveClientStatusView({ state: 'connected', serverUrl: 'http://192.168.1.10:3088' }), {
462
464
  ...bare,