@linxin666/dsh-remote-web-ui 0.3.6 → 0.3.9
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 +77 -468
- package/README.zh.md +83 -113
- package/lib/client.js +955 -203
- package/lib/client.js.map +1 -1
- package/lib/index.js +1136 -1466
- package/lib/types/client/FooterRemoteEntry.d.ts +6 -7
- package/lib/types/client/FooterRemoteEntry.d.ts.map +1 -1
- package/lib/types/client/FooterRemoteEntry.js +1 -1
- package/lib/types/client/RemoteEntry.d.ts +7 -4
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +7 -10
- package/lib/types/client/RemotePanel.d.ts +2 -2
- package/lib/types/client/RemotePanel.d.ts.map +1 -1
- package/lib/types/client/RemotePanel.js +2 -2
- package/lib/types/client/RemoteSettingsCard.d.ts +9 -5
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +56 -3
- package/lib/types/client/deep-link.d.ts +10 -13
- package/lib/types/client/deep-link.d.ts.map +1 -1
- package/lib/types/client/deep-link.js +14 -74
- package/lib/types/client/index.d.ts +2 -6
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +73 -39
- package/lib/types/client/locales.d.ts +26 -12
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +36 -22
- package/lib/types/client/mobile-adapt.d.ts +40 -0
- package/lib/types/client/mobile-adapt.d.ts.map +1 -0
- package/lib/types/client/mobile-adapt.js +967 -0
- package/lib/types/client/pair-api.d.ts +24 -6
- package/lib/types/client/pair-api.d.ts.map +1 -1
- package/lib/types/client/pair-api.js +9 -14
- package/lib/types/client/remote-channel.d.ts +3 -0
- package/lib/types/client/remote-channel.d.ts.map +1 -1
- package/lib/types/client/remote-channel.js +35 -3
- package/lib/types/client/settings-form.d.ts +35 -43
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +60 -53
- package/lib/types/firewall.d.ts +75 -0
- package/lib/types/firewall.d.ts.map +1 -0
- package/lib/types/firewall.js +184 -0
- package/lib/types/gate.d.ts +11 -5
- package/lib/types/gate.d.ts.map +1 -1
- package/lib/types/gate.js +11 -5
- package/lib/types/index.d.ts +39 -10
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +206 -58
- package/lib/types/inner-auth.d.ts +32 -0
- package/lib/types/inner-auth.d.ts.map +1 -0
- package/lib/types/inner-auth.js +72 -0
- package/lib/types/lan-bind-plan.d.ts +55 -0
- package/lib/types/lan-bind-plan.d.ts.map +1 -0
- package/lib/types/lan-bind-plan.js +60 -0
- package/lib/types/lan-bind.d.ts +69 -0
- package/lib/types/lan-bind.d.ts.map +1 -0
- package/lib/types/lan-bind.js +128 -0
- package/lib/types/loopback-proxy.d.ts +9 -2
- package/lib/types/loopback-proxy.d.ts.map +1 -1
- package/lib/types/loopback-proxy.js +24 -2
- package/lib/types/pairing.d.ts +15 -3
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +20 -6
- package/lib/types/remote-api.d.ts +30 -13
- package/lib/types/remote-api.d.ts.map +1 -1
- package/lib/types/remote-api.js +55 -42
- package/lib/types/remote-channel-boot.d.ts +21 -2
- package/lib/types/remote-channel-boot.d.ts.map +1 -1
- package/lib/types/remote-channel-boot.js +65 -3
- package/lib/types/remote-channel-rules.d.ts +8 -0
- package/lib/types/remote-channel-rules.d.ts.map +1 -1
- package/lib/types/remote-channel-rules.js +9 -2
- package/lib/types/remote-methods.d.ts +45 -11
- package/lib/types/remote-methods.d.ts.map +1 -1
- package/lib/types/remote-methods.js +56 -27
- package/lib/types/remote-presence-pet.d.ts +51 -0
- package/lib/types/remote-presence-pet.d.ts.map +1 -0
- package/lib/types/remote-presence-pet.js +101 -0
- package/lib/types/routes.d.ts +76 -4
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +336 -19
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/tunnel.js +7 -1
- package/package.json +20 -18
- package/src/client/FooterRemoteEntry.tsx +9 -10
- package/src/client/RemoteEntry.tsx +13 -14
- package/src/client/RemotePanel.tsx +5 -16
- package/src/client/RemoteSettingsCard.tsx +72 -13
- package/src/client/deep-link.ts +14 -75
- package/src/client/index.ts +77 -39
- package/src/client/locales.ts +36 -22
- package/src/client/mobile-adapt.ts +913 -0
- package/src/client/pair-api.ts +26 -15
- package/src/client/remote-channel.ts +31 -3
- package/src/client/settings-form.ts +78 -92
- package/src/dsh-home.ts +1 -1
- package/src/firewall.ts +230 -0
- package/src/gate.ts +11 -5
- package/src/index.ts +235 -65
- package/src/inner-auth.ts +85 -0
- package/src/lan-bind-plan.ts +81 -0
- package/src/lan-bind.ts +145 -0
- package/src/loopback-proxy.ts +34 -0
- package/src/pairing.persist.test.ts +1 -1
- package/src/pairing.ts +20 -6
- package/src/remote-api.ts +67 -45
- package/src/remote-channel-boot.ts +70 -3
- package/src/remote-channel-rules.ts +17 -2
- package/src/remote-methods.ts +58 -27
- package/src/remote-presence-pet.ts +129 -0
- package/src/routes.ts +358 -19
- package/src/tunnel.ts +7 -1
- package/assets/apple-touch-icon.png +0 -0
- package/assets/mobile-icon-192.png +0 -0
- package/assets/mobile-icon-512.png +0 -0
- package/assets/mobile-service-worker.js +0 -63
- package/lib/mobile.js +0 -17658
- package/lib/mobile.js.map +0 -1
- package/lib/types/mobile/PairRequiredView.d.ts +0 -7
- package/lib/types/mobile/PairRequiredView.d.ts.map +0 -1
- package/lib/types/mobile/PairRequiredView.js +0 -28
- package/lib/types/mobile/PairRequiredView.test.d.ts +0 -2
- package/lib/types/mobile/PairRequiredView.test.d.ts.map +0 -1
- package/lib/types/mobile/PairRequiredView.test.js +0 -24
- package/lib/types/mobile/api.d.ts +0 -145
- package/lib/types/mobile/api.d.ts.map +0 -1
- package/lib/types/mobile/api.js +0 -103
- package/lib/types/mobile/index.d.ts +0 -10
- package/lib/types/mobile/index.d.ts.map +0 -1
- package/lib/types/mobile/index.js +0 -38
- package/lib/types/mobile/markdown.d.ts +0 -34
- package/lib/types/mobile/markdown.d.ts.map +0 -1
- package/lib/types/mobile/markdown.js +0 -285
- package/lib/types/mobile/markdown.test.d.ts +0 -2
- package/lib/types/mobile/markdown.test.d.ts.map +0 -1
- package/lib/types/mobile/markdown.test.js +0 -79
- package/lib/types/mobile/messages.d.ts +0 -149
- package/lib/types/mobile/messages.d.ts.map +0 -1
- package/lib/types/mobile/messages.js +0 -561
- package/lib/types/mobile/messages.test.d.ts +0 -2
- package/lib/types/mobile/messages.test.d.ts.map +0 -1
- package/lib/types/mobile/messages.test.js +0 -356
- package/lib/types/mobile/mobile-styles.d.ts +0 -6
- package/lib/types/mobile/mobile-styles.d.ts.map +0 -1
- package/lib/types/mobile/mobile-styles.js +0 -1438
- package/lib/types/mobile/mobile-theme.d.ts +0 -22
- package/lib/types/mobile/mobile-theme.d.ts.map +0 -1
- package/lib/types/mobile/mobile-theme.js +0 -70
- package/lib/types/mobile/mobile-theme.test.d.ts +0 -2
- package/lib/types/mobile/mobile-theme.test.d.ts.map +0 -1
- package/lib/types/mobile/mobile-theme.test.js +0 -71
- package/lib/types/mobile/mux.d.ts +0 -128
- package/lib/types/mobile/mux.d.ts.map +0 -1
- package/lib/types/mobile/mux.js +0 -305
- package/lib/types/mobile/mux.test.d.ts +0 -2
- package/lib/types/mobile/mux.test.d.ts.map +0 -1
- package/lib/types/mobile/mux.test.js +0 -267
- package/lib/types/mobile/pairing.d.ts +0 -31
- package/lib/types/mobile/pairing.d.ts.map +0 -1
- package/lib/types/mobile/pairing.js +0 -62
- package/lib/types/mobile/rpc.d.ts +0 -38
- package/lib/types/mobile/rpc.d.ts.map +0 -1
- package/lib/types/mobile/rpc.js +0 -83
- package/lib/types/mobile/theme-toggle.d.ts +0 -12
- package/lib/types/mobile/theme-toggle.d.ts.map +0 -1
- package/lib/types/mobile/theme-toggle.js +0 -26
- package/lib/types/mobile/views/App.d.ts +0 -57
- package/lib/types/mobile/views/App.d.ts.map +0 -1
- package/lib/types/mobile/views/App.js +0 -150
- package/lib/types/mobile/views/App.test.d.ts +0 -2
- package/lib/types/mobile/views/App.test.d.ts.map +0 -1
- package/lib/types/mobile/views/App.test.js +0 -52
- package/lib/types/mobile/views/ChatView.d.ts +0 -63
- package/lib/types/mobile/views/ChatView.d.ts.map +0 -1
- package/lib/types/mobile/views/ChatView.js +0 -821
- package/lib/types/mobile/views/ChatView.stream.test.d.ts +0 -2
- package/lib/types/mobile/views/ChatView.stream.test.d.ts.map +0 -1
- package/lib/types/mobile/views/ChatView.stream.test.js +0 -236
- package/lib/types/mobile/views/ChatView.test.d.ts +0 -2
- package/lib/types/mobile/views/ChatView.test.d.ts.map +0 -1
- package/lib/types/mobile/views/ChatView.test.js +0 -718
- package/lib/types/mobile/views/SessionListView.d.ts +0 -27
- package/lib/types/mobile/views/SessionListView.d.ts.map +0 -1
- package/lib/types/mobile/views/SessionListView.js +0 -139
- package/lib/types/mobile/views/SessionListView.test.d.ts +0 -2
- package/lib/types/mobile/views/SessionListView.test.d.ts.map +0 -1
- package/lib/types/mobile/views/SessionListView.test.js +0 -167
- package/lib/types/mobile/views/WorkspaceView.d.ts +0 -21
- package/lib/types/mobile/views/WorkspaceView.d.ts.map +0 -1
- package/lib/types/mobile/views/WorkspaceView.js +0 -93
- package/lib/types/mobile/views/WorkspaceView.test.d.ts +0 -2
- package/lib/types/mobile/views/WorkspaceView.test.d.ts.map +0 -1
- package/lib/types/mobile/views/WorkspaceView.test.js +0 -183
- package/lib/types/mobile-api.d.ts +0 -65
- package/lib/types/mobile-api.d.ts.map +0 -1
- package/lib/types/mobile-api.js +0 -404
- package/lib/types/mobile-pending.d.ts +0 -38
- package/lib/types/mobile-pending.d.ts.map +0 -1
- package/lib/types/mobile-pending.js +0 -59
- package/lib/types/mobile-pwa.d.ts +0 -6
- package/lib/types/mobile-pwa.d.ts.map +0 -1
- package/lib/types/mobile-pwa.js +0 -17
- package/lib/types/mobile-routes.d.ts +0 -13
- package/lib/types/mobile-routes.d.ts.map +0 -1
- package/lib/types/mobile-routes.js +0 -217
- package/lib/types/paired-model-catalog.d.ts +0 -17
- package/lib/types/paired-model-catalog.d.ts.map +0 -1
- package/lib/types/paired-model-catalog.js +0 -311
- package/src/mobile/PairRequiredView.test.tsx +0 -28
- package/src/mobile/PairRequiredView.tsx +0 -58
- package/src/mobile/api.ts +0 -229
- package/src/mobile/index.tsx +0 -41
- package/src/mobile/markdown.test.ts +0 -91
- package/src/mobile/markdown.ts +0 -291
- package/src/mobile/messages.test.ts +0 -402
- package/src/mobile/messages.ts +0 -685
- package/src/mobile/mobile-styles.ts +0 -1439
- package/src/mobile/mobile-theme.test.ts +0 -84
- package/src/mobile/mobile-theme.ts +0 -76
- package/src/mobile/mux.test.ts +0 -314
- package/src/mobile/mux.ts +0 -336
- package/src/mobile/pairing.ts +0 -77
- package/src/mobile/rpc.ts +0 -90
- package/src/mobile/theme-toggle.tsx +0 -47
- package/src/mobile/views/App.test.tsx +0 -62
- package/src/mobile/views/App.tsx +0 -213
- package/src/mobile/views/ChatView.stream.test.tsx +0 -268
- package/src/mobile/views/ChatView.test.tsx +0 -816
- package/src/mobile/views/ChatView.tsx +0 -1342
- package/src/mobile/views/SessionListView.test.tsx +0 -207
- package/src/mobile/views/SessionListView.tsx +0 -244
- package/src/mobile/views/WorkspaceView.test.tsx +0 -227
- package/src/mobile/views/WorkspaceView.tsx +0 -245
- package/src/mobile-api.ts +0 -418
- package/src/mobile-pending.ts +0 -88
- package/src/mobile-pwa.ts +0 -19
- package/src/mobile-routes.ts +0 -231
- package/src/paired-model-catalog.ts +0 -272
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LAN bind toggle: the managed cordis patch block that pins the web
|
|
3
|
+
* server's bind. On = a block binding 0.0.0.0; off = the same block binding
|
|
4
|
+
* 127.0.0.1. The block takes effect on the next `dsh web` start (the live
|
|
5
|
+
* patch watcher cannot rebind a running listener, and on this harness line
|
|
6
|
+
* the user patch layer cannot evaluate webStartup-dependent expressions
|
|
7
|
+
* reliably - static values are the only dependable form), so the plugin
|
|
8
|
+
* re-asserts the block at every boot and the settings card reports the
|
|
9
|
+
* running bind honestly.
|
|
10
|
+
*
|
|
11
|
+
* Same-id patch rows REPLACE the row config wholesale, so the block carries
|
|
12
|
+
* the official web-app webserver row's full config (bind + compression)
|
|
13
|
+
* with the bind values materialized. The CLI's --host 0.0.0.0 guard stays
|
|
14
|
+
* intact: deliberate LAN exposure happens through this configuration layer
|
|
15
|
+
* only. Until the user flips the toggle once, the plugin never touches the
|
|
16
|
+
* patch file.
|
|
17
|
+
*
|
|
18
|
+
* The block discipline (markers, atomic write, strip-and-rewrite) follows
|
|
19
|
+
* the dsh-LAN reference implementation (MIT).
|
|
20
|
+
*/
|
|
21
|
+
export declare const LAN_BIND_BLOCK_BEGIN = "# --- remote-web-ui lan-bind block (managed - do not edit) ---";
|
|
22
|
+
export declare const LAN_BIND_BLOCK_END = "# --- end remote-web-ui lan-bind block ---";
|
|
23
|
+
/** The two bind hosts the managed block pins. */
|
|
24
|
+
export type LanBindHost = '0.0.0.0' | '127.0.0.1';
|
|
25
|
+
/**
|
|
26
|
+
* The absolute path of the profile patch file this toggle manages. The value
|
|
27
|
+
* is config-controlled (and the DSH_PROFILE env fallback bypasses schema
|
|
28
|
+
* validation entirely), so the path is guarded twice: the profile must be a
|
|
29
|
+
* single safe path segment, and the resolved file must stay under the
|
|
30
|
+
* profiles directory.
|
|
31
|
+
*/
|
|
32
|
+
export declare function profilePatchFile(profile: string, home?: string): string;
|
|
33
|
+
/** Remove the managed block (and its markers) from patch content. */
|
|
34
|
+
export declare function stripManagedBlock(content: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Render the managed block for one bind state. The values are static: the
|
|
37
|
+
* user patch layer has no reliable lazy service evaluation, and the plugin
|
|
38
|
+
* re-asserts the block at every boot so CLI flags (--port, --host) win by
|
|
39
|
+
* rewriting it before the next start.
|
|
40
|
+
*/
|
|
41
|
+
export declare function managedBlock(host: LanBindHost, port: number): string;
|
|
42
|
+
/** The bind state a managed block pins (undefined when there is no block). */
|
|
43
|
+
export interface ManagedBindState {
|
|
44
|
+
host: LanBindHost | (string & {});
|
|
45
|
+
port: number | undefined;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Parse the block's pinned bind out of patch content. A block whose values
|
|
49
|
+
* were hand-edited reports the literals as-is so the card can surface them
|
|
50
|
+
* instead of silently claiming one of the two known states.
|
|
51
|
+
*/
|
|
52
|
+
export declare function managedBindOf(content: string): ManagedBindState | undefined;
|
|
53
|
+
/** Full file-level state for the settings card and the boot re-assert. */
|
|
54
|
+
export declare function lanBindState(profile: string, home?: string): {
|
|
55
|
+
blockPresent: boolean;
|
|
56
|
+
host?: string;
|
|
57
|
+
port?: number;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Write (or rewrite) the managed block with the given bind. The rest of the
|
|
61
|
+
* patch file is preserved; the block is rewritten atomically (unique temp
|
|
62
|
+
* file + rename, so concurrent writers can never rename a half-written
|
|
63
|
+
* file) with the original file's permissions preserved. A hand-truncated
|
|
64
|
+
* unterminated block (BEGIN marker without END, which stripManagedBlock
|
|
65
|
+
* cannot match) is truncated at its BEGIN marker first so the rewrite can
|
|
66
|
+
* never stack a second webserver row onto the orphan.
|
|
67
|
+
*/
|
|
68
|
+
export declare function writeLanBind(host: LanBindHost, port: number, profile: string, home?: string): void;
|
|
69
|
+
//# sourceMappingURL=lan-bind.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lan-bind.d.ts","sourceRoot":"","sources":["../../src/lan-bind.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AASH,eAAO,MAAM,oBAAoB,mEAAmE,CAAA;AACpG,eAAO,MAAM,kBAAkB,+CAA+C,CAAA;AAE9E,iDAAiD;AACjD,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,WAAW,CAAA;AAEjD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,MAAkB,GAAG,MAAM,CAUlF;AAYD,qEAAqE;AACrE,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAKzD;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAcpE;AAED,8EAA8E;AAC9E,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,WAAW,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;IACjC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;CACzB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAW3E;AAED,0EAA0E;AAC1E,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,MAAkB,GAAG;IAAE,YAAY,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAI/H;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,MAAkB,GAAG,IAAI,CAW7G"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LAN bind toggle: the managed cordis patch block that pins the web
|
|
3
|
+
* server's bind. On = a block binding 0.0.0.0; off = the same block binding
|
|
4
|
+
* 127.0.0.1. The block takes effect on the next `dsh web` start (the live
|
|
5
|
+
* patch watcher cannot rebind a running listener, and on this harness line
|
|
6
|
+
* the user patch layer cannot evaluate webStartup-dependent expressions
|
|
7
|
+
* reliably - static values are the only dependable form), so the plugin
|
|
8
|
+
* re-asserts the block at every boot and the settings card reports the
|
|
9
|
+
* running bind honestly.
|
|
10
|
+
*
|
|
11
|
+
* Same-id patch rows REPLACE the row config wholesale, so the block carries
|
|
12
|
+
* the official web-app webserver row's full config (bind + compression)
|
|
13
|
+
* with the bind values materialized. The CLI's --host 0.0.0.0 guard stays
|
|
14
|
+
* intact: deliberate LAN exposure happens through this configuration layer
|
|
15
|
+
* only. Until the user flips the toggle once, the plugin never touches the
|
|
16
|
+
* patch file.
|
|
17
|
+
*
|
|
18
|
+
* The block discipline (markers, atomic write, strip-and-rewrite) follows
|
|
19
|
+
* the dsh-LAN reference implementation (MIT).
|
|
20
|
+
*/
|
|
21
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
22
|
+
import { dirname, join, resolve, sep } from 'node:path';
|
|
23
|
+
import { dshHome } from "./dsh-home.js";
|
|
24
|
+
/** Profile names safe to interpolate into the patch path: one path segment, no traversal. */
|
|
25
|
+
const PROFILE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
26
|
+
export const LAN_BIND_BLOCK_BEGIN = '# --- remote-web-ui lan-bind block (managed - do not edit) ---';
|
|
27
|
+
export const LAN_BIND_BLOCK_END = '# --- end remote-web-ui lan-bind block ---';
|
|
28
|
+
/**
|
|
29
|
+
* The absolute path of the profile patch file this toggle manages. The value
|
|
30
|
+
* is config-controlled (and the DSH_PROFILE env fallback bypasses schema
|
|
31
|
+
* validation entirely), so the path is guarded twice: the profile must be a
|
|
32
|
+
* single safe path segment, and the resolved file must stay under the
|
|
33
|
+
* profiles directory.
|
|
34
|
+
*/
|
|
35
|
+
export function profilePatchFile(profile, home = dshHome()) {
|
|
36
|
+
if (!PROFILE_PATTERN.test(profile)) {
|
|
37
|
+
throw new Error(`remote-web-ui: unsafe lan-bind profile ${JSON.stringify(profile)}`);
|
|
38
|
+
}
|
|
39
|
+
const file = resolve(join(home, 'profiles', profile, 'cordis.patch.yml'));
|
|
40
|
+
const root = resolve(join(home, 'profiles')) + sep;
|
|
41
|
+
if (!file.startsWith(root)) {
|
|
42
|
+
throw new Error(`remote-web-ui: lan-bind profile ${JSON.stringify(profile)} escapes the profiles directory`);
|
|
43
|
+
}
|
|
44
|
+
return file;
|
|
45
|
+
}
|
|
46
|
+
function readPatchContent(file) {
|
|
47
|
+
if (!existsSync(file))
|
|
48
|
+
return '';
|
|
49
|
+
return readFileSync(file, 'utf8');
|
|
50
|
+
}
|
|
51
|
+
/** Escape one literal string for embedding into a RegExp pattern. */
|
|
52
|
+
function escapeRegex(text) {
|
|
53
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
54
|
+
}
|
|
55
|
+
/** Remove the managed block (and its markers) from patch content. */
|
|
56
|
+
export function stripManagedBlock(content) {
|
|
57
|
+
const begin = escapeRegex(LAN_BIND_BLOCK_BEGIN);
|
|
58
|
+
const end = escapeRegex(LAN_BIND_BLOCK_END);
|
|
59
|
+
const pattern = new RegExp(`\\r?\\n?${begin}[\\s\\S]*?${end}\\r?\\n?`, 'g');
|
|
60
|
+
return content.replace(pattern, '\n');
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Render the managed block for one bind state. The values are static: the
|
|
64
|
+
* user patch layer has no reliable lazy service evaluation, and the plugin
|
|
65
|
+
* re-asserts the block at every boot so CLI flags (--port, --host) win by
|
|
66
|
+
* rewriting it before the next start.
|
|
67
|
+
*/
|
|
68
|
+
export function managedBlock(host, port) {
|
|
69
|
+
return [
|
|
70
|
+
LAN_BIND_BLOCK_BEGIN,
|
|
71
|
+
'- id: webserver',
|
|
72
|
+
" name: '@deepseek-ai/dsh-host-webserver'",
|
|
73
|
+
' config:',
|
|
74
|
+
` host: '${host}'`,
|
|
75
|
+
` port: ${String(port)}`,
|
|
76
|
+
' compression: gzip',
|
|
77
|
+
' compressionLevel: 1',
|
|
78
|
+
' compressionThresholdBytes: 1024',
|
|
79
|
+
LAN_BIND_BLOCK_END,
|
|
80
|
+
'',
|
|
81
|
+
].join('\n');
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Parse the block's pinned bind out of patch content. A block whose values
|
|
85
|
+
* were hand-edited reports the literals as-is so the card can surface them
|
|
86
|
+
* instead of silently claiming one of the two known states.
|
|
87
|
+
*/
|
|
88
|
+
export function managedBindOf(content) {
|
|
89
|
+
const begin = content.indexOf(LAN_BIND_BLOCK_BEGIN);
|
|
90
|
+
if (begin === -1)
|
|
91
|
+
return undefined;
|
|
92
|
+
const end = content.indexOf(LAN_BIND_BLOCK_END, begin);
|
|
93
|
+
const block = end === -1 ? content.slice(begin) : content.slice(begin, end);
|
|
94
|
+
const hostMatch = /host:\s*'([^']+)'/.exec(block);
|
|
95
|
+
const portMatch = /port:\s*(\d+)/.exec(block);
|
|
96
|
+
return {
|
|
97
|
+
host: hostMatch?.[1] ?? '',
|
|
98
|
+
port: portMatch !== null ? Number(portMatch[1]) : undefined,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** Full file-level state for the settings card and the boot re-assert. */
|
|
102
|
+
export function lanBindState(profile, home = dshHome()) {
|
|
103
|
+
const state = managedBindOf(readPatchContent(profilePatchFile(profile, home)));
|
|
104
|
+
if (state === undefined)
|
|
105
|
+
return { blockPresent: false };
|
|
106
|
+
return { blockPresent: true, host: state.host, port: state.port };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Write (or rewrite) the managed block with the given bind. The rest of the
|
|
110
|
+
* patch file is preserved; the block is rewritten atomically (unique temp
|
|
111
|
+
* file + rename, so concurrent writers can never rename a half-written
|
|
112
|
+
* file) with the original file's permissions preserved. A hand-truncated
|
|
113
|
+
* unterminated block (BEGIN marker without END, which stripManagedBlock
|
|
114
|
+
* cannot match) is truncated at its BEGIN marker first so the rewrite can
|
|
115
|
+
* never stack a second webserver row onto the orphan.
|
|
116
|
+
*/
|
|
117
|
+
export function writeLanBind(host, port, profile, home = dshHome()) {
|
|
118
|
+
const file = profilePatchFile(profile, home);
|
|
119
|
+
const stripped = stripManagedBlock(readPatchContent(file));
|
|
120
|
+
const orphanBegin = stripped.indexOf(LAN_BIND_BLOCK_BEGIN);
|
|
121
|
+
const base = (orphanBegin === -1 ? stripped : stripped.slice(0, orphanBegin)).trimEnd();
|
|
122
|
+
const content = `${base}\n\n${managedBlock(host, port)}`;
|
|
123
|
+
const mode = existsSync(file) ? statSync(file).mode & 0o777 : 0o600;
|
|
124
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
125
|
+
const temp = `${file}.remote-web-ui-tmp-${process.pid.toString(36)}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
126
|
+
writeFileSync(temp, content, { mode });
|
|
127
|
+
renameSync(temp, file);
|
|
128
|
+
}
|
|
@@ -8,14 +8,19 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
10
10
|
import type { Duplex } from 'node:stream';
|
|
11
|
+
import type { InnerAuth } from './inner-auth.ts';
|
|
11
12
|
/**
|
|
12
13
|
* Pipe one HTTP request to loopback and stream the response back.
|
|
13
14
|
* @param req - the already-gated outer request.
|
|
14
15
|
* @param res - the outer response.
|
|
15
16
|
* @param port - local webServer port.
|
|
16
17
|
* @param upstreamPath - path + query on 127.0.0.1 (must start with `/`).
|
|
18
|
+
* @param auth - when given, the process's inner browser-auth credential is
|
|
19
|
+
* attached so the connection plugin's /api route (fence + browser auth,
|
|
20
|
+
* authority-bound cookie, no loopback exemption on this cohort) accepts
|
|
21
|
+
* the re-issued request; a 401 answer invalidates the cached credential.
|
|
17
22
|
*/
|
|
18
|
-
export declare function proxyLoopbackHttp(req: IncomingMessage, res: ServerResponse, port: number, upstreamPath: string): void;
|
|
23
|
+
export declare function proxyLoopbackHttp(req: IncomingMessage, res: ServerResponse, port: number, upstreamPath: string, auth?: InnerAuth): void;
|
|
19
24
|
/**
|
|
20
25
|
* Rebuild a WebSocket handshake as loopback-shaped and pipe both directions.
|
|
21
26
|
* @param req - the already-gated upgrade request.
|
|
@@ -23,6 +28,8 @@ export declare function proxyLoopbackHttp(req: IncomingMessage, res: ServerRespo
|
|
|
23
28
|
* @param head - bytes already read past the handshake.
|
|
24
29
|
* @param port - local webServer port.
|
|
25
30
|
* @param upstreamPath - path + query on 127.0.0.1.
|
|
31
|
+
* @param cookie - the inner browser-auth credential for the handshake, when
|
|
32
|
+
* the upstream route enforces it (the gateway event-stream mux does).
|
|
26
33
|
*/
|
|
27
|
-
export declare function proxyLoopbackUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer, port: number, upstreamPath: string): void;
|
|
34
|
+
export declare function proxyLoopbackUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer, port: number, upstreamPath: string, cookie?: string): void;
|
|
28
35
|
//# sourceMappingURL=loopback-proxy.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loopback-proxy.d.ts","sourceRoot":"","sources":["../../src/loopback-proxy.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAuB,cAAc,EAAE,MAAM,WAAW,CAAA;AAGrF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"loopback-proxy.d.ts","sourceRoot":"","sources":["../../src/loopback-proxy.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAuB,cAAc,EAAE,MAAM,WAAW,CAAA;AAGrF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAoBhD;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EACpB,IAAI,CAAC,EAAE,SAAS,GACf,IAAI,CAIN;AAmDD;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,eAAe,EACpB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EACpB,MAAM,CAAC,EAAE,MAAM,GACd,IAAI,CAoCN"}
|
|
@@ -31,8 +31,17 @@ const HTTP_FORWARD_RESPONSE_HEADERS = [
|
|
|
31
31
|
* @param res - the outer response.
|
|
32
32
|
* @param port - local webServer port.
|
|
33
33
|
* @param upstreamPath - path + query on 127.0.0.1 (must start with `/`).
|
|
34
|
+
* @param auth - when given, the process's inner browser-auth credential is
|
|
35
|
+
* attached so the connection plugin's /api route (fence + browser auth,
|
|
36
|
+
* authority-bound cookie, no loopback exemption on this cohort) accepts
|
|
37
|
+
* the re-issued request; a 401 answer invalidates the cached credential.
|
|
34
38
|
*/
|
|
35
|
-
export function proxyLoopbackHttp(req, res, port, upstreamPath) {
|
|
39
|
+
export function proxyLoopbackHttp(req, res, port, upstreamPath, auth) {
|
|
40
|
+
void Promise.resolve(auth?.ready())
|
|
41
|
+
.catch(() => undefined)
|
|
42
|
+
.then((cookie) => { pipeLoopbackHttp(req, res, port, upstreamPath, auth, typeof cookie === 'string' ? cookie : undefined); });
|
|
43
|
+
}
|
|
44
|
+
function pipeLoopbackHttp(req, res, port, upstreamPath, auth, cookie) {
|
|
36
45
|
const headers = {
|
|
37
46
|
host: `127.0.0.1:${String(port)}`,
|
|
38
47
|
'sec-fetch-site': 'same-origin',
|
|
@@ -46,6 +55,8 @@ export function proxyLoopbackHttp(req, res, port, upstreamPath) {
|
|
|
46
55
|
const accept = req.headers.accept;
|
|
47
56
|
if (typeof accept === 'string')
|
|
48
57
|
headers.accept = accept;
|
|
58
|
+
if (cookie !== undefined)
|
|
59
|
+
headers.cookie = cookie;
|
|
49
60
|
const upstream = httpRequest({
|
|
50
61
|
host: '127.0.0.1',
|
|
51
62
|
port,
|
|
@@ -53,6 +64,11 @@ export function proxyLoopbackHttp(req, res, port, upstreamPath) {
|
|
|
53
64
|
method: req.method,
|
|
54
65
|
headers,
|
|
55
66
|
}, (upstreamRes) => {
|
|
67
|
+
// The cached credential went stale (secret rotation, credential store
|
|
68
|
+
// reset): drop it so the next request re-redeems. The in-flight response
|
|
69
|
+
// still pipes through untouched.
|
|
70
|
+
if (upstreamRes.statusCode === 401 && cookie !== undefined)
|
|
71
|
+
auth?.invalidate();
|
|
56
72
|
const out = {};
|
|
57
73
|
for (const name of HTTP_FORWARD_RESPONSE_HEADERS) {
|
|
58
74
|
const value = upstreamRes.headers[name];
|
|
@@ -78,14 +94,18 @@ export function proxyLoopbackHttp(req, res, port, upstreamPath) {
|
|
|
78
94
|
* @param head - bytes already read past the handshake.
|
|
79
95
|
* @param port - local webServer port.
|
|
80
96
|
* @param upstreamPath - path + query on 127.0.0.1.
|
|
97
|
+
* @param cookie - the inner browser-auth credential for the handshake, when
|
|
98
|
+
* the upstream route enforces it (the gateway event-stream mux does).
|
|
81
99
|
*/
|
|
82
|
-
export function proxyLoopbackUpgrade(req, socket, head, port, upstreamPath) {
|
|
100
|
+
export function proxyLoopbackUpgrade(req, socket, head, port, upstreamPath, cookie) {
|
|
83
101
|
const lines = [
|
|
84
102
|
`GET ${upstreamPath} HTTP/1.1`,
|
|
85
103
|
`Host: 127.0.0.1:${String(port)}`,
|
|
86
104
|
'Upgrade: websocket',
|
|
87
105
|
'Connection: Upgrade',
|
|
88
106
|
];
|
|
107
|
+
if (cookie !== undefined)
|
|
108
|
+
lines.push(`Cookie: ${cookie}`);
|
|
89
109
|
for (const name of WS_FORWARD_HEADERS) {
|
|
90
110
|
const value = req.headers[name];
|
|
91
111
|
if (value === undefined)
|
|
@@ -94,6 +114,8 @@ export function proxyLoopbackUpgrade(req, socket, head, port, upstreamPath) {
|
|
|
94
114
|
}
|
|
95
115
|
const handshake = `${lines.join('\r\n')}\r\n\r\n`;
|
|
96
116
|
const upstream = connect(port, '127.0.0.1');
|
|
117
|
+
socket.setKeepAlive(true, 20_000);
|
|
118
|
+
upstream.setKeepAlive(true, 20_000);
|
|
97
119
|
const tearDown = () => {
|
|
98
120
|
upstream.destroy();
|
|
99
121
|
socket.destroy();
|
package/lib/types/pairing.d.ts
CHANGED
|
@@ -7,8 +7,12 @@
|
|
|
7
7
|
* Security invariants:
|
|
8
8
|
* - One active token at a time; `issue()` replaces it, so a refreshed QR
|
|
9
9
|
* immediately invalidates the previous link.
|
|
10
|
-
* - A token
|
|
11
|
-
*
|
|
10
|
+
* - A token stays re-usable until it expires or is replaced: the first
|
|
11
|
+
* successful `accept()` marks it consumed, but the same link may pair
|
|
12
|
+
* again within the window (each re-accept mints a fresh device session).
|
|
13
|
+
* Mobile flows routinely split across cookie contexts (camera preview to
|
|
14
|
+
* in-app browser to the system browser), and the later context must be
|
|
15
|
+
* able to complete its own pairing from the same link.
|
|
12
16
|
* - Tokens expire; `accept()` on an expired token is refused like an
|
|
13
17
|
* unknown one (no oracle for validity).
|
|
14
18
|
* - `stop()` revokes every device session and clears the token, so paired
|
|
@@ -44,7 +48,15 @@ export interface TokenRecord {
|
|
|
44
48
|
/** LAN IP literal the QR link was built from (optional; default first). */
|
|
45
49
|
address?: string;
|
|
46
50
|
}
|
|
47
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* Default idle-expiry window: 30 days without a heartbeat or a gated
|
|
53
|
+
* request. The reopen service worker refreshes lastSeenAt on every
|
|
54
|
+
* navigation it serves, so the window only runs out through genuine
|
|
55
|
+
* disuse; 30 days matches the browser-credential lifetimes the surrounding
|
|
56
|
+
* flow was built around, while the effective device lifetime stays shorter
|
|
57
|
+
* than the 365-day cookie because of this sweep. Override per deployment
|
|
58
|
+
* through the idleExpireMs config.
|
|
59
|
+
*/
|
|
48
60
|
export declare const DEFAULT_IDLE_EXPIRE_MS: number;
|
|
49
61
|
/** One paired device session, keyed by the device id stored in its cookie. */
|
|
50
62
|
export interface DeviceSession {
|
|
@@ -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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"}
|
package/lib/types/pairing.js
CHANGED
|
@@ -7,8 +7,12 @@
|
|
|
7
7
|
* Security invariants:
|
|
8
8
|
* - One active token at a time; `issue()` replaces it, so a refreshed QR
|
|
9
9
|
* immediately invalidates the previous link.
|
|
10
|
-
* - A token
|
|
11
|
-
*
|
|
10
|
+
* - A token stays re-usable until it expires or is replaced: the first
|
|
11
|
+
* successful `accept()` marks it consumed, but the same link may pair
|
|
12
|
+
* again within the window (each re-accept mints a fresh device session).
|
|
13
|
+
* Mobile flows routinely split across cookie contexts (camera preview to
|
|
14
|
+
* in-app browser to the system browser), and the later context must be
|
|
15
|
+
* able to complete its own pairing from the same link.
|
|
12
16
|
* - Tokens expire; `accept()` on an expired token is refused like an
|
|
13
17
|
* unknown one (no oracle for validity).
|
|
14
18
|
* - `stop()` revokes every device session and clears the token, so paired
|
|
@@ -19,8 +23,16 @@
|
|
|
19
23
|
import { randomBytes } from 'node:crypto';
|
|
20
24
|
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
21
25
|
import { dirname } from 'node:path';
|
|
22
|
-
/**
|
|
23
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Default idle-expiry window: 30 days without a heartbeat or a gated
|
|
28
|
+
* request. The reopen service worker refreshes lastSeenAt on every
|
|
29
|
+
* navigation it serves, so the window only runs out through genuine
|
|
30
|
+
* disuse; 30 days matches the browser-credential lifetimes the surrounding
|
|
31
|
+
* flow was built around, while the effective device lifetime stays shorter
|
|
32
|
+
* than the 365-day cookie because of this sweep. Override per deployment
|
|
33
|
+
* through the idleExpireMs config.
|
|
34
|
+
*/
|
|
35
|
+
export const DEFAULT_IDLE_EXPIRE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
24
36
|
/** Cap on the persisted/displayed User-Agent string. */
|
|
25
37
|
const MAX_USER_AGENT_CHARS = 180;
|
|
26
38
|
/** Thrown by issue() for an address outside the sampled LAN literals. */
|
|
@@ -243,8 +255,10 @@ export class PairingService {
|
|
|
243
255
|
*/
|
|
244
256
|
accept(token, userAgent) {
|
|
245
257
|
const record = this.tokens.get(token);
|
|
246
|
-
|
|
247
|
-
|
|
258
|
+
// A consumed token stays a valid bearer credential until expiry or
|
|
259
|
+
// replacement (see the module doc): re-accept mints a fresh device.
|
|
260
|
+
if (record === undefined || this.stopped || this.clock.now() > record.expiresAt) {
|
|
261
|
+
return { ok: false, code: 'invalid' };
|
|
248
262
|
}
|
|
249
263
|
record.consumed = true;
|
|
250
264
|
const deviceId = this.clock.randomToken();
|
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The remote desktop data channel: `/remote` is this plugin's own prefix, so
|
|
3
|
-
* the paired-device cookie is the access control
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
* the paired-device cookie is the access control. After that gate, every
|
|
4
|
+
* fenced same-origin path the browser rewrote here is re-issued to 127.0.0.1
|
|
5
|
+
* as a loopback-shaped request so sibling plugin fences (and the connection
|
|
6
|
+
* plugin's `/api`) accept it — no `--trusted-host` and no per-plugin pairing
|
|
7
|
+
* consult.
|
|
8
8
|
*
|
|
9
9
|
* Security model:
|
|
10
10
|
* - While `requirePairingForLan` is on (default), every request must carry a
|
|
11
11
|
* live paired-device cookie, enforced before any bytes are forwarded and
|
|
12
12
|
* before any host call. With the policy off, the cookie gate is skipped
|
|
13
|
-
* (the
|
|
14
|
-
* -
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
13
|
+
* (the local-only denials below still apply).
|
|
14
|
+
* - A paired remote desktop is a full-control credential: the browser half
|
|
15
|
+
* flips the official UI into host mode (the transport ownsHost hook), so
|
|
16
|
+
* the configuration plane (settings, credentials, presets, deliverables)
|
|
17
|
+
* rides this channel like every other call. The four control planes in
|
|
18
|
+
* LOCAL_ONLY_PREFIXES stay physically local.
|
|
19
19
|
* - Everything else is HTTP- or WebSocket-proxied to the local port with
|
|
20
20
|
* Host rewritten, Origin and cookies dropped, and a synthetic same-origin
|
|
21
21
|
* browser marker added after authentication. Plugin loopback fences then
|
|
22
22
|
* pass. The pairing cookie never leaves this process.
|
|
23
23
|
*/
|
|
24
|
+
import type { IncomingMessage } from 'node:http';
|
|
24
25
|
import type { WebRoute, WebUpgradeRoute } from '@deepseek-ai/dsh-host-webserver';
|
|
25
26
|
import type { PairingService } from './pairing.ts';
|
|
26
|
-
|
|
27
|
+
import type { InnerAuth } from './inner-auth.ts';
|
|
28
|
+
export { DESKTOP_LAUNCHER_PATH, LOCAL_ONLY_PREFIXES, PLUGIN_MANAGER_PATH, REMOTE_API_PATHS, REMOTE_DEVICE_HEADER, REMOTE_DEVICE_QUERY, REMOTE_PREFIX, REMOTE_UPGRADE_PATHS, WEB_UI_SETTINGS_BRIDGE_PATH, localOnlyDenial, } from './remote-methods.ts';
|
|
27
29
|
export { REMOTE_API_PREFIX } from './remote-methods.ts';
|
|
28
30
|
/** Route-family dependencies. */
|
|
29
31
|
export interface RemoteApiDeps {
|
|
@@ -38,6 +40,13 @@ export interface RemoteApiDeps {
|
|
|
38
40
|
* edit takes effect without a restart. Defaults to true.
|
|
39
41
|
*/
|
|
40
42
|
requirePairingForLan?: boolean | (() => boolean);
|
|
43
|
+
/**
|
|
44
|
+
* The process's inner browser-auth credential attached to re-issued
|
|
45
|
+
* requests (the connection plugin's /api route enforces that cookie and
|
|
46
|
+
* the pairing gate above already ran). Undefined keeps the previous
|
|
47
|
+
* cookie-less behavior — the inner route then answers 401 on this cohort.
|
|
48
|
+
*/
|
|
49
|
+
auth?: InnerAuth;
|
|
41
50
|
}
|
|
42
51
|
/**
|
|
43
52
|
* Map `/remote/...` to the inner path, or undefined when the outer path is
|
|
@@ -45,10 +54,18 @@ export interface RemoteApiDeps {
|
|
|
45
54
|
*/
|
|
46
55
|
export declare function innerPathOf(pathname: string): string | undefined;
|
|
47
56
|
/**
|
|
48
|
-
* Whether a paired inner path must stay physically local
|
|
57
|
+
* Whether a paired inner path must stay physically local (delegates to the
|
|
58
|
+
* shared LOCAL_ONLY_PREFIXES table).
|
|
49
59
|
* @returns a denial message, or undefined when the path may be proxied.
|
|
50
60
|
*/
|
|
51
61
|
export declare function loopbackOnlyDenial(innerPath: string): string | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Resolve a live device credential for a gated HTTP request: the pairing
|
|
64
|
+
* cookie first, then the cookieless header the boot patch attaches. Unknown
|
|
65
|
+
* or revoked ids are a no-op - a stale id never re-arms a device.
|
|
66
|
+
* @returns the touched device id, or undefined when neither credential is live.
|
|
67
|
+
*/
|
|
68
|
+
export declare function pairedDeviceIdOf(req: IncomingMessage, service: PairingService): string | undefined;
|
|
52
69
|
/**
|
|
53
70
|
* Build the remote desktop channel HTTP routes.
|
|
54
71
|
* @param deps - pairing service + local port + live pairing policy.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-api.d.ts","sourceRoot":"","sources":["../../src/remote-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;
|
|
1
|
+
{"version":3,"file":"remote-api.d.ts","sourceRoot":"","sources":["../../src/remote-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,WAAW,CAAA;AAEhE,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAA;AAChF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAGlD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAUhD,OAAO,EACL,qBAAqB,EACrB,mBAAmB,EACnB,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,EACpB,mBAAmB,EACnB,aAAa,EACb,oBAAoB,EACpB,2BAA2B,EAC3B,eAAe,GAChB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAgBvD,iCAAiC;AACjC,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,OAAO,EAAE,cAAc,CAAA;IACvB,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,CAAA;IAChD;;;;;OAKG;IACH,IAAI,CAAC,EAAE,SAAS,CAAA;CACjB;AAWD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAUhE;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAExE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,eAAe,EAAE,OAAO,EAAE,cAAc,GAAG,MAAM,GAAG,SAAS,CAQlG;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,QAAQ,EAAE,CA0CnE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAWzF;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,aAAa,GAAG,eAAe,EAAE,CA2CjF"}
|