@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.
Files changed (89) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +148 -77
  3. package/README.zh.md +32 -26
  4. package/lib/client.js +602 -67
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +667 -32
  7. package/lib/mobile.js +3 -1
  8. package/lib/mobile.js.map +1 -1
  9. package/lib/types/client/FenceNotice.d.ts +21 -0
  10. package/lib/types/client/FenceNotice.d.ts.map +1 -0
  11. package/lib/types/client/FenceNotice.js +10 -0
  12. package/lib/types/client/RemoteEntry.d.ts.map +1 -1
  13. package/lib/types/client/RemoteEntry.js +18 -11
  14. package/lib/types/client/RemotePanel.d.ts +10 -4
  15. package/lib/types/client/RemotePanel.d.ts.map +1 -1
  16. package/lib/types/client/RemotePanel.js +7 -6
  17. package/lib/types/client/RemoteSettingsCard.d.ts +4 -0
  18. package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
  19. package/lib/types/client/RemoteSettingsCard.js +3 -1
  20. package/lib/types/client/device-name.d.ts +3 -0
  21. package/lib/types/client/device-name.d.ts.map +1 -0
  22. package/lib/types/client/device-name.js +30 -0
  23. package/lib/types/client/index.d.ts.map +1 -1
  24. package/lib/types/client/index.js +48 -0
  25. package/lib/types/client/locales.d.ts +51 -1
  26. package/lib/types/client/locales.d.ts.map +1 -1
  27. package/lib/types/client/locales.js +79 -29
  28. package/lib/types/client/pair-api.d.ts +35 -0
  29. package/lib/types/client/pair-api.d.ts.map +1 -1
  30. package/lib/types/client/pair-api.js +39 -0
  31. package/lib/types/client/remote-channel.d.ts +93 -0
  32. package/lib/types/client/remote-channel.d.ts.map +1 -0
  33. package/lib/types/client/remote-channel.js +246 -0
  34. package/lib/types/dsh-home.d.ts +17 -0
  35. package/lib/types/dsh-home.d.ts.map +1 -0
  36. package/lib/types/dsh-home.js +34 -0
  37. package/lib/types/gate.d.ts.map +1 -1
  38. package/lib/types/gate.js +5 -2
  39. package/lib/types/index.d.ts +19 -12
  40. package/lib/types/index.d.ts.map +1 -1
  41. package/lib/types/index.js +73 -5
  42. package/lib/types/loopback-proxy.d.ts +28 -0
  43. package/lib/types/loopback-proxy.d.ts.map +1 -0
  44. package/lib/types/loopback-proxy.js +112 -0
  45. package/lib/types/mobile/views/SessionListView.d.ts.map +1 -1
  46. package/lib/types/mobile/views/SessionListView.js +6 -1
  47. package/lib/types/mobile-api.d.ts.map +1 -1
  48. package/lib/types/mobile-api.js +6 -2
  49. package/lib/types/mobile-routes.js +1 -1
  50. package/lib/types/pairing.d.ts +63 -11
  51. package/lib/types/pairing.d.ts.map +1 -1
  52. package/lib/types/pairing.js +137 -20
  53. package/lib/types/pairing.persist.test.js +60 -5
  54. package/lib/types/posture.d.ts +75 -0
  55. package/lib/types/posture.d.ts.map +1 -0
  56. package/lib/types/posture.js +116 -0
  57. package/lib/types/remote-api.d.ts +60 -0
  58. package/lib/types/remote-api.d.ts.map +1 -0
  59. package/lib/types/remote-api.js +181 -0
  60. package/lib/types/remote-methods.d.ts +33 -0
  61. package/lib/types/remote-methods.d.ts.map +1 -0
  62. package/lib/types/remote-methods.js +54 -0
  63. package/lib/types/routes.d.ts +4 -0
  64. package/lib/types/routes.d.ts.map +1 -1
  65. package/lib/types/routes.js +33 -4
  66. package/package.json +2 -2
  67. package/src/client/FenceNotice.tsx +43 -0
  68. package/src/client/RemoteEntry.tsx +19 -9
  69. package/src/client/RemotePanel.tsx +83 -10
  70. package/src/client/RemoteSettingsCard.tsx +16 -0
  71. package/src/client/device-name.ts +31 -0
  72. package/src/client/index.ts +46 -0
  73. package/src/client/locales.ts +79 -29
  74. package/src/client/pair-api.ts +60 -0
  75. package/src/client/remote-channel.ts +269 -0
  76. package/src/client/remote.module.css +240 -16
  77. package/src/dsh-home.ts +36 -0
  78. package/src/gate.ts +5 -2
  79. package/src/index.ts +89 -17
  80. package/src/loopback-proxy.ts +126 -0
  81. package/src/mobile/views/SessionListView.tsx +6 -1
  82. package/src/mobile-api.ts +6 -2
  83. package/src/mobile-routes.ts +1 -1
  84. package/src/pairing.persist.test.ts +64 -5
  85. package/src/pairing.ts +167 -20
  86. package/src/posture.ts +154 -0
  87. package/src/remote-api.ts +217 -0
  88. package/src/remote-methods.ts +62 -0
  89. 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,+BAqMlF"}
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, workspace)]);
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;AAwDlD,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"}
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"}
@@ -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; there is no per-device revocation yet.
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>移动端远程控制</title>',
38
+ '<title>远程访问</title>',
39
39
  '</head>',
40
40
  '<body>',
41
41
  '<div id="root"></div>',
@@ -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 (default), sessions stay memory-only — the previous behavior.
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 and
159
- * stop); `lastSeenAt` refreshes are deliberately not persisted here so a
160
- * per-request write storm is avoided — after a restart the phone's first
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: re-evaluate the derived snapshot (a device aging past
227
- * the offline window flips the phase to disconnected). Emits only when
228
- * the snapshot actually changed.
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":"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"}
1
+ {"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../../src/pairing.ts"],"names":[],"mappings":"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"}