linkshell-cli 0.10.45 → 0.10.47

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 (44) hide show
  1. package/README.md +2 -0
  2. package/dist/cli/src/auth.d.ts +6 -2
  3. package/dist/cli/src/auth.js +12 -4
  4. package/dist/cli/src/auth.js.map +1 -1
  5. package/dist/cli/src/commands/desktop-workspace-state.d.ts +19 -0
  6. package/dist/cli/src/commands/desktop-workspace-state.js +92 -0
  7. package/dist/cli/src/commands/desktop-workspace-state.js.map +1 -0
  8. package/dist/cli/src/commands/desktop-workspace.d.ts +12 -0
  9. package/dist/cli/src/commands/desktop-workspace.js +415 -0
  10. package/dist/cli/src/commands/desktop-workspace.js.map +1 -0
  11. package/dist/cli/src/commands/desktop.d.ts +5 -5
  12. package/dist/cli/src/commands/desktop.js +55 -5
  13. package/dist/cli/src/commands/desktop.js.map +1 -1
  14. package/dist/cli/src/commands/login.d.ts +3 -1
  15. package/dist/cli/src/commands/login.js +44 -26
  16. package/dist/cli/src/commands/login.js.map +1 -1
  17. package/dist/cli/src/index.js +15 -0
  18. package/dist/cli/src/index.js.map +1 -1
  19. package/dist/cli/src/pro-membership.d.ts +16 -0
  20. package/dist/cli/src/pro-membership.js +91 -0
  21. package/dist/cli/src/pro-membership.js.map +1 -0
  22. package/dist/cli/tsconfig.tsbuildinfo +1 -1
  23. package/dist/client-core/src/pro-membership.d.ts +18 -0
  24. package/dist/client-core/src/pro-membership.js +109 -0
  25. package/dist/client-core/src/pro-membership.js.map +1 -0
  26. package/dist/host/src/input.d.ts +29 -2
  27. package/dist/host/src/input.js +99 -4
  28. package/dist/host/src/input.js.map +1 -1
  29. package/dist/host/src/screen-session.d.ts +35 -0
  30. package/dist/host/src/screen-session.js +26 -0
  31. package/dist/host/src/screen-session.js.map +1 -0
  32. package/dist/host/src/screen-viewer.js +293 -30
  33. package/dist/host/src/screen-viewer.js.map +1 -1
  34. package/dist/host/src/screen.d.ts +3 -1
  35. package/dist/host/src/screen.js +454 -160
  36. package/dist/host/src/screen.js.map +1 -1
  37. package/package.json +6 -4
  38. package/src/auth.ts +9 -4
  39. package/src/commands/desktop-workspace-state.ts +87 -0
  40. package/src/commands/desktop-workspace.ts +353 -0
  41. package/src/commands/desktop.ts +49 -5
  42. package/src/commands/login.ts +33 -34
  43. package/src/index.ts +16 -0
  44. package/src/pro-membership.ts +82 -0
package/README.md CHANGED
@@ -25,6 +25,8 @@ Connect your phone one of two ways:
25
25
  linkshell pair # scan the QR code in the app
26
26
  ```
27
27
 
28
+ Signed-in Free accounts can redeem a Pro activation code in the LinkShell phone app, Mac app or web workspace's account controls, or under Connection Settings → Membership & activation codes in Apple Watch independent mode. [Buy a code on Liandong Xiaopu](https://pay.ldxp.cn/item/7lgv1s). [iTool Settings → Upgrade to Pro](https://itool.tech/en/settings) remains available with the same account, including for renewing an active Pro membership.
29
+
28
30
  Start sessions from the phone, or launch agents from your terminal so they can be handed over at any time:
29
31
 
30
32
  ```bash
@@ -16,8 +16,12 @@ export { SUPABASE_URL, SUPABASE_ANON_KEY };
16
16
  * Refresh the access token using the refresh token.
17
17
  * Returns updated tokens or null if refresh failed.
18
18
  */
19
- export declare function refreshAccessToken(): Promise<AuthTokens | null>;
19
+ export declare function refreshAccessToken(options?: {
20
+ signal?: AbortSignal;
21
+ }): Promise<AuthTokens | null>;
20
22
  /**
21
23
  * Get a valid access token, refreshing if needed.
22
24
  */
23
- export declare function getValidToken(): Promise<string | null>;
25
+ export declare function getValidToken(options?: {
26
+ signal?: AbortSignal;
27
+ }): Promise<string | null>;
@@ -43,7 +43,9 @@ let inflightRefresh = null;
43
43
  * Refresh the access token using the refresh token.
44
44
  * Returns updated tokens or null if refresh failed.
45
45
  */
46
- export async function refreshAccessToken() {
46
+ export async function refreshAccessToken(options = {}) {
47
+ if (options.signal?.aborted)
48
+ return null;
47
49
  if (inflightRefresh)
48
50
  return inflightRefresh;
49
51
  inflightRefresh = (async () => {
@@ -60,7 +62,7 @@ export async function refreshAccessToken() {
60
62
  apikey: SUPABASE_ANON_KEY,
61
63
  },
62
64
  body: JSON.stringify({ refresh_token: auth.refreshToken }),
63
- signal: AbortSignal.timeout(30_000),
65
+ signal: options.signal ? AbortSignal.any([options.signal, AbortSignal.timeout(30_000)]) : AbortSignal.timeout(30_000),
64
66
  });
65
67
  if (!res.ok)
66
68
  return null;
@@ -72,6 +74,10 @@ export async function refreshAccessToken() {
72
74
  userId: body.user.id,
73
75
  email: body.user.email ?? auth.email,
74
76
  };
77
+ // A login or logout may finish while the refresh is in flight.
78
+ const current = loadAuth();
79
+ if (options.signal?.aborted || tokens.userId !== auth.userId || current?.userId !== auth.userId || current.refreshToken !== auth.refreshToken)
80
+ return null;
75
81
  saveAuth(tokens);
76
82
  return tokens;
77
83
  }
@@ -86,13 +92,15 @@ export async function refreshAccessToken() {
86
92
  /**
87
93
  * Get a valid access token, refreshing if needed.
88
94
  */
89
- export async function getValidToken() {
95
+ export async function getValidToken(options = {}) {
96
+ if (options.signal?.aborted)
97
+ return null;
90
98
  const auth = loadAuth();
91
99
  if (!auth)
92
100
  return null;
93
101
  // If token expires in less than 120s, refresh
94
102
  if (auth.expiresAt - Date.now() < 120_000) {
95
- const refreshed = await refreshAccessToken();
103
+ const refreshed = await refreshAccessToken(options);
96
104
  return refreshed?.accessToken ?? null;
97
105
  }
98
106
  return auth.accessToken;
@@ -1 +1 @@
1
- {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../../src/auth.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../../src/auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import { type Identity } from "@linkshell/wire";
2
+ interface DesktopState {
3
+ schema: 1;
4
+ identity: Identity;
5
+ storage: Record<string, unknown>;
6
+ }
7
+ export declare function validStorageKey(key: unknown): key is string;
8
+ /** The browser origin changes on every launch; paired identity and drafts must not. */
9
+ export declare class DesktopClientState {
10
+ private readonly file;
11
+ private value;
12
+ private writes;
13
+ private constructor();
14
+ static open(home: string): Promise<DesktopClientState>;
15
+ snapshot(): Pick<DesktopState, "identity" | "storage">;
16
+ set(key: string, value: unknown): Promise<void>;
17
+ flush(): Promise<void>;
18
+ }
19
+ export {};
@@ -0,0 +1,92 @@
1
+ import { chmod, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { randomBytes } from "node:crypto";
4
+ import { createIdentity, idOf, sign, verifySignature } from "@linkshell/wire";
5
+ const MAX_STATE_BYTES = 8 * 1024 * 1024;
6
+ const MAX_KEYS = 2048;
7
+ export function validStorageKey(key) {
8
+ return typeof key === "string" && key.length > 0 && key.length <= 1024 &&
9
+ [...key].every((character) => character.charCodeAt(0) >= 32 && character.charCodeAt(0) !== 127) &&
10
+ !["__proto__", "prototype", "constructor"].includes(key);
11
+ }
12
+ function validIdentity(value) {
13
+ if (!value || typeof value !== "object")
14
+ return false;
15
+ const identity = value;
16
+ if (![identity.id, identity.signKey, identity.boxKey, identity.signSecret, identity.boxSecret]
17
+ .every((part) => typeof part === "string" && /^[A-Za-z0-9_-]{16,128}$/.test(part)))
18
+ return false;
19
+ try {
20
+ return idOf(identity.signKey) === identity.id &&
21
+ verifySignature(identity.signKey, "linkshell-desktop-identity", sign(identity, "linkshell-desktop-identity"));
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ /** The browser origin changes on every launch; paired identity and drafts must not. */
28
+ export class DesktopClientState {
29
+ file;
30
+ value;
31
+ writes = Promise.resolve();
32
+ constructor(file, value) {
33
+ this.file = file;
34
+ this.value = value;
35
+ }
36
+ static async open(home) {
37
+ await mkdir(home, { recursive: true, mode: 0o700 });
38
+ const file = join(home, "desktop-client.json");
39
+ try {
40
+ const initial = { schema: 1, identity: createIdentity(), storage: {} };
41
+ await writeFile(file, JSON.stringify(initial), { flag: "wx", mode: 0o600 });
42
+ }
43
+ catch (error) {
44
+ if (error.code !== "EEXIST")
45
+ throw error;
46
+ }
47
+ if ((await stat(file)).size > MAX_STATE_BYTES)
48
+ throw new Error("桌面偏好文件过大,请检查本地存储");
49
+ const value = JSON.parse(await readFile(file, "utf8"));
50
+ if (value.schema !== 1 || !validIdentity(value.identity) || !value.storage || typeof value.storage !== "object" ||
51
+ Array.isArray(value.storage) || Object.keys(value.storage).some((key) => !validStorageKey(key))) {
52
+ // Replacing a damaged identity would silently lose every existing pairing.
53
+ throw new Error("桌面设备身份文件无法读取,请保留此文件并检查本地存储");
54
+ }
55
+ await chmod(file, 0o600);
56
+ return new DesktopClientState(file, value);
57
+ }
58
+ snapshot() {
59
+ return { identity: this.value.identity, storage: this.value.storage };
60
+ }
61
+ set(key, value) {
62
+ if (!validStorageKey(key))
63
+ return Promise.reject(new Error("无效的偏好名称"));
64
+ const write = this.writes.then(async () => {
65
+ const storage = { ...this.value.storage };
66
+ if (value === null)
67
+ delete storage[key];
68
+ else
69
+ storage[key] = value;
70
+ if (Object.keys(storage).length > MAX_KEYS)
71
+ throw new Error("桌面偏好条目过多");
72
+ const next = { ...this.value, storage };
73
+ const text = JSON.stringify(next);
74
+ if (Buffer.byteLength(text) > MAX_STATE_BYTES)
75
+ throw new Error("桌面偏好存储空间已满");
76
+ const temporary = `${this.file}.${randomBytes(12).toString("hex")}.tmp`;
77
+ try {
78
+ await writeFile(temporary, text, { flag: "wx", mode: 0o600 });
79
+ await rename(temporary, this.file);
80
+ this.value = next;
81
+ }
82
+ finally {
83
+ await rm(temporary, { force: true });
84
+ }
85
+ });
86
+ // A rejected write must not prevent later, smaller preferences from saving.
87
+ this.writes = write.catch(() => { });
88
+ return write;
89
+ }
90
+ async flush() { await this.writes; }
91
+ }
92
+ //# sourceMappingURL=desktop-workspace-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"desktop-workspace-state.js","sourceRoot":"","sources":["../../../../src/commands/desktop-workspace-state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACvF,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,IAAI,EAAE,eAAe,EAAiB,MAAM,iBAAiB,CAAC;AAE7F,MAAM,eAAe,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;AACxC,MAAM,QAAQ,GAAG,IAAI,CAAC;AAQtB,MAAM,UAAU,eAAe,CAAC,GAAY;IAC1C,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,GAAG,CAAC,MAAM,IAAI,IAAI;QACpE,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC;QAC/F,CAAC,CAAC,WAAW,EAAE,WAAW,EAAE,aAAa,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;AAC7D,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACnC,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACtD,MAAM,QAAQ,GAAG,KAAiB,CAAC;IACnC,IAAI,CAAC,CAAC,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,SAAS,CAAC;SAC3F,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,IAAI,yBAAyB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IACnG,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,EAAE;YAC3C,eAAe,CAAC,QAAQ,CAAC,OAAO,EAAE,4BAA4B,EAAE,IAAI,CAAC,QAAQ,EAAE,4BAA4B,CAAC,CAAC,CAAC;IAClH,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,KAAK,CAAC;IAAC,CAAC;AAC3B,CAAC;AAED,uFAAuF;AACvF,MAAM,OAAO,kBAAkB;IAGQ;IAAsB;IAFnD,MAAM,GAAkB,OAAO,CAAC,OAAO,EAAE,CAAC;IAElD,YAAqC,IAAY,EAAU,KAAmB;QAAzC,SAAI,GAAJ,IAAI,CAAQ;QAAU,UAAK,GAAL,KAAK,CAAc;IAAG,CAAC;IAElF,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAY;QAC5B,MAAM,KAAK,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACpD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,qBAAqB,CAAC,CAAC;QAC/C,IAAI,CAAC;YACH,MAAM,OAAO,GAAiB,EAAE,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,cAAc,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;YACrF,MAAM,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAC9E,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;gBAAE,MAAM,KAAK,CAAC;QACtE,CAAC;QACD,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,eAAe;YAAE,MAAM,IAAI,KAAK,CAAC,kBAAkB,CAAC,CAAC;QACnF,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAiB,CAAC;QACvE,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,IAAI,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ;YAC3G,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;YACpG,2EAA2E;YAC3E,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;QAChD,CAAC;QACD,MAAM,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QACzB,OAAO,IAAI,kBAAkB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAC7C,CAAC;IAED,QAAQ;QACN,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;IACxE,CAAC;IAED,GAAG,CAAC,GAAW,EAAE,KAAc;QAC7B,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC;YAAE,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;QACvE,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE;YACxC,MAAM,OAAO,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;YAC1C,IAAI,KAAK,KAAK,IAAI;gBAAE,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC;;gBACnC,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;YAC1B,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,GAAG,QAAQ;gBAAE,MAAM,IAAI,KAAK,CAAC,UAAU,CAAC,CAAC;YACxE,MAAM,IAAI,GAAiB,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,CAAC;YACtD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;YAClC,IAAI,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,eAAe;gBAAE,MAAM,IAAI,KAAK,CAAC,YAAY,CAAC,CAAC;YAC7E,MAAM,SAAS,GAAG,GAAG,IAAI,CAAC,IAAI,IAAI,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC;YACxE,IAAI,CAAC;gBACH,MAAM,SAAS,CAAC,SAAS,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;gBAC9D,MAAM,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;gBACnC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;YACpB,CAAC;oBAAS,CAAC;gBACT,MAAM,EAAE,CAAC,SAAS,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YACvC,CAAC;QACH,CAAC,CAAC,CAAC;QACH,4EAA4E;QAC5E,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QACpC,OAAO,KAAK,CAAC;IACf,CAAC;IAED,KAAK,CAAC,KAAK,KAAoB,MAAM,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;CACpD"}
@@ -0,0 +1,12 @@
1
+ import type { Readable } from "node:stream";
2
+ export interface DesktopWorkspace {
3
+ url: string;
4
+ stop(): Promise<void>;
5
+ }
6
+ /** A capability-protected UI server owned by the native application's lifetime. */
7
+ export declare function startDesktopWorkspace(options: {
8
+ assets: string;
9
+ home?: string;
10
+ hostSocket?: string;
11
+ }): Promise<DesktopWorkspace>;
12
+ export declare function runDesktopWorkspace(assets: string, input?: Readable): Promise<void>;
@@ -0,0 +1,415 @@
1
+ import { createServer } from "node:http";
2
+ import { randomBytes, timingSafeEqual } from "node:crypto";
3
+ import { readFile, readdir, realpath } from "node:fs/promises";
4
+ import { extname, isAbsolute, join } from "node:path";
5
+ import { homedir } from "node:os";
6
+ import WebSocket, { WebSocketServer } from "ws";
7
+ import { isMethodName, machineInfoSchema, rpcMessageSchema } from "@linkshell/wire";
8
+ import { getValidToken, loadAuth } from "../auth.js";
9
+ import { runLogin } from "./login.js";
10
+ import { runLogout } from "./logout.js";
11
+ import { silenceSqliteWarning } from "./host.js";
12
+ import { DesktopClientState, validStorageKey } from "./desktop-workspace-state.js";
13
+ const MAX_REQUEST_BYTES = 1024 * 1024;
14
+ const MAX_PROXY_BYTES = 40 * 1024 * 1024;
15
+ const MIME = {
16
+ ".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8",
17
+ ".css": "text/css; charset=utf-8", ".svg": "image/svg+xml", ".png": "image/png",
18
+ ".jpg": "image/jpeg", ".webp": "image/webp", ".ico": "image/x-icon",
19
+ ".woff": "font/woff", ".woff2": "font/woff2", ".json": "application/json",
20
+ };
21
+ class HttpError extends Error {
22
+ status;
23
+ constructor(status, message) {
24
+ super(message);
25
+ this.status = status;
26
+ }
27
+ }
28
+ function json(response, status, value) {
29
+ if (response.destroyed || response.writableEnded)
30
+ return;
31
+ response.writeHead(status, {
32
+ "content-type": "application/json; charset=utf-8",
33
+ "cache-control": "no-store", "x-content-type-options": "nosniff",
34
+ "referrer-policy": "no-referrer", "x-frame-options": "DENY",
35
+ });
36
+ response.end(JSON.stringify(value));
37
+ }
38
+ async function body(request) {
39
+ if (request.headers["content-type"]?.split(";")[0]?.trim() !== "application/json") {
40
+ throw new HttpError(415, "请求需要使用 JSON 格式");
41
+ }
42
+ if (Number(request.headers["content-length"]) > MAX_REQUEST_BYTES)
43
+ throw new HttpError(413, "偏好内容过长");
44
+ let length = 0;
45
+ const chunks = [];
46
+ for await (const chunk of request) {
47
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
48
+ length += bytes.length;
49
+ if (length > MAX_REQUEST_BYTES)
50
+ throw new HttpError(413, "偏好内容过长");
51
+ chunks.push(bytes);
52
+ }
53
+ try {
54
+ const value = JSON.parse(Buffer.concat(chunks).toString("utf8"));
55
+ if (value && typeof value === "object" && !Array.isArray(value))
56
+ return value;
57
+ }
58
+ catch { /* Request contents may contain private drafts; never echo them. */ }
59
+ throw new HttpError(400, "无效的 JSON 请求");
60
+ }
61
+ async function loadAssets(directory) {
62
+ if (!isAbsolute(directory))
63
+ throw new Error("桌面工作台资源目录必须使用绝对路径");
64
+ const assets = new Map();
65
+ const root = await realpath(directory);
66
+ const walk = async (path, prefix = "") => {
67
+ for (const entry of await readdir(path, { withFileTypes: true })) {
68
+ if (entry.isDirectory())
69
+ await walk(join(path, entry.name), `${prefix}/${entry.name}`);
70
+ else if (entry.isFile())
71
+ assets.set(`${prefix}/${entry.name}`, await readFile(join(path, entry.name)));
72
+ }
73
+ };
74
+ await walk(root);
75
+ if (!assets.has("/index.html"))
76
+ throw new Error("桌面工作台资源缺失,请重新安装 LinkShell");
77
+ return assets;
78
+ }
79
+ async function localComputer(target, upstreams) {
80
+ // Query the existing socket directly: discovering this computer never starts a host.
81
+ return new Promise((resolve) => {
82
+ const client = new WebSocket(`ws+unix://${target}:/`, { perMessageDeflate: false, handshakeTimeout: 1_500, maxPayload: MAX_PROXY_BYTES });
83
+ upstreams.add(client);
84
+ const finish = (computer) => {
85
+ clearTimeout(timeout);
86
+ resolve(computer);
87
+ client.terminate();
88
+ };
89
+ const timeout = setTimeout(() => finish(), 3_000);
90
+ client.on("error", () => finish());
91
+ client.on("close", () => { upstreams.delete(client); clearTimeout(timeout); resolve(undefined); });
92
+ client.on("open", () => client.send(JSON.stringify({ jsonrpc: "2.0", id: "desktop-info", method: "machine.info", params: {} })));
93
+ client.on("message", (raw) => {
94
+ try {
95
+ const response = JSON.parse(raw.toString());
96
+ if (!response || typeof response !== "object" || !("id" in response) || response.id !== "desktop-info")
97
+ return;
98
+ const parsed = machineInfoSchema.safeParse("result" in response ? response.result : undefined);
99
+ if (!parsed.success) {
100
+ finish();
101
+ return;
102
+ }
103
+ const info = parsed.data;
104
+ finish({
105
+ key: "desktop:local", gateway: "desktop:local",
106
+ machine: {
107
+ id: info.machineId, name: info.hostname.replace(/\.local$/, ""), platform: info.platform,
108
+ // Local transport uses a protected Unix socket, not a relay identity or tunnel.
109
+ signKey: "", boxKey: "", online: true, via: "paired",
110
+ },
111
+ });
112
+ }
113
+ catch {
114
+ finish();
115
+ }
116
+ });
117
+ });
118
+ }
119
+ async function account(signal) {
120
+ const accessToken = await getValidToken({ signal });
121
+ const current = loadAuth();
122
+ if (!current?.userId)
123
+ return {};
124
+ return {
125
+ userId: current.userId, email: current.email,
126
+ ...(accessToken && current.accessToken === accessToken ? { accessToken } : {}),
127
+ };
128
+ }
129
+ function proxyHost(client, target, upstreams) {
130
+ const host = new WebSocket(`ws+unix://${target}:/`, {
131
+ perMessageDeflate: false, handshakeTimeout: 3_000, maxPayload: MAX_PROXY_BYTES,
132
+ });
133
+ upstreams.add(host);
134
+ let pending = [];
135
+ let pendingBytes = 0;
136
+ const fail = (code, reason) => {
137
+ if (client.readyState === WebSocket.OPEN)
138
+ client.close(code, reason);
139
+ host.terminate();
140
+ pending = [];
141
+ };
142
+ const forward = (text) => {
143
+ if (host.bufferedAmount + Buffer.byteLength(text) > MAX_PROXY_BYTES) {
144
+ fail(1013, "连接繁忙,请重试");
145
+ }
146
+ else
147
+ host.send(text, (error) => { if (error)
148
+ fail(1011, "本机连接已断开"); });
149
+ };
150
+ client.on("message", (raw, binary) => {
151
+ if (binary) {
152
+ fail(1008, "不支持此消息格式");
153
+ return;
154
+ }
155
+ const text = raw.toString();
156
+ let message;
157
+ try {
158
+ message = rpcMessageSchema.safeParse(JSON.parse(text));
159
+ }
160
+ catch {
161
+ fail(1008, "无效的 RPC 请求");
162
+ return;
163
+ }
164
+ if (!message.success || typeof message.data.method !== "string") {
165
+ fail(1008, "无效的 RPC 请求");
166
+ return;
167
+ }
168
+ const { method, id } = message.data;
169
+ // Unix clients include privileged native-agent shims; this browser is only a normal client.
170
+ if (method.startsWith("desktop.") || !isMethodName(method)) {
171
+ if (id !== undefined && client.readyState === WebSocket.OPEN) {
172
+ client.send(JSON.stringify({ jsonrpc: "2.0", id, error: { code: -32601, message: "不支持此操作" } }));
173
+ }
174
+ return;
175
+ }
176
+ if (host.readyState === WebSocket.CONNECTING) {
177
+ pendingBytes += Buffer.byteLength(text);
178
+ if (pendingBytes > MAX_PROXY_BYTES) {
179
+ fail(1013, "连接繁忙,请重试");
180
+ return;
181
+ }
182
+ pending.push(text);
183
+ }
184
+ else if (host.readyState === WebSocket.OPEN)
185
+ forward(text);
186
+ });
187
+ host.on("open", () => {
188
+ if (client.readyState !== WebSocket.OPEN) {
189
+ host.terminate();
190
+ return;
191
+ }
192
+ for (const text of pending) {
193
+ if (host.readyState !== WebSocket.OPEN)
194
+ break;
195
+ forward(text);
196
+ }
197
+ pending = [];
198
+ pendingBytes = 0;
199
+ });
200
+ host.on("message", (data, binary) => {
201
+ if (client.readyState !== WebSocket.OPEN)
202
+ return;
203
+ const size = Array.isArray(data) ? data.reduce((total, part) => total + part.length, 0) : data.byteLength;
204
+ if (client.bufferedAmount + size > MAX_PROXY_BYTES) {
205
+ fail(1013, "连接繁忙,请重试");
206
+ return;
207
+ }
208
+ client.send(data, { binary }, (error) => { if (error)
209
+ host.terminate(); });
210
+ });
211
+ host.on("error", () => fail(1013, "本机服务未运行,请在本机服务中启动"));
212
+ host.on("close", () => {
213
+ upstreams.delete(host);
214
+ pending = [];
215
+ if (client.readyState === WebSocket.OPEN)
216
+ client.close(1013, "本机服务已断开");
217
+ });
218
+ client.on("error", () => host.terminate());
219
+ client.on("close", () => { pending = []; host.terminate(); });
220
+ }
221
+ /** A capability-protected UI server owned by the native application's lifetime. */
222
+ export async function startDesktopWorkspace(options) {
223
+ silenceSqliteWarning();
224
+ const home = options.home ?? process.env.LINKSHELL_HOME ?? join(homedir(), ".linkshell");
225
+ const assets = await loadAssets(options.assets);
226
+ const state = await DesktopClientState.open(home);
227
+ const hostSocket = options.hostSocket ?? (await import("@linkshell/host")).hostPaths(home).hostSocket;
228
+ const capability = randomBytes(32).toString("base64url");
229
+ const secret = Buffer.from(capability);
230
+ const hasToken = (value) => {
231
+ const supplied = Buffer.from(value ?? "");
232
+ return supplied.length === secret.length && timingSafeEqual(supplied, secret);
233
+ };
234
+ let workspaceOrigin = "";
235
+ let workspaceHost = "";
236
+ let previewOrigin = "";
237
+ let previewHost = "";
238
+ let login;
239
+ let loginAbort;
240
+ let stopping;
241
+ const peers = new WebSocketServer({ noServer: true, perMessageDeflate: false, maxPayload: MAX_PROXY_BYTES });
242
+ const upstreams = new Set();
243
+ const sockets = new Set();
244
+ const accountAbort = new AbortController();
245
+ const serve = async (request, response) => {
246
+ const host = request.headers.host;
247
+ if (host !== workspaceHost && host !== previewHost)
248
+ throw new HttpError(403, "来源不受信任");
249
+ if (!request.url?.startsWith("/") || request.url.startsWith("//"))
250
+ throw new HttpError(400, "无效的请求地址");
251
+ const url = new URL(request.url, workspaceOrigin);
252
+ const path = url.pathname;
253
+ if (host === previewHost) {
254
+ // A preview service worker can only ever own this other origin and /preview/ scope.
255
+ if (!path.startsWith("/preview/"))
256
+ throw new HttpError(404, "未找到资源");
257
+ }
258
+ else {
259
+ const origin = request.headers.origin;
260
+ if (origin !== undefined && origin !== workspaceOrigin)
261
+ throw new HttpError(403, "来源不受信任");
262
+ if (path.startsWith("/desktop/")) {
263
+ if (!hasToken(request.headers.authorization?.replace(/^Bearer /, "")) ||
264
+ !request.headers.authorization?.startsWith("Bearer "))
265
+ throw new HttpError(401, "桌面连接已过期,请重新打开工作台");
266
+ // Same-origin GET fetches omit Origin. Mutations always send it in WebKit.
267
+ if (!["GET", "HEAD"].includes(request.method ?? "") && origin !== workspaceOrigin)
268
+ throw new HttpError(403, "来源不受信任");
269
+ if (request.method === "GET" && path === "/desktop/bootstrap") {
270
+ json(response, 200, { ...state.snapshot(), localComputer: await localComputer(hostSocket, upstreams) });
271
+ return;
272
+ }
273
+ if (request.method === "GET" && path === "/desktop/local") {
274
+ json(response, 200, { computer: await localComputer(hostSocket, upstreams) });
275
+ return;
276
+ }
277
+ if (request.method === "GET" && path === "/desktop/account") {
278
+ json(response, 200, await account(accountAbort.signal));
279
+ return;
280
+ }
281
+ if (request.method === "PUT" && path === "/desktop/storage") {
282
+ const input = await body(request);
283
+ if (!validStorageKey(input.key) || !Object.hasOwn(input, "value"))
284
+ throw new HttpError(400, "无效的偏好内容");
285
+ try {
286
+ await state.set(input.key, input.value);
287
+ }
288
+ catch {
289
+ throw new HttpError(507, "桌面偏好保存失败,请检查可用空间或缩短内容");
290
+ }
291
+ json(response, 200, { ok: true });
292
+ return;
293
+ }
294
+ if (request.method === "POST" && path === "/desktop/login") {
295
+ if (!login) {
296
+ loginAbort = new AbortController();
297
+ login = runLogin({ signal: loginAbort.signal }).finally(() => { login = undefined; loginAbort = undefined; });
298
+ }
299
+ const result = await login;
300
+ if (!result?.success)
301
+ throw new HttpError(409, "登录未完成,请重试");
302
+ json(response, 200, await account(accountAbort.signal));
303
+ return;
304
+ }
305
+ if (request.method === "POST" && path === "/desktop/logout") {
306
+ loginAbort?.abort();
307
+ await login;
308
+ await runLogout();
309
+ json(response, 200, {});
310
+ return;
311
+ }
312
+ throw new HttpError(404, "未找到操作");
313
+ }
314
+ // Never install a preview service worker in the identity-bearing workspace origin.
315
+ if (path.startsWith("/preview/"))
316
+ throw new HttpError(404, "未找到资源");
317
+ }
318
+ if (!["GET", "HEAD"].includes(request.method ?? ""))
319
+ throw new HttpError(405, "不支持此请求方法");
320
+ const assetPath = path === "/" ? "/index.html" : path;
321
+ const content = path === "/config.js" ? Buffer.from(`window.__LINKSHELL_CONFIG__ = ${JSON.stringify({ desktop: true, deployment: "official", previewOrigin })};\n`) : assets.get(assetPath);
322
+ if (!content)
323
+ throw new HttpError(404, "未找到资源");
324
+ response.writeHead(200, {
325
+ "content-type": MIME[extname(assetPath)] ?? "application/octet-stream", "content-length": content.byteLength,
326
+ "cache-control": "no-store", "x-content-type-options": "nosniff", "referrer-policy": "no-referrer",
327
+ ...(host === workspaceHost ? { "x-frame-options": "DENY", "content-security-policy": "frame-ancestors 'none'" } : {}),
328
+ });
329
+ response.end(request.method === "HEAD" ? undefined : content);
330
+ };
331
+ const server = createServer((request, response) => {
332
+ void serve(request, response).catch((error) => {
333
+ json(response, error instanceof HttpError ? error.status : 500, { error: error instanceof HttpError ? error.message : "桌面工作台暂时无法完成操作,请重试" });
334
+ });
335
+ });
336
+ server.requestTimeout = 6 * 60_000;
337
+ server.headersTimeout = 10_000;
338
+ server.on("connection", (socket) => { sockets.add(socket); socket.on("close", () => sockets.delete(socket)); });
339
+ server.on("upgrade", (request, socket, head) => {
340
+ let url;
341
+ try {
342
+ url = new URL(request.url ?? "", workspaceOrigin);
343
+ }
344
+ catch {
345
+ socket.destroy();
346
+ return;
347
+ }
348
+ if (request.headers.host !== workspaceHost || request.headers.origin !== workspaceOrigin ||
349
+ !request.url?.startsWith("/") || request.url.startsWith("//") || url.pathname !== "/desktop/host" ||
350
+ !hasToken(url.searchParams.get("token") ?? undefined)) {
351
+ socket.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
352
+ return;
353
+ }
354
+ peers.handleUpgrade(request, socket, head, (client) => proxyHost(client, hostSocket, upstreams));
355
+ });
356
+ await new Promise((resolve, reject) => {
357
+ server.once("error", reject);
358
+ server.listen(0, "127.0.0.1", () => { server.off("error", reject); resolve(); });
359
+ });
360
+ const address = server.address();
361
+ if (!address || typeof address === "string")
362
+ throw new Error("桌面工作台无法启动");
363
+ workspaceHost = `127.0.0.1:${address.port}`;
364
+ previewHost = `localhost:${address.port}`;
365
+ workspaceOrigin = `http://${workspaceHost}`;
366
+ previewOrigin = `http://${previewHost}`;
367
+ return {
368
+ url: `${workspaceOrigin}/#desktop=${capability}`,
369
+ stop() {
370
+ return stopping ??= (async () => {
371
+ accountAbort.abort();
372
+ loginAbort?.abort();
373
+ for (const client of peers.clients)
374
+ client.terminate();
375
+ for (const upstream of upstreams)
376
+ upstream.terminate();
377
+ for (const socket of sockets)
378
+ socket.destroy();
379
+ await Promise.all([
380
+ new Promise((resolve) => server.close(() => resolve())),
381
+ new Promise((resolve) => peers.close(() => resolve())),
382
+ state.flush(),
383
+ ]);
384
+ })();
385
+ },
386
+ };
387
+ }
388
+ export async function runDesktopWorkspace(assets, input = process.stdin) {
389
+ let close;
390
+ const ended = new Promise((resolve) => { close = resolve; });
391
+ input.once("end", close);
392
+ input.once("close", close);
393
+ input.once("error", close);
394
+ process.once("SIGINT", close);
395
+ process.once("SIGTERM", close);
396
+ input.resume();
397
+ if (input.readableEnded || input.destroyed)
398
+ close();
399
+ let workspace;
400
+ try {
401
+ workspace = await startDesktopWorkspace({ assets });
402
+ process.stdout.write(`${JSON.stringify({ url: workspace.url })}\n`);
403
+ await ended;
404
+ }
405
+ finally {
406
+ input.off("end", close);
407
+ input.off("close", close);
408
+ input.off("error", close);
409
+ input.pause();
410
+ process.off("SIGINT", close);
411
+ process.off("SIGTERM", close);
412
+ await workspace?.stop();
413
+ }
414
+ }
415
+ //# sourceMappingURL=desktop-workspace.js.map