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.
- 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/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 +198 -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 +246 -0
- 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/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 +51 -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/adapters/dsh-better-sidebar/index.ts +144 -0
- package/src/adapters/dsh-better-sidebar/methods.ts +215 -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 +252 -0
- 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
|
@@ -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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAKH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"relay-client.d.ts","sourceRoot":"","sources":["../../src/relay-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAKH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAC1D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAsF3D;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,0BAA0B;IAC1B,GAAG,IAAI,MAAM,CAAA;IACb,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAA;IAC/C,YAAY,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAA;IACnC,uDAAuD;IACvD,MAAM,IAAI,MAAM,CAAA;CACjB;AAED;;;;GAIG;AACH,eAAO,MAAM,YAAY,EAAE,UAS1B,CAAA;AAED;0BAC0B;AAC1B,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,YAAY,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,cAAc,CAAA;AAEtG,oEAAoE;AACpE,MAAM,WAAW,cAAc;IAC7B,aAAa,EAAE,MAAM,CAAA;IACrB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACpC;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAA;CAC5C;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,UAAW,SAAQ,KAAK;IACnC,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;4DAEwD;IACxD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;gBAErB,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAQpH;AAcD,MAAM,WAAW,wBAAwB;IACvC;kDAC8C;IAC9C,YAAY,EAAE,MAAM,MAAM,GAAG,SAAS,CAAA;IACtC;mBACe;IACf,QAAQ,EAAE,MAAM,MAAM,GAAG,SAAS,CAAA;IAClC,oDAAoD;IACpD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACxB;6EACyE;IACzE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;4DACwD;IACxD,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB;iEAC6D;IAC7D,KAAK,CAAC,EAAE,UAAU,CAAA;IAClB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;gFAI4E;IAC5E,sBAAsB,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACxF;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAA;IAC1B,kEAAkE;IAClE,QAAQ,CAAC,aAAa,EAAE,cAAc,GAAG,SAAS,CAAA;IAClD;;;;eAIW;IACX,QAAQ,CAAC,mBAAmB,EAAE,MAAM,GAAG,SAAS,CAAA;IAChD;;0DAEsD;IACtD,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IACnC;;qDAEiD;IACjD,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAA;IACtC;;;;;8CAK0C;IAC1C,QAAQ,CAAC,MAAM,EAAE,kBAAkB,GAAG,SAAS,CAAA;IAC/C;;0EAEsE;IACtE,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,GAAG,MAAM,IAAI,CAAA;IAC5D;;;;;kDAK8C;IAC9C,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnC;;;wEAGoE;IACpE,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAA;IACnC,uEAAuE;IACvE,OAAO,IAAI,OAAO,CAAC,cAAc,CAAC,CAAA;IAClC;;iEAE6D;IAC7D,SAAS,IAAI,OAAO,CAAA;IACpB;;;2DAGuD;IACvD,kBAAkB,IAAI,IAAI,CAAA;IAC1B;kEAC8D;IAC9D,IAAI,IAAI,IAAI,CAAA;IACZ;mFAC+E;IAC/E,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAChG;;;;;8BAK0B;IAC1B,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/D;;;;iEAI6D;IAC7D,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IACzF;;;gDAG4C;IAC5C,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC,OAAO,CAAC,CAAA;IAC1G;;;;;0CAKsC;IACtC,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,eAAe,CAAC,CAAA;IAClF;;;;;;;;gEAQ4D;IAC5D,MAAM,CAAC,OAAO,EAAE,kBAAkB,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,eAAe,CAAC,CAAA;IACnF;;;;;;;;;sDASkD;IAClD,aAAa,CAAC,OAAO,EAAE,kBAAkB,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;IACnF;;;;;;;oCAOgC;IAChC,gBAAgB,CACd,OAAO,EAAE,kBAAkB,GAAG;QAC5B,SAAS,EAAE;YAAE,mBAAmB,EAAE,MAAM,CAAC;YAAC,uBAAuB,EAAE,MAAM,CAAC;YAAC,wBAAwB,CAAC,EAAE,MAAM,CAAC;YAAC,0BAA0B,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;QACnJ,IAAI,CAAC,EAAE,MAAM,CAAA;KACd,EACD,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,aAAa,CAAC,UAAU,CAAC,CAAA;KAAE,CAAC,CAAA;IACjE;;;oBAGgB;IAChB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACrG,mCAAmC;IACnC,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACvE;AAED;;6BAE6B;AAC7B,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAA;IACZ;wBACoB;IACpB,KAAK,EAAE,MAAM,CAAA;IACb;0EACsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;0EAEsE;IACtE,IAAI,CAAC,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,IAAI,CAAA;IACxC,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;8EAC0E;IAC1E,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,+DAA+D;AAC/D,MAAM,WAAW,kBAAkB;IACjC,6EAA6E;IAC7E,SAAS,EAAE,MAAM,CAAA;IACjB,mEAAmE;IACnE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;iBAEa;IACb,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,IAAI,CAAA;IACvC;kEAC8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,2EAA2E;AAC3E,MAAM,WAAW,eAAe;IAC9B,iDAAiD;IACjD,MAAM,EAAE,MAAM,CAAA;IACd,gEAAgE;IAChE,WAAW,EAAE,MAAM,GAAG,SAAS,CAAA;IAC/B;gCAC4B;IAC5B,IAAI,EAAE,MAAM,CAAA;CACb;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/E;AAMD;;;;;;GAMG;AACH,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAEvE;AAoED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,WAAW,CA21ChF"}
|
|
@@ -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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;
|
|
1
|
+
{"version":3,"file":"relay-server.d.ts","sourceRoot":"","sources":["../../src/relay-server.ts"],"names":[],"mappings":"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"}
|
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
|
+
"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
|
-
|
|
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,
|