@linkshell/gateway 0.6.3 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/Dockerfile +9 -2
  2. package/README.md +1 -1
  3. package/dist/gateway/src/main.js +4 -0
  4. package/dist/gateway/src/main.js.map +1 -1
  5. package/dist/gateway/src/serve.d.ts +3 -1
  6. package/dist/gateway/src/serve.js +5 -1
  7. package/dist/gateway/src/serve.js.map +1 -1
  8. package/dist/gateway/src/web.d.ts +12 -0
  9. package/dist/gateway/src/web.js +53 -0
  10. package/dist/gateway/src/web.js.map +1 -0
  11. package/dist/gateway/tsconfig.tsbuildinfo +1 -1
  12. package/dist/wire/src/computer-preview.d.ts +35 -0
  13. package/dist/wire/src/computer-preview.js +51 -0
  14. package/dist/wire/src/computer-preview.js.map +1 -0
  15. package/dist/wire/src/goal.d.ts +66 -0
  16. package/dist/wire/src/goal.js +19 -0
  17. package/dist/wire/src/goal.js.map +1 -0
  18. package/dist/wire/src/index.d.ts +2 -0
  19. package/dist/wire/src/index.js +2 -0
  20. package/dist/wire/src/index.js.map +1 -1
  21. package/dist/wire/src/model.d.ts +78 -0
  22. package/dist/wire/src/model.js +31 -0
  23. package/dist/wire/src/model.js.map +1 -1
  24. package/dist/wire/src/rpc.d.ts +1289 -60
  25. package/dist/wire/src/rpc.js +53 -3
  26. package/dist/wire/src/rpc.js.map +1 -1
  27. package/dist/wire/src/updates.d.ts +334 -22
  28. package/dist/wire/src/updates.js +11 -1
  29. package/dist/wire/src/updates.js.map +1 -1
  30. package/package.json +7 -4
  31. package/scripts/build-web.mjs +7 -0
  32. package/src/main.ts +4 -0
  33. package/src/serve.ts +5 -1
  34. package/src/web.ts +57 -0
  35. package/web-client/assets/Prototype-B4M4qwtx.js +2 -0
  36. package/web-client/assets/account-R7yD1rZE.js +29 -0
  37. package/web-client/assets/index-DkcaGYC0.css +1 -0
  38. package/web-client/assets/index-Dnl-KjuJ.js +200 -0
  39. package/web-client/assets/mark-CUOqBxI3.png +0 -0
  40. package/web-client/assets/markdown-SZOrMO_3.js +45 -0
  41. package/web-client/assets/qr-scanner-worker.min-D85Z9gVD.js +98 -0
  42. package/web-client/assets/qr-scanner.min-DLeCi9No.js +1 -0
  43. package/web-client/assets/terminal-DOrYoP_4.css +32 -0
  44. package/web-client/assets/terminal-DWFexEE4.js +38 -0
  45. package/web-client/config.js +6 -0
  46. package/web-client/favicon.png +0 -0
  47. package/web-client/index.html +21 -0
  48. package/web-client/preview/bridge.html +38 -0
  49. package/web-client/preview/bridge.js +100 -0
  50. package/web-client/preview/worker.js +190 -0
@@ -0,0 +1,6 @@
1
+ // Public deployment settings. Never put tokens or private keys in this file.
2
+ // A self-hosted site can replace this file without rebuilding the application.
3
+ window.__LINKSHELL_CONFIG__ = {
4
+ deployment: "official",
5
+ gatewayUrl: "",
6
+ };
Binary file
@@ -0,0 +1,21 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="theme-color" content="#F2F2F7" />
7
+ <meta name="description" content="LinkShell 网页工作台" />
8
+ <link rel="icon" href="./favicon.png" type="image/png" />
9
+ <title>LinkShell · 工作台</title>
10
+ <script type="module" crossorigin src="/assets/index-Dnl-KjuJ.js"></script>
11
+ <link rel="modulepreload" crossorigin href="/assets/markdown-SZOrMO_3.js">
12
+ <link rel="modulepreload" crossorigin href="/assets/account-R7yD1rZE.js">
13
+ <link rel="modulepreload" crossorigin href="/assets/terminal-DWFexEE4.js">
14
+ <link rel="stylesheet" crossorigin href="/assets/terminal-DOrYoP_4.css">
15
+ <link rel="stylesheet" crossorigin href="/assets/index-DkcaGYC0.css">
16
+ </head>
17
+ <body>
18
+ <div id="root"></div>
19
+ <script src="/config.js"></script>
20
+ </body>
21
+ </html>
@@ -0,0 +1,38 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>LinkShell 隔离预览</title>
7
+ <style>
8
+ html,
9
+ body,
10
+ iframe {
11
+ width: 100%;
12
+ height: 100%;
13
+ border: 0;
14
+ margin: 0;
15
+ }
16
+ body {
17
+ font: 14px system-ui;
18
+ }
19
+ #note {
20
+ padding: 24px;
21
+ color: #666;
22
+ }
23
+ iframe {
24
+ display: none;
25
+ }
26
+ </style>
27
+ </head>
28
+ <body>
29
+ <p id="note">正在准备隔离预览…</p>
30
+ <iframe
31
+ id="page"
32
+ title="开发网页"
33
+ sandbox="allow-scripts allow-same-origin allow-forms allow-modals allow-downloads allow-pointer-lock"
34
+ allow="autoplay; fullscreen"
35
+ ></iframe>
36
+ <script src="/preview/bridge.js"></script>
37
+ </body>
38
+ </html>
@@ -0,0 +1,100 @@
1
+ // This shell must be served on a different origin from the account UI.
2
+ const channel = location.hash.slice(1);
3
+ const page = document.getElementById("page");
4
+ const note = document.getElementById("note");
5
+ const requests = new Map();
6
+ let parentOrigin;
7
+ let worker;
8
+ window.addEventListener("message", async (event) => {
9
+ const message = event.data;
10
+ if (!message || message.channel !== channel) return;
11
+ if (
12
+ event.source === parent &&
13
+ message.type === "preview-init" &&
14
+ !parentOrigin
15
+ ) {
16
+ if (event.origin === location.origin) {
17
+ note.textContent = "预览必须与账号页面使用不同来源";
18
+ return;
19
+ }
20
+ parentOrigin = event.origin;
21
+ try {
22
+ const registration = await navigator.serviceWorker.register(
23
+ "/preview/worker.js?v=2",
24
+ { scope: "/preview/" },
25
+ );
26
+ const next =
27
+ registration.installing ?? registration.waiting ?? registration.active;
28
+ if (next && next.state !== "activated")
29
+ await new Promise((resolve, reject) => {
30
+ const changed = () => {
31
+ if (next.state === "activated" || next.state === "redundant") {
32
+ next.removeEventListener("statechange", changed);
33
+ if (next.state === "activated") resolve();
34
+ else reject(new Error("预览服务更新失败"));
35
+ }
36
+ };
37
+ next.addEventListener("statechange", changed);
38
+ changed();
39
+ });
40
+ await navigator.serviceWorker.ready;
41
+ worker = registration.active;
42
+ const ready = new MessageChannel();
43
+ ready.port1.onmessage = () => {
44
+ page.src = `/preview/view/${channel}/`;
45
+ page.style.display = "block";
46
+ note.remove();
47
+ };
48
+ worker.postMessage(
49
+ { type: "init", token: channel, bootstrap: message.bootstrap },
50
+ [ready.port2],
51
+ );
52
+ } catch (error) {
53
+ note.textContent = String(error);
54
+ }
55
+ } else if (event.source === parent && event.origin === parentOrigin) {
56
+ if (/^http-/.test(message.type)) {
57
+ requests.get(message.id)?.postMessage(message);
58
+ if (["http-end", "http-error"].includes(message.type)) {
59
+ requests.get(message.id)?.close();
60
+ requests.delete(message.id);
61
+ }
62
+ } else page.contentWindow.postMessage(message, location.origin);
63
+ } else if (
64
+ event.source === page.contentWindow &&
65
+ event.origin === location.origin &&
66
+ parentOrigin &&
67
+ /^ws-/.test(message.type)
68
+ ) {
69
+ parent.postMessage(message, parentOrigin);
70
+ }
71
+ });
72
+ navigator.serviceWorker.addEventListener("message", (event) => {
73
+ if (event.data?.token !== channel || !parentOrigin || event.source !== worker)
74
+ return;
75
+ requests.set(event.data.id, event.ports[0]);
76
+ event.ports[0].onmessage = (message) => {
77
+ if (message.data?.cancel) {
78
+ parent.postMessage(
79
+ { channel, type: "http-cancel", id: event.data.id },
80
+ parentOrigin,
81
+ );
82
+ event.ports[0].close();
83
+ requests.delete(event.data.id);
84
+ }
85
+ };
86
+ parent.postMessage(
87
+ { ...event.data, channel, type: "http-request" },
88
+ parentOrigin,
89
+ );
90
+ });
91
+ parent.postMessage({ channel, type: "preview-ready" }, "*");
92
+
93
+ window.addEventListener("pagehide", () => {
94
+ for (const [id, port] of requests) {
95
+ parent.postMessage({ channel, type: "http-cancel", id }, parentOrigin);
96
+ port.close();
97
+ }
98
+ requests.clear();
99
+ worker?.postMessage({ type: "dispose", token: channel });
100
+ });
@@ -0,0 +1,190 @@
1
+ /* global clients */
2
+ const sessions = new Map();
3
+ const owners = new Map();
4
+ self.addEventListener("install", () => {
5
+ void self.skipWaiting();
6
+ });
7
+ self.addEventListener("activate", (event) => {
8
+ event.waitUntil(clients.claim());
9
+ });
10
+ self.addEventListener("message", (event) => {
11
+ if (
12
+ event.data?.type === "dispose" &&
13
+ sessions.get(event.data.token)?.owner === event.source?.id
14
+ ) {
15
+ sessions.delete(event.data.token);
16
+ for (const [owner, token] of owners)
17
+ if (token === event.data.token) owners.delete(owner);
18
+ }
19
+ if (
20
+ event.data?.type === "init" &&
21
+ event.source &&
22
+ new URL(event.source.url).pathname === "/preview/bridge.html"
23
+ ) {
24
+ sessions.set(event.data.token, {
25
+ owner: event.source.id,
26
+ bootstrap: event.data.bootstrap,
27
+ });
28
+ event.ports[0].postMessage({ ready: true });
29
+ }
30
+ });
31
+ function navigationBootstrap(prefix) {
32
+ const rewrite = (value) => {
33
+ const url = new URL(value, location.href);
34
+ if (url.origin !== location.origin || url.pathname.startsWith(prefix))
35
+ return url.href;
36
+ return prefix + url.pathname.replace(/^\//, "") + url.search + url.hash;
37
+ };
38
+ document.addEventListener(
39
+ "click",
40
+ (event) => {
41
+ const anchor = event.target.closest?.("a[href]");
42
+ if (anchor) anchor.href = rewrite(anchor.href);
43
+ },
44
+ true,
45
+ );
46
+ document.addEventListener(
47
+ "submit",
48
+ (event) => {
49
+ if (event.target.action)
50
+ event.target.action = rewrite(event.target.action);
51
+ },
52
+ true,
53
+ );
54
+ for (const key of ["pushState", "replaceState"]) {
55
+ const original = history[key].bind(history);
56
+ history[key] = (state, title, url) =>
57
+ original(state, title, url == null ? url : rewrite(url));
58
+ }
59
+ }
60
+ self.addEventListener("fetch", (event) => {
61
+ const url = new URL(event.request.url);
62
+ if (url.origin !== self.location.origin) return;
63
+ const match = /^\/preview\/view\/([\w-]+)\/(.*)$/.exec(url.pathname);
64
+ const token = match?.[1] ?? owners.get(event.clientId);
65
+ const session = sessions.get(token);
66
+ if (!session) return;
67
+ if (event.resultingClientId) owners.set(event.resultingClientId, token);
68
+ event.respondWith(
69
+ (async () => {
70
+ const client = await clients.get(session.owner);
71
+ if (!client)
72
+ return new Response("工作台已关闭,请重新打开预览", { status: 503 });
73
+ const id = crypto.randomUUID();
74
+ const wire = new MessageChannel();
75
+ let controller;
76
+ let finished = false;
77
+ let rejectHead;
78
+ let timer;
79
+ const stop = (error) => {
80
+ if (finished) return;
81
+ finished = true;
82
+ clearTimeout(timer);
83
+ if (error) {
84
+ rejectHead?.(error);
85
+ controller.error(error);
86
+ } else controller.close();
87
+ wire.port1.close();
88
+ };
89
+ const stream = new ReadableStream({
90
+ start(value) {
91
+ controller = value;
92
+ },
93
+ cancel() {
94
+ if (finished) return;
95
+ finished = true;
96
+ clearTimeout(timer);
97
+ wire.port1.postMessage({ cancel: true });
98
+ wire.port1.close();
99
+ },
100
+ });
101
+ const touch = () => {
102
+ clearTimeout(timer);
103
+ timer = setTimeout(() => {
104
+ wire.port1.postMessage({ cancel: true });
105
+ stop(new Error("预览请求超时"));
106
+ }, 65000);
107
+ };
108
+ const result = new Promise((resolve, reject) => {
109
+ rejectHead = reject;
110
+ wire.port1.onmessage = (event) => {
111
+ if (finished) return;
112
+ touch();
113
+ const message = event.data;
114
+ if (message.type === "http-head") resolve(message);
115
+ else if (message.type === "http-chunk")
116
+ controller.enqueue(new Uint8Array(message.body));
117
+ else if (message.type === "http-end") stop();
118
+ else if (message.type === "http-error")
119
+ stop(new Error(message.error));
120
+ };
121
+ touch();
122
+ });
123
+ const body = ["GET", "HEAD"].includes(event.request.method)
124
+ ? undefined
125
+ : await event.request.arrayBuffer();
126
+ client.postMessage(
127
+ {
128
+ token,
129
+ id,
130
+ path: (match ? "/" + match[2] : url.pathname) + url.search,
131
+ method: event.request.method,
132
+ headers: [...event.request.headers],
133
+ body,
134
+ },
135
+ [wire.port2],
136
+ );
137
+ try {
138
+ const response = await result;
139
+ if (response.error)
140
+ return new Response(response.error, { status: 502 });
141
+ const headers = new Headers(response.headers);
142
+ for (const name of [
143
+ "content-length",
144
+ "transfer-encoding",
145
+ "content-encoding",
146
+ "content-security-policy",
147
+ "content-security-policy-report-only",
148
+ "x-frame-options",
149
+ "set-cookie",
150
+ ])
151
+ headers.delete(name);
152
+ const prefix = `/preview/view/${token}/`;
153
+ const redirect = headers.get("location");
154
+ if (redirect) {
155
+ const target = new URL(redirect, "http://localhost");
156
+ if (
157
+ target.hostname === "localhost" ||
158
+ target.hostname === "127.0.0.1"
159
+ )
160
+ headers.set(
161
+ "location",
162
+ prefix +
163
+ target.pathname.replace(/^\//, "") +
164
+ target.search +
165
+ target.hash,
166
+ );
167
+ }
168
+ let output = stream;
169
+ if (headers.get("content-type")?.includes("text/html")) {
170
+ const script = `<script>(${session.bootstrap})(${JSON.stringify(token)});(${navigationBootstrap.toString()})(${JSON.stringify(prefix)});</script>`;
171
+ const text = await new Response(stream).text();
172
+ output = new TextEncoder().encode(
173
+ /<head(?:\s[^>]*)?>/i.test(text)
174
+ ? text.replace(/<head(?:\s[^>]*)?>/i, "$&" + script)
175
+ : script + text,
176
+ );
177
+ }
178
+ return new Response(
179
+ [204, 205, 304].includes(response.status) ||
180
+ event.request.method === "HEAD"
181
+ ? null
182
+ : output,
183
+ { status: response.status, headers },
184
+ );
185
+ } catch (error) {
186
+ return new Response(String(error), { status: 502 });
187
+ }
188
+ })(),
189
+ );
190
+ });