@linxin666/dsh-remote-web-ui 0.2.4 → 0.2.5
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/README.i18n.yaml +2 -2
- package/README.md +148 -77
- package/README.zh.md +32 -26
- package/lib/client.js +602 -67
- package/lib/client.js.map +1 -1
- package/lib/index.js +667 -32
- package/lib/mobile.js +3 -1
- package/lib/mobile.js.map +1 -1
- package/lib/types/client/FenceNotice.d.ts +21 -0
- package/lib/types/client/FenceNotice.d.ts.map +1 -0
- package/lib/types/client/FenceNotice.js +10 -0
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +18 -11
- package/lib/types/client/RemotePanel.d.ts +10 -4
- package/lib/types/client/RemotePanel.d.ts.map +1 -1
- package/lib/types/client/RemotePanel.js +7 -6
- package/lib/types/client/RemoteSettingsCard.d.ts +4 -0
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +3 -1
- package/lib/types/client/device-name.d.ts +3 -0
- package/lib/types/client/device-name.d.ts.map +1 -0
- package/lib/types/client/device-name.js +30 -0
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +48 -0
- package/lib/types/client/locales.d.ts +51 -1
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +79 -29
- package/lib/types/client/pair-api.d.ts +35 -0
- package/lib/types/client/pair-api.d.ts.map +1 -1
- package/lib/types/client/pair-api.js +39 -0
- package/lib/types/client/remote-channel.d.ts +93 -0
- package/lib/types/client/remote-channel.d.ts.map +1 -0
- package/lib/types/client/remote-channel.js +246 -0
- package/lib/types/dsh-home.d.ts +17 -0
- package/lib/types/dsh-home.d.ts.map +1 -0
- package/lib/types/dsh-home.js +34 -0
- package/lib/types/gate.d.ts.map +1 -1
- package/lib/types/gate.js +5 -2
- package/lib/types/index.d.ts +19 -12
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +73 -5
- package/lib/types/loopback-proxy.d.ts +28 -0
- package/lib/types/loopback-proxy.d.ts.map +1 -0
- package/lib/types/loopback-proxy.js +112 -0
- package/lib/types/mobile/views/SessionListView.d.ts.map +1 -1
- package/lib/types/mobile/views/SessionListView.js +6 -1
- package/lib/types/mobile-api.d.ts.map +1 -1
- package/lib/types/mobile-api.js +6 -2
- package/lib/types/mobile-routes.js +1 -1
- package/lib/types/pairing.d.ts +63 -11
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +137 -20
- package/lib/types/pairing.persist.test.js +60 -5
- package/lib/types/posture.d.ts +75 -0
- package/lib/types/posture.d.ts.map +1 -0
- package/lib/types/posture.js +116 -0
- package/lib/types/remote-api.d.ts +60 -0
- package/lib/types/remote-api.d.ts.map +1 -0
- package/lib/types/remote-api.js +181 -0
- package/lib/types/remote-methods.d.ts +33 -0
- package/lib/types/remote-methods.d.ts.map +1 -0
- package/lib/types/remote-methods.js +54 -0
- package/lib/types/routes.d.ts +4 -0
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +33 -4
- package/package.json +2 -2
- package/src/client/FenceNotice.tsx +43 -0
- package/src/client/RemoteEntry.tsx +19 -9
- package/src/client/RemotePanel.tsx +83 -10
- package/src/client/RemoteSettingsCard.tsx +16 -0
- package/src/client/device-name.ts +31 -0
- package/src/client/index.ts +46 -0
- package/src/client/locales.ts +79 -29
- package/src/client/pair-api.ts +60 -0
- package/src/client/remote-channel.ts +269 -0
- package/src/client/remote.module.css +240 -16
- package/src/dsh-home.ts +36 -0
- package/src/gate.ts +5 -2
- package/src/index.ts +89 -17
- package/src/loopback-proxy.ts +126 -0
- package/src/mobile/views/SessionListView.tsx +6 -1
- package/src/mobile-api.ts +6 -2
- package/src/mobile-routes.ts +1 -1
- package/src/pairing.persist.test.ts +64 -5
- package/src/pairing.ts +167 -20
- package/src/posture.ts +154 -0
- package/src/remote-api.ts +217 -0
- package/src/remote-methods.ts +62 -0
- package/src/routes.ts +33 -4
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loopback-shaped reverse proxy used by the remote desktop channel: after
|
|
3
|
+
* the pairing cookie gate, traffic is re-issued to 127.0.0.1 so sibling
|
|
4
|
+
* plugin fences (socket + Host loopback) accept it. Origin, cookies, and
|
|
5
|
+
* caller-controlled Sec-Fetch markers are dropped. HTTP requests receive a
|
|
6
|
+
* synthetic same-origin marker after the pairing gate so sibling loopback
|
|
7
|
+
* routes that require a browser tripwire accept the authenticated proxy.
|
|
8
|
+
*/
|
|
9
|
+
import { request as httpRequest } from 'node:http';
|
|
10
|
+
import { connect } from 'node:net';
|
|
11
|
+
import { writeJson } from './http.js';
|
|
12
|
+
/** WebSocket handshake headers forwarded to the loopback upstream. */
|
|
13
|
+
const WS_FORWARD_HEADERS = [
|
|
14
|
+
'sec-websocket-key',
|
|
15
|
+
'sec-websocket-version',
|
|
16
|
+
'sec-websocket-extensions',
|
|
17
|
+
'sec-websocket-protocol',
|
|
18
|
+
];
|
|
19
|
+
/** Response headers copied from the loopback upstream (no hop-by-hop). */
|
|
20
|
+
const HTTP_FORWARD_RESPONSE_HEADERS = [
|
|
21
|
+
'content-type',
|
|
22
|
+
'content-length',
|
|
23
|
+
'content-disposition',
|
|
24
|
+
'cache-control',
|
|
25
|
+
'etag',
|
|
26
|
+
'last-modified',
|
|
27
|
+
];
|
|
28
|
+
/**
|
|
29
|
+
* Pipe one HTTP request to loopback and stream the response back.
|
|
30
|
+
* @param req - the already-gated outer request.
|
|
31
|
+
* @param res - the outer response.
|
|
32
|
+
* @param port - local webServer port.
|
|
33
|
+
* @param upstreamPath - path + query on 127.0.0.1 (must start with `/`).
|
|
34
|
+
*/
|
|
35
|
+
export function proxyLoopbackHttp(req, res, port, upstreamPath) {
|
|
36
|
+
const headers = {
|
|
37
|
+
host: `127.0.0.1:${String(port)}`,
|
|
38
|
+
'sec-fetch-site': 'same-origin',
|
|
39
|
+
};
|
|
40
|
+
const contentType = req.headers['content-type'];
|
|
41
|
+
if (typeof contentType === 'string')
|
|
42
|
+
headers['content-type'] = contentType;
|
|
43
|
+
const contentLength = req.headers['content-length'];
|
|
44
|
+
if (typeof contentLength === 'string')
|
|
45
|
+
headers['content-length'] = contentLength;
|
|
46
|
+
const accept = req.headers.accept;
|
|
47
|
+
if (typeof accept === 'string')
|
|
48
|
+
headers.accept = accept;
|
|
49
|
+
const upstream = httpRequest({
|
|
50
|
+
host: '127.0.0.1',
|
|
51
|
+
port,
|
|
52
|
+
path: upstreamPath,
|
|
53
|
+
method: req.method,
|
|
54
|
+
headers,
|
|
55
|
+
}, (upstreamRes) => {
|
|
56
|
+
const out = {};
|
|
57
|
+
for (const name of HTTP_FORWARD_RESPONSE_HEADERS) {
|
|
58
|
+
const value = upstreamRes.headers[name];
|
|
59
|
+
if (value !== undefined)
|
|
60
|
+
out[name] = value;
|
|
61
|
+
}
|
|
62
|
+
res.writeHead(upstreamRes.statusCode ?? 502, out);
|
|
63
|
+
upstreamRes.pipe(res);
|
|
64
|
+
});
|
|
65
|
+
upstream.on('error', () => {
|
|
66
|
+
if (!res.headersSent) {
|
|
67
|
+
writeJson(res, 502, { ok: false, error: { code: 'upstream-failure', message: 'upstream request failed' } });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
res.destroy();
|
|
71
|
+
});
|
|
72
|
+
req.pipe(upstream);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Rebuild a WebSocket handshake as loopback-shaped and pipe both directions.
|
|
76
|
+
* @param req - the already-gated upgrade request.
|
|
77
|
+
* @param socket - the client duplex.
|
|
78
|
+
* @param head - bytes already read past the handshake.
|
|
79
|
+
* @param port - local webServer port.
|
|
80
|
+
* @param upstreamPath - path + query on 127.0.0.1.
|
|
81
|
+
*/
|
|
82
|
+
export function proxyLoopbackUpgrade(req, socket, head, port, upstreamPath) {
|
|
83
|
+
const lines = [
|
|
84
|
+
`GET ${upstreamPath} HTTP/1.1`,
|
|
85
|
+
`Host: 127.0.0.1:${String(port)}`,
|
|
86
|
+
'Upgrade: websocket',
|
|
87
|
+
'Connection: Upgrade',
|
|
88
|
+
];
|
|
89
|
+
for (const name of WS_FORWARD_HEADERS) {
|
|
90
|
+
const value = req.headers[name];
|
|
91
|
+
if (value === undefined)
|
|
92
|
+
continue;
|
|
93
|
+
lines.push(`${name}: ${Array.isArray(value) ? value.join(', ') : value}`);
|
|
94
|
+
}
|
|
95
|
+
const handshake = `${lines.join('\r\n')}\r\n\r\n`;
|
|
96
|
+
const upstream = connect(port, '127.0.0.1');
|
|
97
|
+
const tearDown = () => {
|
|
98
|
+
upstream.destroy();
|
|
99
|
+
socket.destroy();
|
|
100
|
+
};
|
|
101
|
+
upstream.on('error', tearDown);
|
|
102
|
+
socket.on('error', tearDown);
|
|
103
|
+
upstream.on('close', () => { socket.destroy(); });
|
|
104
|
+
socket.on('close', () => { upstream.destroy(); });
|
|
105
|
+
upstream.on('connect', () => {
|
|
106
|
+
upstream.write(handshake);
|
|
107
|
+
if (head.length > 0)
|
|
108
|
+
upstream.write(head);
|
|
109
|
+
socket.pipe(upstream);
|
|
110
|
+
upstream.pipe(socket);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SessionListView.d.ts","sourceRoot":"","sources":["../../../../src/mobile/views/SessionListView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,aAAa,IAAI,YAAY,EAAE,MAAM,8CAA8C,CAAA;AAIjG,OAAO,EAAuD,KAAK,WAAW,EAAE,MAAM,WAAW,CAAA;AAGjG,kCAAkC;AAClC,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,YAAY,CAAA;IACvB,MAAM,IAAI,IAAI,CAAA;IACd,MAAM,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAAA;CACnC;AAUD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,oBAAoB,+
|
|
1
|
+
{"version":3,"file":"SessionListView.d.ts","sourceRoot":"","sources":["../../../../src/mobile/views/SessionListView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,aAAa,IAAI,YAAY,EAAE,MAAM,8CAA8C,CAAA;AAIjG,OAAO,EAAuD,KAAK,WAAW,EAAE,MAAM,WAAW,CAAA;AAGjG,kCAAkC;AAClC,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,YAAY,CAAA;IACvB,MAAM,IAAI,IAAI,CAAA;IACd,MAAM,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAAA;CACnC;AAUD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,oBAAoB,+BA0MlF"}
|
|
@@ -39,6 +39,10 @@ export function SessionListView({ workspace, onBack, onPick }) {
|
|
|
39
39
|
const [presetsLoading, setPresetsLoading] = useState(true);
|
|
40
40
|
const cursorRef = useRef(undefined);
|
|
41
41
|
const busyRef = useRef(false);
|
|
42
|
+
// The freshest owned-id set: the mount effect re-reads the workspace roster
|
|
43
|
+
// so a session created since this row was captured still shows; loadMore
|
|
44
|
+
// filters later pages through this ref instead of the stale prop.
|
|
45
|
+
const workspaceRef = useRef(workspace);
|
|
42
46
|
// First page on mount. The workspace roster is re-read alongside so the
|
|
43
47
|
// owned-id set is fresh: a session created (or attached) since this
|
|
44
48
|
// workspace row was captured must not vanish from the filter.
|
|
@@ -51,6 +55,7 @@ export function SessionListView({ workspace, onBack, onPick }) {
|
|
|
51
55
|
return;
|
|
52
56
|
const fresh = workspaces.find(candidate => candidate.workspaceId === workspace.workspaceId);
|
|
53
57
|
const current = fresh ?? workspace;
|
|
58
|
+
workspaceRef.current = current;
|
|
54
59
|
setRows(ownedItems(page.items, current));
|
|
55
60
|
cursorRef.current = page.nextCursor;
|
|
56
61
|
setHasMore(page.hasMore);
|
|
@@ -96,7 +101,7 @@ export function SessionListView({ workspace, onBack, onPick }) {
|
|
|
96
101
|
setLoading(false);
|
|
97
102
|
cursorRef.current = page.nextCursor;
|
|
98
103
|
setHasMore(page.hasMore);
|
|
99
|
-
setRows(previous => [...previous, ...ownedItems(page.items,
|
|
104
|
+
setRows(previous => [...previous, ...ownedItems(page.items, workspaceRef.current)]);
|
|
100
105
|
}, (reason) => {
|
|
101
106
|
busyRef.current = false;
|
|
102
107
|
setLoading(false);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mobile-api.d.ts","sourceRoot":"","sources":["../../src/mobile-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAA;AAG9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"mobile-api.d.ts","sourceRoot":"","sources":["../../src/mobile-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAA;AAG9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAyDlD,iCAAiC;AACjC,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,OAAO,EAAE,cAAc,CAAA;IACvB,0DAA0D;IAC1D,QAAQ,EAAE,QAAQ,CAAA;IAClB,kEAAkE;IAClE,iBAAiB,EAAE,MAAM,OAAO,CAAA;IAChC,oFAAoF;IACpF,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED,8BAA8B;AAC9B,eAAO,MAAM,gBAAgB;aAC3B,MAAM,EAAE,mBAAmB;CACnB,CAAA;AAOV;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,QAAQ,EAAE,CA4InE"}
|
package/lib/types/mobile-api.js
CHANGED
|
@@ -26,7 +26,8 @@ import { readCookie } from './gate.js';
|
|
|
26
26
|
* note the paired-device cookie also passes the global api/gate for the full
|
|
27
27
|
* ApiProxy surface (gate.ts), so a paired phone is a full-control credential:
|
|
28
28
|
* the allowlist only constrains this /m/api proxy, not the cookie's reach.
|
|
29
|
-
* stop() revokes every device;
|
|
29
|
+
* stop() revokes every device; the loopback panel can also revoke one
|
|
30
|
+
* device at a time.
|
|
30
31
|
*/
|
|
31
32
|
const MOBILE_ALLOWLIST = new Set([
|
|
32
33
|
'workspace.list',
|
|
@@ -238,8 +239,11 @@ async function dispatch(apiProxy, method, payload, rpcId, signal) {
|
|
|
238
239
|
const request = { rpcId: RpcId(rpcId), payload };
|
|
239
240
|
if (method === 'session.list') {
|
|
240
241
|
const full = await apiProxy.sessions.list(request);
|
|
242
|
+
// The error path must carry the same 'server-response' envelope the
|
|
243
|
+
// success path builds, or the phone's callUnary throws a transport error
|
|
244
|
+
// and masks the real business error.
|
|
241
245
|
if (!full.result.ok)
|
|
242
|
-
return full;
|
|
246
|
+
return { type: 'server-response', rpcId, result: full.result };
|
|
243
247
|
const items = full.result.value.items;
|
|
244
248
|
const cursor = payload?.cursor;
|
|
245
249
|
// Every call pages (the first call with no cursor IS the first page):
|
|
@@ -35,7 +35,7 @@ function pageHtml(bundleUrl) {
|
|
|
35
35
|
'<meta name="apple-mobile-web-app-title" content="DSH Remote">',
|
|
36
36
|
'<link rel="manifest" href="/m/manifest.webmanifest">',
|
|
37
37
|
'<link rel="apple-touch-icon" href="/m/apple-touch-icon.png">',
|
|
38
|
-
'<title
|
|
38
|
+
'<title>远程访问</title>',
|
|
39
39
|
'</head>',
|
|
40
40
|
'<body>',
|
|
41
41
|
'<div id="root"></div>',
|
package/lib/types/pairing.d.ts
CHANGED
|
@@ -13,7 +13,10 @@
|
|
|
13
13
|
* unknown one (no oracle for validity).
|
|
14
14
|
* - `stop()` revokes every device session and clears the token, so paired
|
|
15
15
|
* devices are cut off on their next gated request.
|
|
16
|
+
* - `revoke()` drops one device session; idle sessions older than
|
|
17
|
+
* `idleExpireMs` are deleted on sweep, load, and the next gated request.
|
|
16
18
|
*/
|
|
19
|
+
import type { PostureSnapshot } from './posture.ts';
|
|
17
20
|
/** The observable pairing phases the panel renders. */
|
|
18
21
|
export type PairingPhase =
|
|
19
22
|
/** The server is not bound all-interfaces: no usable QR exists. */
|
|
@@ -41,12 +44,29 @@ export interface TokenRecord {
|
|
|
41
44
|
/** LAN IP literal the QR link was built from (optional; default first). */
|
|
42
45
|
address?: string;
|
|
43
46
|
}
|
|
47
|
+
/** Default idle-expiry window: 7 days without heartbeat or a gated request. */
|
|
48
|
+
export declare const DEFAULT_IDLE_EXPIRE_MS: number;
|
|
44
49
|
/** One paired device session, keyed by the device id stored in its cookie. */
|
|
45
50
|
export interface DeviceSession {
|
|
46
51
|
/** Pairing time (ms epoch). */
|
|
47
52
|
createdAt: number;
|
|
48
53
|
/** Last time the device passed a gated request or heartbeat. */
|
|
49
54
|
lastSeenAt: number;
|
|
55
|
+
/** Sanitized User-Agent captured at accept (optional; missing on old files). */
|
|
56
|
+
userAgent?: string;
|
|
57
|
+
}
|
|
58
|
+
/** One device row in a loopback snapshot (ids are session credentials). */
|
|
59
|
+
export interface DeviceSnapshot {
|
|
60
|
+
/** Cookie value / session credential of this device. */
|
|
61
|
+
id: string;
|
|
62
|
+
/** Pairing time (ms epoch). */
|
|
63
|
+
createdAt: number;
|
|
64
|
+
/** Last heartbeat or gated request (ms epoch). */
|
|
65
|
+
lastSeenAt: number;
|
|
66
|
+
/** Whether lastSeenAt is within the offline window. */
|
|
67
|
+
online: boolean;
|
|
68
|
+
/** Sanitized User-Agent captured at accept, when known. */
|
|
69
|
+
userAgent?: string;
|
|
50
70
|
}
|
|
51
71
|
/** One tunnel status frame (auto-tunnel only; undefined when disabled). */
|
|
52
72
|
export interface TunnelStatus {
|
|
@@ -68,6 +88,8 @@ export interface PairingSnapshot {
|
|
|
68
88
|
publicUrl?: string;
|
|
69
89
|
/** Auto-tunnel status, while the auto-tunnel feature is active. */
|
|
70
90
|
tunnel?: TunnelStatus;
|
|
91
|
+
/** Latest /api posture probe (undefined until the first round completes). */
|
|
92
|
+
posture?: PostureSnapshot;
|
|
71
93
|
/** Opaque (non-secret) id of the active token (undefined when stopped/lan-required). */
|
|
72
94
|
tokenId?: string;
|
|
73
95
|
/** Absolute expiry of the active token. */
|
|
@@ -76,6 +98,8 @@ export interface PairingSnapshot {
|
|
|
76
98
|
deviceCount: number;
|
|
77
99
|
/** Count of devices active within the offline window. */
|
|
78
100
|
onlineCount: number;
|
|
101
|
+
/** Per-device roster for the loopback panel (never sent on /api/pair/status). */
|
|
102
|
+
devices: DeviceSnapshot[];
|
|
79
103
|
}
|
|
80
104
|
/** Service tunables (config-validated upstream; plain numbers here). */
|
|
81
105
|
export interface PairingConfig {
|
|
@@ -87,11 +111,16 @@ export interface PairingConfig {
|
|
|
87
111
|
maxDevices: number;
|
|
88
112
|
/** Cookie name carrying the device id. */
|
|
89
113
|
cookieName: string;
|
|
114
|
+
/**
|
|
115
|
+
* Idle sessions older than this are deleted (memory and disk). Defaults
|
|
116
|
+
* to {@link DEFAULT_IDLE_EXPIRE_MS} when omitted.
|
|
117
|
+
*/
|
|
118
|
+
idleExpireMs?: number;
|
|
90
119
|
/**
|
|
91
120
|
* Path to a JSON file where paired device sessions are persisted. When
|
|
92
121
|
* set, sessions survive process restarts (the phone keeps its 365-day
|
|
93
122
|
* cookie), so re-pairing after a dsh web restart is not required. When
|
|
94
|
-
* unset
|
|
123
|
+
* unset, sessions stay memory-only.
|
|
95
124
|
*/
|
|
96
125
|
devicesFile?: string;
|
|
97
126
|
}
|
|
@@ -138,6 +167,9 @@ export declare class PairingService {
|
|
|
138
167
|
private publicBase;
|
|
139
168
|
/** Auto-tunnel status, while the auto-tunnel feature is active. */
|
|
140
169
|
private tunnelStatus;
|
|
170
|
+
private posture;
|
|
171
|
+
/** True when lastSeenAt changed since the last persist (flushed on sweep). */
|
|
172
|
+
private dirty;
|
|
141
173
|
/**
|
|
142
174
|
* @param config - tunables. The settings surface replaces the object (a
|
|
143
175
|
* fresh literal) when a committed section changes; every operation reads
|
|
@@ -153,12 +185,13 @@ export declare class PairingService {
|
|
|
153
185
|
private loadPersisted;
|
|
154
186
|
/** FIFO-cap the device table (a persisted file may outlive a lowered cap). */
|
|
155
187
|
private clampToMaxDevices;
|
|
188
|
+
/** Drop sessions whose lastSeenAt is older than idleExpireMs. */
|
|
189
|
+
private evictIdle;
|
|
156
190
|
/**
|
|
157
191
|
* Write the current device table to the configured file. Called on the
|
|
158
|
-
* mutation boundaries that change the set of live sessions (accept
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
* heartbeat re-warms its own session.
|
|
192
|
+
* mutation boundaries that change the set of live sessions (accept, stop,
|
|
193
|
+
* revoke, idle eviction) and, throttled, from sweep() so lastSeenAt
|
|
194
|
+
* survives a restart without a write per request.
|
|
162
195
|
*
|
|
163
196
|
* Device ids are session credentials (the gate authorizes requests by the
|
|
164
197
|
* cookie's device id), so the file is written 0600 via a temp file and
|
|
@@ -182,6 +215,8 @@ export declare class PairingService {
|
|
|
182
215
|
setPublicBaseUrl(url: string | undefined): void;
|
|
183
216
|
/** Set or clear the auto-tunnel status frame (undefined when the feature is off). */
|
|
184
217
|
setTunnelStatus(status: TunnelStatus | undefined): void;
|
|
218
|
+
/** Set the latest /api posture probe result (see posture.ts). */
|
|
219
|
+
setPosture(snapshot: PostureSnapshot | undefined): void;
|
|
185
220
|
/**
|
|
186
221
|
* Issue a fresh token, replacing (invalidating) any previous one. A
|
|
187
222
|
* stopped service re-arms through this call (the panel's refresh button).
|
|
@@ -203,19 +238,27 @@ export declare class PairingService {
|
|
|
203
238
|
* successful call for the same token is impossible because the first
|
|
204
239
|
* consumes it.
|
|
205
240
|
* @param token - the token secret from the QR link.
|
|
241
|
+
* @param userAgent - optional User-Agent header captured at accept.
|
|
206
242
|
* @returns the new device id, or a refusal code.
|
|
207
243
|
*/
|
|
208
|
-
accept(token: string): AcceptResult;
|
|
244
|
+
accept(token: string, userAgent?: string): AcceptResult;
|
|
209
245
|
/**
|
|
210
246
|
* Stop remote control: revoke every device session and clear the token.
|
|
211
247
|
* The phone's next gated /api request 403s; the panel falls back to
|
|
212
248
|
* stopped until a fresh QR is issued.
|
|
213
249
|
*/
|
|
214
250
|
stop(): void;
|
|
251
|
+
/**
|
|
252
|
+
* Revoke one paired device. The next gated request from that cookie is
|
|
253
|
+
* refused; other sessions stay live. Unknown ids are a no-op.
|
|
254
|
+
* @param deviceId - the cookie value of the device to drop.
|
|
255
|
+
* @returns true when a live session was removed.
|
|
256
|
+
*/
|
|
257
|
+
revoke(deviceId: string): boolean;
|
|
215
258
|
/**
|
|
216
259
|
* The api/gate path: record activity for a device id and report whether
|
|
217
260
|
* the request may proceed. Unknown or revoked ids (including any device
|
|
218
|
-
* after stop()) are refused.
|
|
261
|
+
* after stop() or idle expiry) are refused.
|
|
219
262
|
* @param deviceId - the cookie value of the requesting device.
|
|
220
263
|
* @returns true when the device session is live and was refreshed.
|
|
221
264
|
*/
|
|
@@ -223,20 +266,29 @@ export declare class PairingService {
|
|
|
223
266
|
/** Explicit presence heartbeat (the phone's client sends these). */
|
|
224
267
|
heartbeat(deviceId: string): boolean;
|
|
225
268
|
/**
|
|
226
|
-
* Periodic sweep:
|
|
227
|
-
* the
|
|
228
|
-
* the
|
|
269
|
+
* Periodic sweep: drop idle sessions, flush a dirty lastSeenAt, and
|
|
270
|
+
* re-evaluate the derived snapshot (a device aging past the offline
|
|
271
|
+
* window flips the phase to disconnected). Emits only when the snapshot
|
|
272
|
+
* actually changed.
|
|
229
273
|
*/
|
|
230
274
|
sweep(): void;
|
|
231
275
|
/** The current snapshot (fresh object per call — stable between emits). */
|
|
232
276
|
snapshot(): PairingSnapshot;
|
|
233
|
-
/** Whether a cookie value names a currently live device session. */
|
|
277
|
+
/** Whether a cookie value names a currently live (non-idle) device session. */
|
|
234
278
|
hasDevice(deviceId: string): boolean;
|
|
235
279
|
/** Subscribe to snapshot changes (each emit passes a fresh snapshot). */
|
|
236
280
|
onState(listener: (snapshot: PairingSnapshot) => void): () => void;
|
|
237
281
|
private activeToken;
|
|
282
|
+
/**
|
|
283
|
+
* Return a live session, deleting it first when idle-expired. Side-effecting
|
|
284
|
+
* so a stale cookie cannot pass the gate between sweeps.
|
|
285
|
+
*/
|
|
286
|
+
private liveSession;
|
|
287
|
+
private toDeviceSnapshot;
|
|
238
288
|
private derivePhase;
|
|
239
289
|
private isOnlineAt;
|
|
240
290
|
private notify;
|
|
241
291
|
}
|
|
292
|
+
/** Strip control characters and cap the User-Agent stored with a session. */
|
|
293
|
+
export declare function sanitizeUserAgent(raw: string | undefined): string | undefined;
|
|
242
294
|
//# sourceMappingURL=pairing.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../../src/pairing.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../../src/pairing.ts"],"names":[],"mappings":"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"}
|