@linxin666/dsh-remote-web-ui 0.3.23 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +13 -10
  3. package/README.zh.md +13 -10
  4. package/icon.svg +3 -0
  5. package/lib/client.js +396 -132
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +604 -262
  8. package/lib/types/client/FenceNotice.d.ts.map +1 -1
  9. package/lib/types/client/FenceNotice.js +0 -3
  10. package/lib/types/client/PluginSettingsCard.d.ts +4 -0
  11. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  12. package/lib/types/client/PluginSettingsCard.js +3 -2
  13. package/lib/types/client/RemoteEntry.d.ts.map +1 -1
  14. package/lib/types/client/RemoteEntry.js +14 -9
  15. package/lib/types/client/RemotePanel.d.ts.map +1 -1
  16. package/lib/types/client/RemotePanel.js +13 -4
  17. package/lib/types/client/RemoteSettingsCard.d.ts +6 -6
  18. package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
  19. package/lib/types/client/RemoteSettingsCard.js +14 -8
  20. package/lib/types/client/UpdateEntry.js +2 -2
  21. package/lib/types/client/UpdatePanel.js +2 -2
  22. package/lib/types/client/deep-link.js +2 -2
  23. package/lib/types/client/index.d.ts +16 -8
  24. package/lib/types/client/index.d.ts.map +1 -1
  25. package/lib/types/client/index.js +56 -32
  26. package/lib/types/client/locales.d.ts +0 -2
  27. package/lib/types/client/locales.d.ts.map +1 -1
  28. package/lib/types/client/locales.js +6 -8
  29. package/lib/types/client/mobile-adapt.d.ts.map +1 -1
  30. package/lib/types/client/mobile-adapt.js +143 -28
  31. package/lib/types/client/pair-api.d.ts +32 -3
  32. package/lib/types/client/pair-api.d.ts.map +1 -1
  33. package/lib/types/client/pair-api.js +44 -6
  34. package/lib/types/client/plugin-card-seat.d.ts +104 -0
  35. package/lib/types/client/plugin-card-seat.d.ts.map +1 -0
  36. package/lib/types/client/plugin-card-seat.js +143 -0
  37. package/lib/types/client/remote-channel.d.ts.map +1 -1
  38. package/lib/types/client/remote-channel.js +18 -5
  39. package/lib/types/client/settings-form.d.ts +17 -17
  40. package/lib/types/client/settings-form.d.ts.map +1 -1
  41. package/lib/types/client/settings-form.js +30 -27
  42. package/lib/types/console-output.d.ts +41 -0
  43. package/lib/types/console-output.d.ts.map +1 -0
  44. package/lib/types/console-output.js +89 -0
  45. package/lib/types/http.d.ts +18 -1
  46. package/lib/types/http.d.ts.map +1 -1
  47. package/lib/types/http.js +22 -1
  48. package/lib/types/index.d.ts +19 -11
  49. package/lib/types/index.d.ts.map +1 -1
  50. package/lib/types/index.js +91 -76
  51. package/lib/types/inner-auth.d.ts +3 -2
  52. package/lib/types/inner-auth.d.ts.map +1 -1
  53. package/lib/types/inner-auth.js +3 -2
  54. package/lib/types/pair-grant.d.ts +66 -0
  55. package/lib/types/pair-grant.d.ts.map +1 -0
  56. package/lib/types/pair-grant.js +95 -0
  57. package/lib/types/pairing.d.ts +44 -12
  58. package/lib/types/pairing.d.ts.map +1 -1
  59. package/lib/types/pairing.js +69 -20
  60. package/lib/types/pairing.persist.test.js +26 -0
  61. package/lib/types/posture.d.ts +11 -3
  62. package/lib/types/posture.d.ts.map +1 -1
  63. package/lib/types/posture.js +8 -3
  64. package/lib/types/remote-api.d.ts +10 -13
  65. package/lib/types/remote-api.d.ts.map +1 -1
  66. package/lib/types/remote-api.js +34 -34
  67. package/lib/types/remote-channel-boot.d.ts +6 -1
  68. package/lib/types/remote-channel-boot.d.ts.map +1 -1
  69. package/lib/types/remote-channel-boot.js +16 -8
  70. package/lib/types/remote-channel-rules.d.ts +14 -0
  71. package/lib/types/remote-channel-rules.d.ts.map +1 -1
  72. package/lib/types/remote-channel-rules.js +8 -0
  73. package/lib/types/remote-methods.d.ts +1 -1
  74. package/lib/types/remote-methods.d.ts.map +1 -1
  75. package/lib/types/remote-methods.js +1 -0
  76. package/lib/types/routes.d.ts +47 -5
  77. package/lib/types/routes.d.ts.map +1 -1
  78. package/lib/types/routes.js +160 -58
  79. package/lib/types/tunnel.d.ts +21 -6
  80. package/lib/types/tunnel.d.ts.map +1 -1
  81. package/lib/types/tunnel.js +58 -11
  82. package/lib/types/update.d.ts +4 -0
  83. package/lib/types/update.d.ts.map +1 -1
  84. package/lib/types/update.js +74 -33
  85. package/package.json +23 -20
  86. package/src/client/FenceNotice.tsx +0 -2
  87. package/src/client/PluginSettingsCard.tsx +7 -2
  88. package/src/client/RemoteEntry.tsx +14 -9
  89. package/src/client/RemotePanel.tsx +17 -7
  90. package/src/client/RemoteSettingsCard.tsx +16 -9
  91. package/src/client/UpdateEntry.tsx +2 -2
  92. package/src/client/UpdatePanel.tsx +3 -3
  93. package/src/client/deep-link.ts +2 -2
  94. package/src/client/index.ts +70 -40
  95. package/src/client/locales.ts +6 -8
  96. package/src/client/mobile-adapt.ts +136 -25
  97. package/src/client/pair-api.ts +47 -7
  98. package/src/client/plugin-card-seat.ts +192 -0
  99. package/src/client/remote-channel.ts +19 -4
  100. package/src/client/remote.module.css +38 -0
  101. package/src/client/settings-form.ts +34 -31
  102. package/src/console-output.ts +97 -0
  103. package/src/http.ts +23 -1
  104. package/src/index.ts +102 -79
  105. package/src/inner-auth.ts +3 -2
  106. package/src/pair-grant.ts +131 -0
  107. package/src/pairing.persist.test.ts +36 -0
  108. package/src/pairing.ts +75 -24
  109. package/src/posture.ts +12 -4
  110. package/src/remote-api.ts +34 -41
  111. package/src/remote-channel-boot.ts +16 -8
  112. package/src/remote-channel-rules.ts +17 -0
  113. package/src/remote-methods.ts +1 -0
  114. package/src/routes.ts +165 -57
  115. package/src/tunnel.ts +56 -11
  116. package/src/update.ts +74 -29
@@ -7,10 +7,14 @@
7
7
  * consult.
8
8
  *
9
9
  * Security model:
10
- * - While `requirePairingForLan` is on (default), every request must carry a
11
- * live paired-device cookie, enforced before any bytes are forwarded and
12
- * before any host call. With the policy off, the cookie gate is skipped
13
- * (the local-only denials below still apply).
10
+ * - Every request must carry a live paired-device credential, enforced before
11
+ * any bytes are forwarded and before any host call. This gate is
12
+ * unconditional and does NOT follow `requirePairingForLan`: the channel
13
+ * re-issues traffic with the process's own browser-auth credential, so it is
14
+ * an authentication amplifier — an unpaired caller admitted here would reach
15
+ * the full host API without ever holding that credential. The LAN policy
16
+ * governs the plain `/api` surface only (whether the harness fence's LAN
17
+ * auto-trust stands on its own); it can never widen this channel.
14
18
  * - A paired remote desktop is a full-control credential: the browser half
15
19
  * flips the official UI into host mode (the transport ownsHost hook), so
16
20
  * the configuration plane (settings, credentials, presets, deliverables)
@@ -93,23 +97,21 @@ export function pairedDeviceIdOf(req, service) {
93
97
  }
94
98
  /**
95
99
  * Build the remote desktop channel HTTP routes.
96
- * @param deps - pairing service + local port + live pairing policy.
100
+ * @param deps - pairing service + local port + inner credential.
97
101
  * @returns the routes to register on webServer.
98
102
  */
99
103
  export function makeRemoteApiRoutes(deps) {
100
- const { service, port, requirePairingForLan = true } = deps;
104
+ const { service, port } = deps;
101
105
  const handler = (req, res) => {
102
- // Cookie gate first — same order as /m/api. Do not buffer an unpaired body.
103
- // With the live policy off, untrusted-but-policy-open callers are proxied
104
- // (a stale client rewrite must not 403); loopback-only denials stay below.
105
- const require = typeof requirePairingForLan === 'function' ? requirePairingForLan() : requirePairingForLan;
106
- if (require) {
107
- const paired = pairedDeviceIdOf(req, service);
108
- if (paired === undefined) {
109
- req.resume();
110
- envelopeError(res, 403, 'invalid-request', 'unpaired', 'this device is not paired with the desktop');
111
- return;
112
- }
106
+ // Paired credential first — before any bytes are buffered or forwarded.
107
+ // Unconditional by design: this channel attaches the process's own
108
+ // browser-auth cookie to every re-issued call, so admitting an unpaired
109
+ // caller would hand out that credential's authority (issue #1665).
110
+ const paired = pairedDeviceIdOf(req, service);
111
+ if (paired === undefined) {
112
+ req.resume();
113
+ envelopeError(res, 403, 'invalid-request', 'unpaired', 'this device is not paired with the desktop');
114
+ return;
113
115
  }
114
116
  const method = req.method ?? 'GET';
115
117
  if (!ALLOWED_METHODS.has(method)) {
@@ -159,24 +161,22 @@ export function upgradeInnerPath(reqUrl, fallbackPath) {
159
161
  * @returns the upgrade routes to register on webServer.
160
162
  */
161
163
  export function makeRemoteApiUpgradeRoutes(deps) {
162
- const { service, port, requirePairingForLan = true } = deps;
164
+ const { service, port } = deps;
163
165
  const handlerFor = (fallbackPath) => (req, socket, head) => {
164
- const require = typeof requirePairingForLan === 'function' ? requirePairingForLan() : requirePairingForLan;
165
- if (require) {
166
- // WebSocket handshakes cannot carry headers from the Web API, so the
167
- // cookieless credential rides the query; the cookie stays the primary.
168
- let queryDevice;
169
- try {
170
- queryDevice = new URL(req.url ?? '/', 'http://127.0.0.1').searchParams.get(REMOTE_DEVICE_QUERY) ?? undefined;
171
- }
172
- catch { /* fall through to the cookie */ }
173
- const deviceId = pairedDeviceIdOf(req, service);
174
- const paired = deviceId ?? (queryDevice !== undefined && service.touchDevice(queryDevice) ? queryDevice : undefined);
175
- if (paired === undefined) {
176
- socket.write('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
177
- socket.destroy();
178
- return;
179
- }
166
+ // WebSocket handshakes cannot carry headers from the Web API, so the
167
+ // cookieless credential rides the query; the cookie stays the primary.
168
+ // The gate is unconditional here too (issue #1665).
169
+ let queryDevice;
170
+ try {
171
+ queryDevice = new URL(req.url ?? '/', 'http://127.0.0.1').searchParams.get(REMOTE_DEVICE_QUERY) ?? undefined;
172
+ }
173
+ catch { /* fall through to the cookie */ }
174
+ const deviceId = pairedDeviceIdOf(req, service);
175
+ const paired = deviceId ?? (queryDevice !== undefined && service.touchDevice(queryDevice) ? queryDevice : undefined);
176
+ if (paired === undefined) {
177
+ socket.write('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
178
+ socket.destroy();
179
+ return;
180
180
  }
181
181
  const inner = upgradeInnerPath(req.url, fallbackPath);
182
182
  const denied = loopbackOnlyDenial(inner.split('?')[0] ?? inner);
@@ -17,7 +17,12 @@
17
17
  * transport hook `__DSH_TRANSPORT__ = { ownsHost: true }` before the
18
18
  * connection plugin reads it: the paired remote desktop gets the full
19
19
  * settings/credentials/presets surface, and every call still rides the
20
- * gated /remote channel. It finally publishes the official pre-Cordis
20
+ * gated /remote channel. Host mode is server-granted, not origin-asserted:
21
+ * the hook is installed only when the device-gated app landing (/pair-app)
22
+ * published the grant marker (REMOTE_HOST_GRANT_GLOBAL) ahead of this
23
+ * script, so a shell served to an unpaired browser - a fence-open
24
+ * deployment - never presents itself as the machine owner. It finally
25
+ * publishes the official pre-Cordis
21
26
  * upload hook (`__DSH_FILE_UPLOAD__`), because the background upload
22
27
  * transport otherwise runs inside a Web Worker whose own globals the
23
28
  * main-thread rewrite cannot reach (issue #1580). The script self-skips on
@@ -1 +1 @@
1
- {"version":3,"file":"remote-channel-boot.d.ts","sourceRoot":"","sources":["../../src/remote-channel-boot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAoD,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAErH;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,GAAE,kBAAyC,GAAG,MAAM,CAqJrG;AAED,iFAAiF;AACjF,eAAO,MAAM,iBAAiB,2BAA2B,CAAA;AAQzD;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,SAAwB,EAAE,MAAM,SAAwB,GAAG,MAAM,CAgB9G;AAED,0EAA0E;AAC1E,eAAO,MAAM,0BAA0B,EAAE,MAAuC,CAAA"}
1
+ {"version":3,"file":"remote-channel-boot.d.ts","sourceRoot":"","sources":["../../src/remote-channel-boot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EAAoD,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAErH;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,GAAE,kBAAyC,GAAG,MAAM,CAwJrG;AAED,iFAAiF;AACjF,eAAO,MAAM,iBAAiB,2BAA2B,CAAA;AAQzD;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,SAAwB,EAAE,MAAM,SAAwB,GAAG,MAAM,CAgB9G;AAED,0EAA0E;AAC1E,eAAO,MAAM,0BAA0B,EAAE,MAAuC,CAAA"}
@@ -17,7 +17,12 @@
17
17
  * transport hook `__DSH_TRANSPORT__ = { ownsHost: true }` before the
18
18
  * connection plugin reads it: the paired remote desktop gets the full
19
19
  * settings/credentials/presets surface, and every call still rides the
20
- * gated /remote channel. It finally publishes the official pre-Cordis
20
+ * gated /remote channel. Host mode is server-granted, not origin-asserted:
21
+ * the hook is installed only when the device-gated app landing (/pair-app)
22
+ * published the grant marker (REMOTE_HOST_GRANT_GLOBAL) ahead of this
23
+ * script, so a shell served to an unpaired browser - a fence-open
24
+ * deployment - never presents itself as the machine owner. It finally
25
+ * publishes the official pre-Cordis
21
26
  * upload hook (`__DSH_FILE_UPLOAD__`), because the background upload
22
27
  * transport otherwise runs inside a Web Worker whose own globals the
23
28
  * main-thread rewrite cannot reach (issue #1580). The script self-skips on
@@ -38,14 +43,17 @@ export function buildRemoteChannelBootScript(rules = REMOTE_CHANNEL_RULES) {
38
43
  return '(function(){' +
39
44
  'try{' +
40
45
  'var w=window,loc=w.location,h=loc.hostname;' +
41
- // Loopback origins keep the original paths (mirrors isLoopbackHostname).
42
- "if(h==='localhost'||h==='::1'||/^127(\\.\\d{1,3}){3}$/.test(h))return;" +
43
- // Host mode: the paired remote desktop presents itself as the machine
44
- // owner, so the official UI keeps its full configuration surface (the
45
- // settings mirror, document controller, and deliverables open actions
46
- // all branch on connection.isLoopback). Must run before any boot entry.
47
- 'try{if(w.__DSH_TRANSPORT__===undefined)w.__DSH_TRANSPORT__={};w.__DSH_TRANSPORT__.ownsHost=true}catch(e){}' +
46
+ // Loopback origins (including the bracketed IPv6 literal WHATWG returns)
47
+ // keep the original paths (mirrors isLoopbackHostname).
48
+ "if(h==='localhost'||h==='::1'||h==='[::1]'||/^127(\\.\\d{1,3}){3}$/.test(h))return;" +
48
49
  'var R=' + json + ';' +
50
+ // Host mode is server-granted: only the device-gated app landing
51
+ // publishes the grant marker (in a capture script that runs ahead of
52
+ // this one), so a shell that merely sits on a non-loopback origin - an
53
+ // unpaired browser reaching a fence-open deployment - keeps the official
54
+ // UI's memory-scope presentation instead of posing as the machine owner.
55
+ // Must run before any boot entry.
56
+ 'try{if(w[R.hostGrantGlobal]===true){if(w.__DSH_TRANSPORT__===undefined)w.__DSH_TRANSPORT__={};w.__DSH_TRANSPORT__.ownsHost=true}}catch(e){}' +
49
57
  // The cookieless device credential: read lazily per call - the
50
58
  // /pair-app capture script sets it in head AFTER this boot script ran,
51
59
  // so a parse-time read would always see null.
@@ -11,6 +11,12 @@ import { REMOTE_DEVICE_HEADER, REMOTE_DEVICE_QUERY } from './remote-methods.ts';
11
11
  export { REMOTE_DEVICE_HEADER, REMOTE_DEVICE_QUERY };
12
12
  /** Connection-plugin method prefix under the gated channel. */
13
13
  export declare const REMOTE_API_PREFIX = "/remote/api";
14
+ /**
15
+ * The window global the device-gated app landing publishes to grant host mode.
16
+ * Set by the /pair-app capture script, read by the parse-time boot patch: the
17
+ * transport hook (ownsHost) is server-granted, not asserted from the origin.
18
+ */
19
+ export declare const REMOTE_HOST_GRANT_GLOBAL = "__DSH_REMOTE_HOST_GRANT__";
14
20
  /** Every decision input of the remote-channel rewrite, JSON-serializable. */
15
21
  export interface RemoteChannelRules {
16
22
  readonly remotePrefix: string;
@@ -32,6 +38,14 @@ export interface RemoteChannelRules {
32
38
  readonly uploadPath: string;
33
39
  /** Page global the pre-Cordis upload hook is published under. */
34
40
  readonly uploadHookGlobal: string;
41
+ /**
42
+ * Page global carrying the server-issued host-mode grant. Only the plugin's
43
+ * own device-gated app landing (/pair-app) publishes it; the boot patch
44
+ * therefore enables the official UI's host mode only for a shell the server
45
+ * actually granted, never for one that merely happens to sit on a
46
+ * non-loopback origin.
47
+ */
48
+ readonly hostGrantGlobal: string;
35
49
  }
36
50
  /** The live rule set. */
37
51
  export declare const REMOTE_CHANNEL_RULES: RemoteChannelRules;
@@ -1 +1 @@
1
- {"version":3,"file":"remote-channel-rules.d.ts","sourceRoot":"","sources":["../../src/remote-channel-rules.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,kEAAkE;AAClE,eAAO,MAAM,aAAa,YAAY,CAAA;AAEtC,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAE/E,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,CAAA;AAEpD,+DAA+D;AAC/D,eAAO,MAAM,iBAAiB,gBAAyB,CAAA;AAEvD,6EAA6E;AAC7E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAA;IACrC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;IACnC,yEAAyE;IACzE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,2EAA2E;IAC3E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,yDAAyD;IACzD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,iEAAiE;IACjE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,iEAAiE;IACjE,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;CAClC;AAED,yBAAyB;AACzB,eAAO,MAAM,oBAAoB,EAAE,kBAuBlC,CAAA;AAED,iEAAiE;AACjE,eAAO,MAAM,0BAA0B,gCAAgC,CAAA;AAEvE;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IAC/B,QAAQ,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IAC7B,eAAe,EAAE,OAAO,CAAA;IACxB,OAAO,IAAI,IAAI,CAAA;CAChB"}
1
+ {"version":3,"file":"remote-channel-rules.d.ts","sourceRoot":"","sources":["../../src/remote-channel-rules.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,kEAAkE;AAClE,eAAO,MAAM,aAAa,YAAY,CAAA;AAEtC,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAE/E,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,CAAA;AAEpD,+DAA+D;AAC/D,eAAO,MAAM,iBAAiB,gBAAyB,CAAA;AAEvD;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,8BAA8B,CAAA;AAEnE,6EAA6E;AAC7E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAA;IACrC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;IACnC,yEAAyE;IACzE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,2EAA2E;IAC3E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,yDAAyD;IACzD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,iEAAiE;IACjE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,iEAAiE;IACjE,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;IACjC;;;;;;OAMG;IACH,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;CACjC;AAED,yBAAyB;AACzB,eAAO,MAAM,oBAAoB,EAAE,kBAyBlC,CAAA;AAED,iEAAiE;AACjE,eAAO,MAAM,0BAA0B,gCAAgC,CAAA;AAEvE;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IAC/B,QAAQ,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IAC7B,eAAe,EAAE,OAAO,CAAA;IACxB,OAAO,IAAI,IAAI,CAAA;CAChB"}
@@ -11,6 +11,12 @@ import { REMOTE_DEVICE_HEADER, REMOTE_DEVICE_QUERY } from "./remote-methods.js";
11
11
  export { REMOTE_DEVICE_HEADER, REMOTE_DEVICE_QUERY };
12
12
  /** Connection-plugin method prefix under the gated channel. */
13
13
  export const REMOTE_API_PREFIX = `${REMOTE_PREFIX}/api`;
14
+ /**
15
+ * The window global the device-gated app landing publishes to grant host mode.
16
+ * Set by the /pair-app capture script, read by the parse-time boot patch: the
17
+ * transport hook (ownsHost) is server-granted, not asserted from the origin.
18
+ */
19
+ export const REMOTE_HOST_GRANT_GLOBAL = '__DSH_REMOTE_HOST_GRANT__';
14
20
  /** The live rule set. */
15
21
  export const REMOTE_CHANNEL_RULES = {
16
22
  remotePrefix: REMOTE_PREFIX,
@@ -28,6 +34,7 @@ export const REMOTE_CHANNEL_RULES = {
28
34
  '/api/remote.mux',
29
35
  '/sidebar/ws/terminal',
30
36
  '/sidebar/ws/agent-terminals',
37
+ '/sidebar/ws/agent-opens',
31
38
  '/api/dsh-ssh/terminal',
32
39
  ],
33
40
  deviceHeader: REMOTE_DEVICE_HEADER,
@@ -35,6 +42,7 @@ export const REMOTE_CHANNEL_RULES = {
35
42
  deviceQuery: REMOTE_DEVICE_QUERY,
36
43
  uploadPath: '/api/session/uploadFileBinary',
37
44
  uploadHookGlobal: '__DSH_FILE_UPLOAD__',
45
+ hostGrantGlobal: REMOTE_HOST_GRANT_GLOBAL,
38
46
  };
39
47
  /** The window global the boot patch publishes its seat under. */
40
48
  export const REMOTE_CHANNEL_BOOT_GLOBAL = '__DSH_REMOTE_CHANNEL_BOOT__';
@@ -31,7 +31,7 @@ export declare const REMOTE_API_PATHS: {
31
31
  * Exact upgrade paths registered on webServer (the SDK matches upgrades by
32
32
  * exact path, not prefix). Query strings ride on the request URL.
33
33
  */
34
- export declare const REMOTE_UPGRADE_PATHS: readonly ["/remote/api/remote.mux", "/remote/sidebar/ws/terminal", "/remote/sidebar/ws/agent-terminals", "/remote/api/dsh-ssh/terminal"];
34
+ export declare const REMOTE_UPGRADE_PATHS: readonly ["/remote/api/remote.mux", "/remote/sidebar/ws/terminal", "/remote/sidebar/ws/agent-terminals", "/remote/sidebar/ws/agent-opens", "/remote/api/dsh-ssh/terminal"];
35
35
  /** Plugin-manager HTTP prefix: install/remove stay physically local. */
36
36
  export declare const PLUGIN_MANAGER_PATH = "/api/plugin-manager";
37
37
  /** Family settings-bridge HTTP prefix — re-exposed to paired devices (plain settings parity). */
@@ -1 +1 @@
1
- {"version":3,"file":"remote-methods.d.ts","sourceRoot":"","sources":["../../src/remote-methods.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,gFAAgF;AAChF,eAAO,MAAM,aAAa,YAAY,CAAA;AAEtC,+DAA+D;AAC/D,eAAO,MAAM,iBAAiB,gBAAyB,CAAA;AAEvD;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB;;CAEnB,CAAA;AAEV;;;GAGG;AACH,eAAO,MAAM,oBAAoB,0IAKvB,CAAA;AAEV,wEAAwE;AACxE,eAAO,MAAM,mBAAmB,wBAAwB,CAAA;AAExD,iGAAiG;AACjG,eAAO,MAAM,2BAA2B,6BAA6B,CAAA;AAErE;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,wBAAwB,CAAA;AACzD,eAAO,MAAM,mBAAmB,WAAW,CAAA;AAE3C;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAIvC,CAAA;AAEV;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOrE"}
1
+ {"version":3,"file":"remote-methods.d.ts","sourceRoot":"","sources":["../../src/remote-methods.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,gFAAgF;AAChF,eAAO,MAAM,aAAa,YAAY,CAAA;AAEtC,+DAA+D;AAC/D,eAAO,MAAM,iBAAiB,gBAAyB,CAAA;AAEvD;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB;;CAEnB,CAAA;AAEV;;;GAGG;AACH,eAAO,MAAM,oBAAoB,4KAMvB,CAAA;AAEV,wEAAwE;AACxE,eAAO,MAAM,mBAAmB,wBAAwB,CAAA;AAExD,iGAAiG;AACjG,eAAO,MAAM,2BAA2B,6BAA6B,CAAA;AAErE;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,wBAAwB,CAAA;AACzD,eAAO,MAAM,mBAAmB,WAAW,CAAA;AAE3C;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAIvC,CAAA;AAEV;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOrE"}
@@ -35,6 +35,7 @@ export const REMOTE_UPGRADE_PATHS = [
35
35
  REMOTE_API_PATHS.mux,
36
36
  `${REMOTE_PREFIX}/sidebar/ws/terminal`,
37
37
  `${REMOTE_PREFIX}/sidebar/ws/agent-terminals`,
38
+ `${REMOTE_PREFIX}/sidebar/ws/agent-opens`,
38
39
  `${REMOTE_API_PREFIX}/dsh-ssh/terminal`,
39
40
  ];
40
41
  /** Plugin-manager HTTP prefix: install/remove stay physically local. */
@@ -11,11 +11,19 @@
11
11
  * on the 0.1.2-alpha.2 cohort nothing emits api/gate — direct /api is
12
12
  * governed by the harness fence + browser auth — while the /remote channel
13
13
  * always enforces the pairing cookie itself.
14
+ *
15
+ * The app landing is reached with a one-time grant, never with the device id
16
+ * itself: /pair-accept mints a short-lived, single-consume grant
17
+ * (pair-grant.ts) and redirects to /pair-app?grant=<g>, and only the landing's
18
+ * own response carries the device credential into the shell. The device cookie
19
+ * stays the primary credential; the grant only keeps a live session credential
20
+ * out of the URL, the address bar, and every log on the redirect path.
14
21
  */
15
22
  import type { IncomingMessage, ServerResponse } from 'node:http';
16
23
  import type { WebRoute } from '@deepseek-ai/dsh-host-webserver';
17
24
  import { z } from 'zod';
18
25
  import { type PairingService, type PairingSnapshot } from './pairing.ts';
26
+ import { type PairGrantStore } from './pair-grant.ts';
19
27
  /**
20
28
  * Browser-trust fence for the /api/pair routes, mirroring the connection
21
29
  * package's internal fence semantics (Host/Origin based, DNS-rebinding and
@@ -67,14 +75,24 @@ export declare function isPrivateOrLocalHostname(hostname: string): boolean;
67
75
  export declare function isNonCrossSite(request: IncomingMessage): boolean;
68
76
  /**
69
77
  * The rate-limit bucket key for one accept attempt (pure; unit-tested).
70
- * The first client-visible XFF hop separates buckets behind the auto-tunnel
71
- * (every internet client arrives from 127.0.0.1 there) — but only for
72
- * loopback peers, since a direct LAN client can rotate the header freely.
78
+ * A forwarded hop separates buckets behind the auto-tunnel (every internet
79
+ * client arrives from 127.0.0.1 there) — but only for loopback peers, since a
80
+ * direct LAN client can rotate the header freely. The caller passes the hop it
81
+ * trusts (the edge-appended LAST one; see rateLimitAccept), and the hop is
82
+ * truncated so one oversized header cannot mint a huge key.
73
83
  * @param socketIp - the socket peer address.
74
- * @param forwarded - the first XFF hop, already trimmed, if any.
84
+ * @param forwarded - the trusted XFF hop, already trimmed, if any.
75
85
  * @param bucket - page (GET /pair-accept) vs api (POST /api/pair/accept).
76
86
  */
77
87
  export declare function acceptLimitKey(socketIp: string, forwarded: string | undefined, bucket: 'page' | 'api'): string;
88
+ /**
89
+ * Hard cap on live accept buckets. The key is caller-influenced (a forwarded
90
+ * hop), so a rotating flood must not grow the table without bound: past the
91
+ * cap the oldest bucket is evicted, the same FIFO stance as
92
+ * {@link addBounded}. A legitimate client that loses its bucket merely starts
93
+ * a fresh window.
94
+ */
95
+ export declare const MAX_ACCEPT_BUCKETS = 1024;
78
96
  /**
79
97
  * Cap on the dynamically trusted hosts (see {@link addBounded}): room for
80
98
  * every legitimate router/proxy aliasing shape, small enough that a flood of
@@ -109,7 +127,14 @@ export declare function publicHostOf(url: string | undefined): string | undefine
109
127
  */
110
128
  export declare const APP_DEVICE_STORAGE_KEY = "dsh-remote-device";
111
129
  export declare function appShellCaptureScript(deviceId: string): string;
112
- /** Patch the official index document with the device-capture script. */
130
+ /**
131
+ * Patch the official index document with the device-capture script. The script
132
+ * goes immediately after the opening <head> tag, ahead of the harness-injected
133
+ * parse-time channel boot patch: that patch installs the transport host-mode
134
+ * hook only when this server-served marker is already set (see
135
+ * remote-channel-boot.ts), so host mode is granted by the device-gated landing
136
+ * instead of being asserted from the origin.
137
+ */
113
138
  export declare function patchAppShell(html: string, deviceId: string): string;
114
139
  /**
115
140
  * The reopen service worker served at PAIR_PATHS.appServiceWorker. A paired
@@ -205,6 +230,11 @@ export interface PairRoutesDeps {
205
230
  indexDocument?: (deviceId: string) => Promise<string | undefined>;
206
231
  /** Extra trusted non-loopback hosts (from config or environment). */
207
232
  trustedHosts?: readonly string[] | (() => readonly string[]);
233
+ /**
234
+ * The one-time grant table behind the /pair-accept → /pair-app redirect.
235
+ * Defaults to a fresh store; tests inject one with a fixed clock/entropy.
236
+ */
237
+ grants?: PairGrantStore;
208
238
  }
209
239
  /**
210
240
  * Build the /api/pair route family.
@@ -212,4 +242,16 @@ export interface PairRoutesDeps {
212
242
  * @returns the exact routes to register on webServer.
213
243
  */
214
244
  export declare function makeRoutes(deps: PairRoutesDeps): WebRoute[];
245
+ /**
246
+ * Whether the request reached this process over TLS. The harness itself serves
247
+ * plain HTTP; a tunnel edge terminates TLS and stamps `x-forwarded-proto`
248
+ * (the same signal {@link appOrigin} trusts when it rebuilds a redirect).
249
+ */
250
+ export declare function isHttpsRequest(req: IncomingMessage): boolean;
251
+ /**
252
+ * The device cookie for one request. `Secure` is added only when the request
253
+ * arrived over TLS: LAN pairing runs on plain HTTP, where a Secure cookie is
254
+ * simply dropped by the browser and the phone would silently lose its session.
255
+ */
256
+ export declare function deviceCookie(req: IncomingMessage, cookieName: string, deviceId: string): string;
215
257
  //# sourceMappingURL=routes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"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"}