@openruntime/cli 0.1.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.
- package/assets/auth-connector/icon-128.png +0 -0
- package/assets/auth-connector/icon-16.png +0 -0
- package/assets/auth-connector/icon-32.png +0 -0
- package/assets/auth-connector/icon-48.png +0 -0
- package/dist/args.d.ts +9 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +52 -0
- package/dist/args.js.map +1 -0
- package/dist/auth-connector.d.ts +66 -0
- package/dist/auth-connector.d.ts.map +1 -0
- package/dist/auth-connector.js +1031 -0
- package/dist/auth-connector.js.map +1 -0
- package/dist/bridge-process.d.ts +64 -0
- package/dist/bridge-process.d.ts.map +1 -0
- package/dist/bridge-process.js +228 -0
- package/dist/bridge-process.js.map +1 -0
- package/dist/browser.d.ts +25 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +180 -0
- package/dist/browser.js.map +1 -0
- package/dist/client.d.ts +21 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +132 -0
- package/dist/client.js.map +1 -0
- package/dist/command-definition.d.ts +19 -0
- package/dist/command-definition.d.ts.map +1 -0
- package/dist/command-definition.js +42 -0
- package/dist/command-definition.js.map +1 -0
- package/dist/command-skill.d.ts +5 -0
- package/dist/command-skill.d.ts.map +1 -0
- package/dist/command-skill.js +25 -0
- package/dist/command-skill.js.map +1 -0
- package/dist/entry.d.ts +2 -0
- package/dist/entry.d.ts.map +1 -0
- package/dist/entry.js +16 -0
- package/dist/entry.js.map +1 -0
- package/dist/extension-api.d.ts +85 -0
- package/dist/extension-api.d.ts.map +1 -0
- package/dist/extension-api.js +313 -0
- package/dist/extension-api.js.map +1 -0
- package/dist/extensions/types.d.ts +3 -0
- package/dist/extensions/types.d.ts.map +1 -0
- package/dist/extensions/types.js +2 -0
- package/dist/extensions/types.js.map +1 -0
- package/dist/external-extensions.d.ts +19 -0
- package/dist/external-extensions.d.ts.map +1 -0
- package/dist/external-extensions.js +168 -0
- package/dist/external-extensions.js.map +1 -0
- package/dist/help.d.ts +17 -0
- package/dist/help.d.ts.map +1 -0
- package/dist/help.js +214 -0
- package/dist/help.js.map +1 -0
- package/dist/index.d.ts +100 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1579 -0
- package/dist/index.js.map +1 -0
- package/dist/next-browser-profile-preload.d.ts +2 -0
- package/dist/next-browser-profile-preload.d.ts.map +1 -0
- package/dist/next-browser-profile-preload.js +391 -0
- package/dist/next-browser-profile-preload.js.map +1 -0
- package/dist/operation-log.d.ts +23 -0
- package/dist/operation-log.d.ts.map +1 -0
- package/dist/operation-log.js +76 -0
- package/dist/operation-log.js.map +1 -0
- package/dist/output.d.ts +42 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +111 -0
- package/dist/output.js.map +1 -0
- package/dist/profile.d.ts +56 -0
- package/dist/profile.d.ts.map +1 -0
- package/dist/profile.js +414 -0
- package/dist/profile.js.map +1 -0
- package/dist/record.d.ts +17 -0
- package/dist/record.d.ts.map +1 -0
- package/dist/record.js +1494 -0
- package/dist/record.js.map +1 -0
- package/package.json +46 -0
|
@@ -0,0 +1,1031 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { dirname, join, resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { deflateSync } from "node:zlib";
|
|
9
|
+
import { exportAuthStateProfile } from "./profile.js";
|
|
10
|
+
const AUTH_CONNECTOR_TIMEOUT_MS = 120_000;
|
|
11
|
+
const AUTH_CONNECTOR_MAX_BODY_BYTES = 20 * 1024 * 1024;
|
|
12
|
+
const AUTH_CONNECTOR_QUERY_PARAM = "openruntimeAuthConnector";
|
|
13
|
+
const AUTH_CONNECTOR_EXTENSION_VERSION = "0.1.0";
|
|
14
|
+
const AUTH_CONNECTOR_ICON_SIZES = [16, 32, 48, 128];
|
|
15
|
+
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
16
|
+
const PNG_CRC_TABLE = createPngCrcTable();
|
|
17
|
+
export async function exportAuthProfileWithConnector(options) {
|
|
18
|
+
const requestedUrl = normalizeRequestedHttpUrl(options.requestedUrl);
|
|
19
|
+
const token = randomUUID();
|
|
20
|
+
const extensionDirectory = await writeAuthConnectorExtension(options.extensionDirectory, {
|
|
21
|
+
...(options.extensionIconPath === undefined ? {} : { iconPath: options.extensionIconPath })
|
|
22
|
+
});
|
|
23
|
+
let settleExport = () => undefined;
|
|
24
|
+
let settleError = () => undefined;
|
|
25
|
+
const exportPromise = new Promise((resolve, reject) => {
|
|
26
|
+
settleExport = resolve;
|
|
27
|
+
settleError = reject;
|
|
28
|
+
});
|
|
29
|
+
const state = {
|
|
30
|
+
status: "waiting_for_extension",
|
|
31
|
+
message: "正在等待 OpenRuntime Auth Connector 扩展。",
|
|
32
|
+
token,
|
|
33
|
+
requestedUrl,
|
|
34
|
+
extensionDirectory,
|
|
35
|
+
...(options.extensionInstallUrl === undefined ? {} : { extensionInstallUrl: options.extensionInstallUrl }),
|
|
36
|
+
openBrowser: options.browserOpener ?? openAuthConnectorSetupPage,
|
|
37
|
+
resolve: settleExport,
|
|
38
|
+
reject: settleError
|
|
39
|
+
};
|
|
40
|
+
const server = createServer((request, response) => {
|
|
41
|
+
void handleAuthConnectorRequest(request, response, state);
|
|
42
|
+
});
|
|
43
|
+
const sockets = trackServerSockets(server);
|
|
44
|
+
try {
|
|
45
|
+
await listen(server);
|
|
46
|
+
const address = server.address();
|
|
47
|
+
const setupUrl = `http://127.0.0.1:${address.port}/?${AUTH_CONNECTOR_QUERY_PARAM}=1&token=${encodeURIComponent(token)}&auto=1`;
|
|
48
|
+
await state.openBrowser(setupUrl);
|
|
49
|
+
const timeout = options.timeout ?? AUTH_CONNECTOR_TIMEOUT_MS;
|
|
50
|
+
const payload = await withTimeout(exportPromise, timeout, () => {
|
|
51
|
+
state.status = "error";
|
|
52
|
+
state.message = `Timed out waiting for Chrome auth export after ${timeout}ms.`;
|
|
53
|
+
return new Error(state.message);
|
|
54
|
+
});
|
|
55
|
+
return await exportAuthStateProfile({
|
|
56
|
+
storageState: convertAuthConnectorPayloadToStorageState(payload),
|
|
57
|
+
...(options.outputPath === undefined ? {} : { outputPath: options.outputPath })
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
await closeServer(server, sockets);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export function convertAuthConnectorPayloadToStorageState(payload) {
|
|
65
|
+
return {
|
|
66
|
+
cookies: payload.cookies.map(convertAuthConnectorCookie),
|
|
67
|
+
origins: payload.origins.map((origin) => ({
|
|
68
|
+
origin: origin.origin,
|
|
69
|
+
localStorage: normalizeStorageEntries(origin.localStorage)
|
|
70
|
+
}))
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export function getDefaultAuthConnectorExtensionDirectory() {
|
|
74
|
+
return join(homedir(), ".openruntime", "auth-connector-extension");
|
|
75
|
+
}
|
|
76
|
+
export async function writeAuthConnectorExtension(directory = getDefaultAuthConnectorExtensionDirectory(), options = {}) {
|
|
77
|
+
const extensionDirectory = resolve(directory);
|
|
78
|
+
await mkdir(extensionDirectory, {
|
|
79
|
+
recursive: true,
|
|
80
|
+
mode: 0o700
|
|
81
|
+
});
|
|
82
|
+
await writeJsonFile(join(extensionDirectory, "manifest.json"), createAuthConnectorManifest());
|
|
83
|
+
await writeFile(join(extensionDirectory, "setup-content.js"), AUTH_CONNECTOR_SETUP_CONTENT_SCRIPT, {
|
|
84
|
+
encoding: "utf8",
|
|
85
|
+
mode: 0o600
|
|
86
|
+
});
|
|
87
|
+
await writeFile(join(extensionDirectory, "service-worker.js"), AUTH_CONNECTOR_SERVICE_WORKER_SCRIPT, {
|
|
88
|
+
encoding: "utf8",
|
|
89
|
+
mode: 0o600
|
|
90
|
+
});
|
|
91
|
+
const customIcon = options.iconPath === undefined ? undefined : await readAuthConnectorIconPng(options.iconPath);
|
|
92
|
+
for (const size of AUTH_CONNECTOR_ICON_SIZES) {
|
|
93
|
+
const icon = customIcon ?? await readAuthConnectorDefaultIconPng(size);
|
|
94
|
+
await writeFile(join(extensionDirectory, `icon-${size}.png`), icon, {
|
|
95
|
+
mode: 0o600
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return extensionDirectory;
|
|
99
|
+
}
|
|
100
|
+
export async function openAuthConnectorSetupPage(url) {
|
|
101
|
+
const command = createOpenChromeCommand(url, process.platform);
|
|
102
|
+
await new Promise((resolvePromise, reject) => {
|
|
103
|
+
const child = spawn(command.command, command.args, {
|
|
104
|
+
detached: true,
|
|
105
|
+
stdio: "ignore"
|
|
106
|
+
});
|
|
107
|
+
child.once("error", reject);
|
|
108
|
+
child.once("spawn", () => {
|
|
109
|
+
child.unref();
|
|
110
|
+
resolvePromise();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function convertAuthConnectorCookie(cookie) {
|
|
115
|
+
const converted = {
|
|
116
|
+
name: cookie.name,
|
|
117
|
+
value: cookie.value,
|
|
118
|
+
domain: cookie.domain,
|
|
119
|
+
path: cookie.path ?? "/",
|
|
120
|
+
expires: cookie.session === true || cookie.expirationDate === undefined ? -1 : cookie.expirationDate,
|
|
121
|
+
httpOnly: cookie.httpOnly ?? false,
|
|
122
|
+
secure: cookie.secure ?? false,
|
|
123
|
+
...createOptionalSameSite(cookie.sameSite),
|
|
124
|
+
...createOptionalPartitionKey(cookie.partitionKey)
|
|
125
|
+
};
|
|
126
|
+
return converted;
|
|
127
|
+
}
|
|
128
|
+
function createOptionalSameSite(sameSite) {
|
|
129
|
+
if (sameSite === "strict")
|
|
130
|
+
return { sameSite: "Strict" };
|
|
131
|
+
if (sameSite === "lax")
|
|
132
|
+
return { sameSite: "Lax" };
|
|
133
|
+
if (sameSite === "no_restriction")
|
|
134
|
+
return { sameSite: "None" };
|
|
135
|
+
return {};
|
|
136
|
+
}
|
|
137
|
+
function createOptionalPartitionKey(partitionKey) {
|
|
138
|
+
if (typeof partitionKey === "string" && partitionKey.length > 0) {
|
|
139
|
+
return { partitionKey };
|
|
140
|
+
}
|
|
141
|
+
if (partitionKey !== null && typeof partitionKey === "object" && typeof partitionKey.topLevelSite === "string" && partitionKey.topLevelSite.length > 0) {
|
|
142
|
+
return { partitionKey: partitionKey.topLevelSite };
|
|
143
|
+
}
|
|
144
|
+
return {};
|
|
145
|
+
}
|
|
146
|
+
function normalizeStorageEntries(entries) {
|
|
147
|
+
return (entries ?? [])
|
|
148
|
+
.filter((entry) => typeof entry.name === "string" && typeof entry.value === "string")
|
|
149
|
+
.map((entry) => ({
|
|
150
|
+
name: entry.name,
|
|
151
|
+
value: entry.value
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
async function handleAuthConnectorRequest(request, response, state) {
|
|
155
|
+
try {
|
|
156
|
+
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
157
|
+
if (request.method === "GET" && url.pathname === "/") {
|
|
158
|
+
sendHtml(response, renderSetupPage(state));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const pageIconSize = getAuthConnectorPageIconSize(url.pathname);
|
|
162
|
+
if (request.method === "GET" && pageIconSize !== undefined) {
|
|
163
|
+
sendPng(response, await readAuthConnectorDefaultIconPng(pageIconSize));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (request.method === "GET" && url.pathname === "/request") {
|
|
167
|
+
assertValidToken(url, state);
|
|
168
|
+
state.status = "exporting";
|
|
169
|
+
state.message = "扩展已连接,正在导出登录状态。";
|
|
170
|
+
sendJson(response, {
|
|
171
|
+
requestedUrl: state.requestedUrl
|
|
172
|
+
});
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (request.method === "GET" && url.pathname === "/status") {
|
|
176
|
+
assertValidToken(url, state);
|
|
177
|
+
sendJson(response, {
|
|
178
|
+
status: state.status,
|
|
179
|
+
message: state.message
|
|
180
|
+
});
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (request.method === "POST" && url.pathname === "/open-extensions") {
|
|
184
|
+
assertValidToken(url, state);
|
|
185
|
+
await state.openBrowser("chrome://extensions");
|
|
186
|
+
state.message = "已打开 Chrome 扩展页。加载复制的扩展目录后,回到这里继续导出。";
|
|
187
|
+
sendJson(response, {
|
|
188
|
+
ok: true
|
|
189
|
+
});
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (request.method === "POST" && url.pathname === "/event") {
|
|
193
|
+
assertValidToken(url, state);
|
|
194
|
+
const body = await readJsonBody(request);
|
|
195
|
+
const message = getRecord(body);
|
|
196
|
+
const status = typeof message?.status === "string" ? message.status : "exporting";
|
|
197
|
+
if (status === "exporting" || status === "waiting_for_extension") {
|
|
198
|
+
state.status = status;
|
|
199
|
+
}
|
|
200
|
+
if (typeof message?.message === "string" && message.message.length > 0) {
|
|
201
|
+
state.message = message.message;
|
|
202
|
+
}
|
|
203
|
+
sendJson(response, {
|
|
204
|
+
ok: true
|
|
205
|
+
});
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (request.method === "POST" && url.pathname === "/export") {
|
|
209
|
+
assertValidToken(url, state);
|
|
210
|
+
const payload = assertAuthConnectorPayload(await readJsonBody(request));
|
|
211
|
+
state.status = "exported";
|
|
212
|
+
state.message = "登录状态已导出。";
|
|
213
|
+
state.resolve(payload);
|
|
214
|
+
sendJson(response, {
|
|
215
|
+
ok: true
|
|
216
|
+
});
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (request.method === "POST" && url.pathname === "/error") {
|
|
220
|
+
assertValidToken(url, state);
|
|
221
|
+
const body = getRecord(await readJsonBody(request));
|
|
222
|
+
const message = typeof body?.message === "string" && body.message.length > 0
|
|
223
|
+
? body.message
|
|
224
|
+
: "Chrome auth export failed.";
|
|
225
|
+
state.status = "error";
|
|
226
|
+
state.message = message;
|
|
227
|
+
state.reject(new Error(message));
|
|
228
|
+
sendJson(response, {
|
|
229
|
+
ok: true
|
|
230
|
+
});
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
sendJson(response, {
|
|
234
|
+
error: "not_found"
|
|
235
|
+
}, 404);
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
sendJson(response, {
|
|
239
|
+
error: error instanceof Error ? error.message : String(error)
|
|
240
|
+
}, 400);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function renderSetupPage(state) {
|
|
244
|
+
const installBlock = state.extensionInstallUrl === undefined
|
|
245
|
+
? `<p>没有检测到 OpenRuntime Auth Connector。首次使用需要加载一次扩展。</p>
|
|
246
|
+
<div class="steps">
|
|
247
|
+
<div class="step">
|
|
248
|
+
<span class="step-number">1</span>
|
|
249
|
+
<div>
|
|
250
|
+
<strong>复制扩展目录</strong>
|
|
251
|
+
<code id="extension-path">${escapeHtml(state.extensionDirectory)}</code>
|
|
252
|
+
<button id="copy-path" type="button" class="secondary highlight">复制扩展目录</button>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
<div class="step">
|
|
256
|
+
<span class="step-number">2</span>
|
|
257
|
+
<div>
|
|
258
|
+
<strong>打开 Chrome 扩展页</strong>
|
|
259
|
+
<p class="muted">点击后会打开扩展页。Chrome 仍需要你点一次“加载已解压的扩展程序”,然后选择刚复制的目录。</p>
|
|
260
|
+
<button id="install-extension" type="button" class="secondary">安装扩展</button>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
<div class="step">
|
|
264
|
+
<span class="step-number">3</span>
|
|
265
|
+
<div>
|
|
266
|
+
<strong>回到这里继续导出</strong>
|
|
267
|
+
<p class="muted">扩展加载完成后,这个页面会自动检测;如果没有自动开始,点击上面的导出按钮。</p>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
</div>`
|
|
271
|
+
: `<p>没有检测到 OpenRuntime Auth Connector。首次使用需要先安装扩展。</p>
|
|
272
|
+
<div class="actions">
|
|
273
|
+
<a class="secondary highlight" href="${escapeHtml(state.extensionInstallUrl)}" target="_blank" rel="noreferrer">安装扩展</a>
|
|
274
|
+
</div>`;
|
|
275
|
+
return `<!doctype html>
|
|
276
|
+
<html>
|
|
277
|
+
<head>
|
|
278
|
+
<meta charset="utf-8">
|
|
279
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
280
|
+
<title>OpenRuntime Auth Export</title>
|
|
281
|
+
<link rel="icon" type="image/png" sizes="16x16" href="/icon-16.png">
|
|
282
|
+
<link rel="icon" type="image/png" sizes="32x32" href="/icon-32.png">
|
|
283
|
+
<link rel="apple-touch-icon" href="/icon-128.png">
|
|
284
|
+
<style>
|
|
285
|
+
body { font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; max-width: 760px; margin: 48px auto; padding: 0 24px; color: #1f2937; line-height: 1.5; }
|
|
286
|
+
.header { display: flex; gap: 14px; align-items: center; margin-bottom: 12px; }
|
|
287
|
+
.brand-icon { width: 48px; height: 48px; border-radius: 10px; flex: 0 0 auto; }
|
|
288
|
+
h1 { font-size: 28px; margin: 0 0 12px; }
|
|
289
|
+
.header h1 { margin: 0; }
|
|
290
|
+
p { margin: 10px 0; }
|
|
291
|
+
code { display: block; padding: 12px; border: 1px solid #d1d5db; border-radius: 6px; overflow-wrap: anywhere; background: #f9fafb; }
|
|
292
|
+
.panel { border: 1px solid #d1d5db; border-radius: 8px; padding: 18px; margin: 20px 0; }
|
|
293
|
+
.hidden { display: none; }
|
|
294
|
+
.actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 14px; }
|
|
295
|
+
button, .secondary { display: inline-flex; align-items: center; min-height: 40px; padding: 0 14px; border-radius: 6px; border: 1px solid #9ca3af; font: inherit; cursor: pointer; text-decoration: none; color: #111827; background: white; }
|
|
296
|
+
.primary { min-height: 44px; border-color: #111827; background: #111827; color: white; }
|
|
297
|
+
.muted { color: #6b7280; }
|
|
298
|
+
.steps { display: grid; gap: 16px; margin-top: 14px; }
|
|
299
|
+
.step { display: grid; grid-template-columns: 30px 1fr; gap: 12px; align-items: start; }
|
|
300
|
+
.step-number { display: inline-flex; width: 30px; height: 30px; align-items: center; justify-content: center; border-radius: 999px; background: #111827; color: white; font-weight: 700; }
|
|
301
|
+
.step strong { display: block; margin-bottom: 8px; }
|
|
302
|
+
.highlight { animation: openruntime-pulse 1.4s ease-in-out infinite; border-color: #2563eb; box-shadow: 0 0 0 0 rgba(37, 99, 235, 0.35); }
|
|
303
|
+
@keyframes openruntime-pulse {
|
|
304
|
+
0% { box-shadow: 0 0 0 0 rgba(37, 99, 235, 0.35); }
|
|
305
|
+
70% { box-shadow: 0 0 0 8px rgba(37, 99, 235, 0); }
|
|
306
|
+
100% { box-shadow: 0 0 0 0 rgba(37, 99, 235, 0); }
|
|
307
|
+
}
|
|
308
|
+
</style>
|
|
309
|
+
</head>
|
|
310
|
+
<body>
|
|
311
|
+
<div class="header">
|
|
312
|
+
<img class="brand-icon" src="/icon-128.png" alt="" width="48" height="48">
|
|
313
|
+
<h1>导出浏览器登录状态</h1>
|
|
314
|
+
</div>
|
|
315
|
+
<p>目标网站:</p>
|
|
316
|
+
<code>${escapeHtml(state.requestedUrl)}</code>
|
|
317
|
+
<div class="actions">
|
|
318
|
+
<button id="start-export" type="button" class="primary">导出登录状态</button>
|
|
319
|
+
</div>
|
|
320
|
+
<div id="install-panel" class="panel hidden">
|
|
321
|
+
${installBlock}
|
|
322
|
+
</div>
|
|
323
|
+
<p id="status" class="muted">${escapeHtml(state.message)}</p>
|
|
324
|
+
<script>
|
|
325
|
+
const token = ${JSON.stringify(state.token)};
|
|
326
|
+
const autoStart = new URLSearchParams(window.location.search).get('auto') === '1';
|
|
327
|
+
let lastStatus = ${JSON.stringify(state.status)};
|
|
328
|
+
let connectorReady = false;
|
|
329
|
+
let exportCompleted = false;
|
|
330
|
+
let installPanelVisible = false;
|
|
331
|
+
const installAttemptedKey = 'openruntimeAuthConnectorInstallAttempted:' + token;
|
|
332
|
+
const reloadAttemptedKey = 'openruntimeAuthConnectorReloadAttempted:' + token;
|
|
333
|
+
let didReloadAfterClick = sessionStorage.getItem(reloadAttemptedKey) === '1';
|
|
334
|
+
|
|
335
|
+
function setStatus(message) {
|
|
336
|
+
document.getElementById('status').textContent = message;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function showInstallPanel(message) {
|
|
340
|
+
installPanelVisible = true;
|
|
341
|
+
document.getElementById('install-panel').classList.remove('hidden');
|
|
342
|
+
setStatus(message);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function hideInstallPanel() {
|
|
346
|
+
installPanelVisible = false;
|
|
347
|
+
document.getElementById('install-panel').classList.add('hidden');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function highlightInstallStep() {
|
|
351
|
+
document.getElementById('copy-path')?.classList.remove('highlight');
|
|
352
|
+
document.getElementById('install-extension')?.classList.add('highlight');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function reloadToDetectInstalledExtension() {
|
|
356
|
+
if (connectorReady || exportCompleted || didReloadAfterClick) return;
|
|
357
|
+
if (sessionStorage.getItem(installAttemptedKey) !== '1') return;
|
|
358
|
+
didReloadAfterClick = true;
|
|
359
|
+
sessionStorage.setItem(reloadAttemptedKey, '1');
|
|
360
|
+
location.reload();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function markExportComplete() {
|
|
364
|
+
exportCompleted = true;
|
|
365
|
+
const button = document.getElementById('start-export');
|
|
366
|
+
button.textContent = '关闭页面';
|
|
367
|
+
button.onclick = () => window.close();
|
|
368
|
+
setStatus('导出成功。命令行已输出文件路径,可以关闭这个标签页。');
|
|
369
|
+
setTimeout(() => window.close(), 800);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function requestExportStart() {
|
|
373
|
+
if (exportCompleted) {
|
|
374
|
+
window.close();
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
document.getElementById('start-export').textContent = '正在导出...';
|
|
378
|
+
setStatus('正在连接 OpenRuntime Auth Connector...');
|
|
379
|
+
window.postMessage({
|
|
380
|
+
type: 'openruntime.auth.startFromPage',
|
|
381
|
+
token
|
|
382
|
+
}, window.location.origin);
|
|
383
|
+
|
|
384
|
+
setTimeout(() => {
|
|
385
|
+
if (connectorReady) return;
|
|
386
|
+
if (sessionStorage.getItem(installAttemptedKey) === '1' && lastStatus === 'waiting_for_extension' && !didReloadAfterClick) {
|
|
387
|
+
didReloadAfterClick = true;
|
|
388
|
+
sessionStorage.setItem(reloadAttemptedKey, '1');
|
|
389
|
+
location.reload();
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
document.getElementById('start-export').textContent = '导出登录状态';
|
|
393
|
+
showInstallPanel('没有检测到扩展。请先安装扩展,然后回到这里点击导出。');
|
|
394
|
+
}, 1200);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async function refreshStatus() {
|
|
398
|
+
try {
|
|
399
|
+
const response = await fetch('/status?token=' + encodeURIComponent(token), { cache: 'no-store' });
|
|
400
|
+
const state = await response.json();
|
|
401
|
+
lastStatus = state.status || lastStatus;
|
|
402
|
+
if (state.status === 'exported') {
|
|
403
|
+
markExportComplete();
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (installPanelVisible && state.status === 'waiting_for_extension') {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
setStatus(state.message || state.status || '等待中...');
|
|
410
|
+
} catch {
|
|
411
|
+
if (exportCompleted) return;
|
|
412
|
+
setStatus('本地 OpenRuntime 命令已结束或不可用。');
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
window.addEventListener('message', (event) => {
|
|
417
|
+
if (event.source !== window || event.origin !== window.location.origin) return;
|
|
418
|
+
if (!event.data || event.data.type !== 'openruntime.auth.connectorReady' || event.data.token !== token) return;
|
|
419
|
+
connectorReady = true;
|
|
420
|
+
sessionStorage.removeItem(installAttemptedKey);
|
|
421
|
+
sessionStorage.removeItem(reloadAttemptedKey);
|
|
422
|
+
document.getElementById('start-export').textContent = '导出登录状态';
|
|
423
|
+
hideInstallPanel();
|
|
424
|
+
if (autoStart) {
|
|
425
|
+
setStatus('已检测到扩展,正在导出登录状态。');
|
|
426
|
+
requestExportStart();
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
setStatus('已检测到扩展,点击导出登录状态。');
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
window.addEventListener('message', (event) => {
|
|
433
|
+
if (event.source !== window || event.origin !== window.location.origin) return;
|
|
434
|
+
if (!event.data || event.data.type !== 'openruntime.auth.exportComplete' || event.data.token !== token) return;
|
|
435
|
+
markExportComplete();
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
window.addEventListener('message', (event) => {
|
|
439
|
+
if (event.source !== window || event.origin !== window.location.origin) return;
|
|
440
|
+
if (!event.data || event.data.type !== 'openruntime.auth.exportError' || event.data.token !== token) return;
|
|
441
|
+
document.getElementById('start-export').textContent = '重新导出';
|
|
442
|
+
setStatus(event.data.message || '导出失败,请回到命令行查看错误。');
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
document.getElementById('start-export').addEventListener('click', requestExportStart);
|
|
446
|
+
document.getElementById('install-extension')?.addEventListener('click', async () => {
|
|
447
|
+
const path = document.getElementById('extension-path')?.textContent || '';
|
|
448
|
+
if (path.length > 0) await navigator.clipboard.writeText(path);
|
|
449
|
+
highlightInstallStep();
|
|
450
|
+
sessionStorage.setItem(installAttemptedKey, '1');
|
|
451
|
+
sessionStorage.removeItem(reloadAttemptedKey);
|
|
452
|
+
didReloadAfterClick = false;
|
|
453
|
+
await fetch('/open-extensions?token=' + encodeURIComponent(token), { method: 'POST' });
|
|
454
|
+
setStatus('已打开 Chrome 扩展页,并复制了扩展目录。请选择“加载已解压的扩展程序”。');
|
|
455
|
+
});
|
|
456
|
+
document.getElementById('copy-path')?.addEventListener('click', async () => {
|
|
457
|
+
await navigator.clipboard.writeText(document.getElementById('extension-path').textContent || '');
|
|
458
|
+
highlightInstallStep();
|
|
459
|
+
setStatus('扩展目录已复制。下一步点击“安装扩展”。');
|
|
460
|
+
});
|
|
461
|
+
window.addEventListener('focus', () => setTimeout(reloadToDetectInstalledExtension, 300));
|
|
462
|
+
document.addEventListener('visibilitychange', () => {
|
|
463
|
+
if (document.visibilityState === 'visible') setTimeout(reloadToDetectInstalledExtension, 300);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
setInterval(refreshStatus, 1000);
|
|
467
|
+
refreshStatus();
|
|
468
|
+
if (autoStart) {
|
|
469
|
+
setStatus('正在检测 OpenRuntime Auth Connector...');
|
|
470
|
+
setTimeout(() => {
|
|
471
|
+
if (connectorReady || exportCompleted) return;
|
|
472
|
+
showInstallPanel('没有检测到扩展。请按步骤安装扩展。');
|
|
473
|
+
}, 1600);
|
|
474
|
+
}
|
|
475
|
+
</script>
|
|
476
|
+
</body>
|
|
477
|
+
</html>`;
|
|
478
|
+
}
|
|
479
|
+
function createAuthConnectorManifest() {
|
|
480
|
+
return {
|
|
481
|
+
manifest_version: 3,
|
|
482
|
+
name: "OpenRuntime Auth Connector",
|
|
483
|
+
version: AUTH_CONNECTOR_EXTENSION_VERSION,
|
|
484
|
+
description: "Exports browser auth state to a local OpenRuntime CLI command.",
|
|
485
|
+
permissions: [
|
|
486
|
+
"cookies",
|
|
487
|
+
"scripting",
|
|
488
|
+
"tabs"
|
|
489
|
+
],
|
|
490
|
+
host_permissions: [
|
|
491
|
+
"http://*/*",
|
|
492
|
+
"https://*/*"
|
|
493
|
+
],
|
|
494
|
+
background: {
|
|
495
|
+
service_worker: "service-worker.js"
|
|
496
|
+
},
|
|
497
|
+
content_scripts: [
|
|
498
|
+
{
|
|
499
|
+
matches: [
|
|
500
|
+
"http://127.0.0.1/*",
|
|
501
|
+
"http://localhost/*"
|
|
502
|
+
],
|
|
503
|
+
js: [
|
|
504
|
+
"setup-content.js"
|
|
505
|
+
]
|
|
506
|
+
}
|
|
507
|
+
],
|
|
508
|
+
icons: createAuthConnectorIconManifest(),
|
|
509
|
+
action: {
|
|
510
|
+
default_title: "OpenRuntime Auth Connector",
|
|
511
|
+
default_icon: createAuthConnectorActionIconManifest()
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
function createAuthConnectorIconManifest() {
|
|
516
|
+
return Object.fromEntries(AUTH_CONNECTOR_ICON_SIZES.map((size) => [String(size), `icon-${size}.png`]));
|
|
517
|
+
}
|
|
518
|
+
function createAuthConnectorActionIconManifest() {
|
|
519
|
+
return {
|
|
520
|
+
"16": "icon-16.png",
|
|
521
|
+
"32": "icon-32.png"
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
function getAuthConnectorPageIconSize(pathname) {
|
|
525
|
+
if (pathname === "/favicon.ico")
|
|
526
|
+
return 32;
|
|
527
|
+
const match = /^\/icon-(\d+)\.png$/.exec(pathname);
|
|
528
|
+
if (match === null)
|
|
529
|
+
return undefined;
|
|
530
|
+
const size = Number(match[1]);
|
|
531
|
+
return isAuthConnectorIconSize(size) ? size : undefined;
|
|
532
|
+
}
|
|
533
|
+
function isAuthConnectorIconSize(size) {
|
|
534
|
+
return AUTH_CONNECTOR_ICON_SIZES.includes(size);
|
|
535
|
+
}
|
|
536
|
+
async function readAuthConnectorIconPng(path) {
|
|
537
|
+
const icon = await readFile(resolve(path));
|
|
538
|
+
if (!isPng(icon)) {
|
|
539
|
+
throw new Error(`Auth connector extension icon must be a PNG file: ${path}.`);
|
|
540
|
+
}
|
|
541
|
+
return icon;
|
|
542
|
+
}
|
|
543
|
+
async function readAuthConnectorDefaultIconPng(size) {
|
|
544
|
+
const iconFileName = `icon-${size}.png`;
|
|
545
|
+
for (const iconPath of getAuthConnectorDefaultIconPaths(iconFileName)) {
|
|
546
|
+
try {
|
|
547
|
+
return await readAuthConnectorIconPng(iconPath);
|
|
548
|
+
}
|
|
549
|
+
catch (error) {
|
|
550
|
+
if (!isFileNotFoundError(error)) {
|
|
551
|
+
throw error;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return createAuthConnectorIconPng(size);
|
|
556
|
+
}
|
|
557
|
+
function getAuthConnectorDefaultIconPaths(iconFileName) {
|
|
558
|
+
const paths = [];
|
|
559
|
+
try {
|
|
560
|
+
if (import.meta.url.startsWith("file:")) {
|
|
561
|
+
paths.push(fileURLToPath(new URL(`../assets/auth-connector/${iconFileName}`, import.meta.url)));
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
catch {
|
|
565
|
+
// Some test runners rewrite import.meta.url in a way that still looks file-like.
|
|
566
|
+
}
|
|
567
|
+
paths.push(join(process.cwd(), "assets", "auth-connector", iconFileName));
|
|
568
|
+
return [...new Set(paths)];
|
|
569
|
+
}
|
|
570
|
+
function isFileNotFoundError(error) {
|
|
571
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
572
|
+
}
|
|
573
|
+
function isPng(buffer) {
|
|
574
|
+
return buffer.length >= PNG_SIGNATURE.length && PNG_SIGNATURE.every((byte, index) => buffer[index] === byte);
|
|
575
|
+
}
|
|
576
|
+
function createAuthConnectorIconPng(size) {
|
|
577
|
+
const stride = size * 4 + 1;
|
|
578
|
+
const raw = Buffer.alloc(stride * size);
|
|
579
|
+
const background = [17, 24, 39, 255];
|
|
580
|
+
const surface = [31, 41, 55, 255];
|
|
581
|
+
const cyan = [34, 211, 238, 255];
|
|
582
|
+
const green = [34, 197, 94, 255];
|
|
583
|
+
const white = [248, 250, 252, 255];
|
|
584
|
+
const transparent = [0, 0, 0, 0];
|
|
585
|
+
const radius = Math.max(3, Math.round(size * 0.18));
|
|
586
|
+
for (let y = 0; y < size; y += 1) {
|
|
587
|
+
raw[y * stride] = 0;
|
|
588
|
+
for (let x = 0; x < size; x += 1) {
|
|
589
|
+
const color = getAuthConnectorIconPixelColor(x, y, size, radius, {
|
|
590
|
+
background,
|
|
591
|
+
surface,
|
|
592
|
+
cyan,
|
|
593
|
+
green,
|
|
594
|
+
white,
|
|
595
|
+
transparent
|
|
596
|
+
});
|
|
597
|
+
const offset = y * stride + 1 + x * 4;
|
|
598
|
+
raw[offset] = color[0];
|
|
599
|
+
raw[offset + 1] = color[1];
|
|
600
|
+
raw[offset + 2] = color[2];
|
|
601
|
+
raw[offset + 3] = color[3];
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
const ihdr = Buffer.alloc(13);
|
|
605
|
+
ihdr.writeUInt32BE(size, 0);
|
|
606
|
+
ihdr.writeUInt32BE(size, 4);
|
|
607
|
+
ihdr[8] = 8;
|
|
608
|
+
ihdr[9] = 6;
|
|
609
|
+
ihdr[10] = 0;
|
|
610
|
+
ihdr[11] = 0;
|
|
611
|
+
ihdr[12] = 0;
|
|
612
|
+
return Buffer.concat([
|
|
613
|
+
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
|
614
|
+
createPngChunk("IHDR", ihdr),
|
|
615
|
+
createPngChunk("IDAT", deflateSync(raw)),
|
|
616
|
+
createPngChunk("IEND", Buffer.alloc(0))
|
|
617
|
+
]);
|
|
618
|
+
}
|
|
619
|
+
function getAuthConnectorIconPixelColor(x, y, size, radius, colors) {
|
|
620
|
+
if (isOutsideRoundedSquare(x, y, size, radius))
|
|
621
|
+
return colors.transparent;
|
|
622
|
+
const unit = size / 128;
|
|
623
|
+
if (isInsideRect(x, y, 29 * unit, 34 * unit, 41 * unit, 95 * unit))
|
|
624
|
+
return colors.cyan;
|
|
625
|
+
if (isInsideRect(x, y, 29 * unit, 34 * unit, 69 * unit, 46 * unit))
|
|
626
|
+
return colors.cyan;
|
|
627
|
+
if (isInsideRect(x, y, 29 * unit, 83 * unit, 69 * unit, 95 * unit))
|
|
628
|
+
return colors.cyan;
|
|
629
|
+
if (isInsideRect(x, y, 73 * unit, 34 * unit, 84 * unit, 95 * unit))
|
|
630
|
+
return colors.white;
|
|
631
|
+
if (isInsideRect(x, y, 84 * unit, 34 * unit, 102 * unit, 46 * unit))
|
|
632
|
+
return colors.white;
|
|
633
|
+
if (isInsideRect(x, y, 84 * unit, 58 * unit, 100 * unit, 70 * unit))
|
|
634
|
+
return colors.white;
|
|
635
|
+
if (isInsideRect(x, y, 84 * unit, 83 * unit, 105 * unit, 95 * unit))
|
|
636
|
+
return colors.white;
|
|
637
|
+
const center = 64 * unit;
|
|
638
|
+
const distance = Math.hypot(x + 0.5 - center, y + 0.5 - center);
|
|
639
|
+
if (distance >= 12 * unit && distance <= 22 * unit)
|
|
640
|
+
return colors.green;
|
|
641
|
+
if (isInsideRect(x, y, 18 * unit, 20 * unit, 110 * unit, 108 * unit))
|
|
642
|
+
return colors.surface;
|
|
643
|
+
return colors.background;
|
|
644
|
+
}
|
|
645
|
+
function isOutsideRoundedSquare(x, y, size, radius) {
|
|
646
|
+
const left = x < radius;
|
|
647
|
+
const right = x >= size - radius;
|
|
648
|
+
const top = y < radius;
|
|
649
|
+
const bottom = y >= size - radius;
|
|
650
|
+
if (!(left || right) || !(top || bottom))
|
|
651
|
+
return false;
|
|
652
|
+
const cornerX = left ? radius - 0.5 : size - radius - 0.5;
|
|
653
|
+
const cornerY = top ? radius - 0.5 : size - radius - 0.5;
|
|
654
|
+
return Math.hypot(x - cornerX, y - cornerY) > radius;
|
|
655
|
+
}
|
|
656
|
+
function isInsideRect(x, y, left, top, right, bottom) {
|
|
657
|
+
return x >= left && x < right && y >= top && y < bottom;
|
|
658
|
+
}
|
|
659
|
+
function createPngChunk(type, data) {
|
|
660
|
+
const typeBuffer = Buffer.from(type, "ascii");
|
|
661
|
+
const length = Buffer.alloc(4);
|
|
662
|
+
length.writeUInt32BE(data.byteLength, 0);
|
|
663
|
+
const crc = Buffer.alloc(4);
|
|
664
|
+
crc.writeUInt32BE(calculatePngCrc32(Buffer.concat([typeBuffer, data])), 0);
|
|
665
|
+
return Buffer.concat([length, typeBuffer, data, crc]);
|
|
666
|
+
}
|
|
667
|
+
function calculatePngCrc32(data) {
|
|
668
|
+
let crc = 0xffffffff;
|
|
669
|
+
for (const byte of data) {
|
|
670
|
+
crc = (PNG_CRC_TABLE[(crc ^ byte) & 0xff] ?? 0) ^ (crc >>> 8);
|
|
671
|
+
}
|
|
672
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
673
|
+
}
|
|
674
|
+
function createPngCrcTable() {
|
|
675
|
+
const table = [];
|
|
676
|
+
for (let index = 0; index < 256; index += 1) {
|
|
677
|
+
let value = index;
|
|
678
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
679
|
+
value = (value & 1) === 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
|
|
680
|
+
}
|
|
681
|
+
table[index] = value >>> 0;
|
|
682
|
+
}
|
|
683
|
+
return table;
|
|
684
|
+
}
|
|
685
|
+
const AUTH_CONNECTOR_SETUP_CONTENT_SCRIPT = `"use strict";
|
|
686
|
+
|
|
687
|
+
(async () => {
|
|
688
|
+
const url = new URL(window.location.href);
|
|
689
|
+
if (url.searchParams.get("${AUTH_CONNECTOR_QUERY_PARAM}") !== "1") return;
|
|
690
|
+
const token = url.searchParams.get("token");
|
|
691
|
+
if (!token) return;
|
|
692
|
+
let started = false;
|
|
693
|
+
|
|
694
|
+
async function start() {
|
|
695
|
+
if (started) return;
|
|
696
|
+
started = true;
|
|
697
|
+
try {
|
|
698
|
+
const response = await chrome.runtime.sendMessage({
|
|
699
|
+
type: "openruntime.auth.start",
|
|
700
|
+
connectorUrl: window.location.origin,
|
|
701
|
+
token
|
|
702
|
+
});
|
|
703
|
+
if (response && response.ok === true) {
|
|
704
|
+
window.postMessage({
|
|
705
|
+
type: "openruntime.auth.exportComplete",
|
|
706
|
+
token
|
|
707
|
+
}, window.location.origin);
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
started = false;
|
|
711
|
+
window.postMessage({
|
|
712
|
+
type: "openruntime.auth.exportError",
|
|
713
|
+
token,
|
|
714
|
+
message: response && response.message ? response.message : "Chrome auth export failed."
|
|
715
|
+
}, window.location.origin);
|
|
716
|
+
} catch (error) {
|
|
717
|
+
started = false;
|
|
718
|
+
window.postMessage({
|
|
719
|
+
type: "openruntime.auth.exportError",
|
|
720
|
+
token,
|
|
721
|
+
message: error instanceof Error ? error.message : String(error)
|
|
722
|
+
}, window.location.origin);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
window.postMessage({
|
|
727
|
+
type: "openruntime.auth.connectorReady",
|
|
728
|
+
token
|
|
729
|
+
}, window.location.origin);
|
|
730
|
+
|
|
731
|
+
window.addEventListener("message", (event) => {
|
|
732
|
+
if (event.source !== window || event.origin !== window.location.origin) return;
|
|
733
|
+
if (!event.data || event.data.type !== "openruntime.auth.startFromPage" || event.data.token !== token) return;
|
|
734
|
+
void start();
|
|
735
|
+
});
|
|
736
|
+
})();
|
|
737
|
+
`;
|
|
738
|
+
const AUTH_CONNECTOR_SERVICE_WORKER_SCRIPT = `"use strict";
|
|
739
|
+
|
|
740
|
+
const runningTokens = new Set();
|
|
741
|
+
|
|
742
|
+
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
743
|
+
if (!message || message.type !== "openruntime.auth.start") return false;
|
|
744
|
+
startExport(message).then(
|
|
745
|
+
() => sendResponse({ ok: true }),
|
|
746
|
+
(error) => {
|
|
747
|
+
reportError(message.connectorUrl, message.token, error).finally(() => {
|
|
748
|
+
sendResponse({ ok: false, message: error instanceof Error ? error.message : String(error) });
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
);
|
|
752
|
+
return true;
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
async function startExport(message) {
|
|
756
|
+
const connectorUrl = message.connectorUrl;
|
|
757
|
+
const token = message.token;
|
|
758
|
+
if (runningTokens.has(token)) return;
|
|
759
|
+
runningTokens.add(token);
|
|
760
|
+
try {
|
|
761
|
+
await postEvent(connectorUrl, token, "exporting", "正在打开目标网站。");
|
|
762
|
+
const request = await fetchJson(connectorUrl + "/request?token=" + encodeURIComponent(token));
|
|
763
|
+
const requestedUrl = request.requestedUrl;
|
|
764
|
+
const tab = await getOrCreateTargetTab(requestedUrl);
|
|
765
|
+
await waitForTabLoaded(tab.id);
|
|
766
|
+
await postEvent(connectorUrl, token, "exporting", "正在读取 Cookie 和浏览器存储。");
|
|
767
|
+
const cookies = await chrome.cookies.getAll({ url: requestedUrl });
|
|
768
|
+
const storage = await readTabStorage(tab.id);
|
|
769
|
+
await postJson(connectorUrl + "/export?token=" + encodeURIComponent(token), {
|
|
770
|
+
requestedUrl,
|
|
771
|
+
exportedAt: new Date().toISOString(),
|
|
772
|
+
cookies,
|
|
773
|
+
origins: [
|
|
774
|
+
{
|
|
775
|
+
origin: new URL(requestedUrl).origin,
|
|
776
|
+
localStorage: storage.localStorage,
|
|
777
|
+
sessionStorage: storage.sessionStorage
|
|
778
|
+
}
|
|
779
|
+
]
|
|
780
|
+
});
|
|
781
|
+
} finally {
|
|
782
|
+
runningTokens.delete(token);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
async function getOrCreateTargetTab(requestedUrl) {
|
|
787
|
+
const requestedOrigin = new URL(requestedUrl).origin;
|
|
788
|
+
const tabs = await chrome.tabs.query({});
|
|
789
|
+
const existing = tabs.find((tab) => {
|
|
790
|
+
try {
|
|
791
|
+
return tab.url && new URL(tab.url).origin === requestedOrigin;
|
|
792
|
+
} catch {
|
|
793
|
+
return false;
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
if (existing && existing.id !== undefined) {
|
|
797
|
+
await chrome.tabs.update(existing.id, { active: true });
|
|
798
|
+
return existing;
|
|
799
|
+
}
|
|
800
|
+
return await chrome.tabs.create({ url: requestedUrl, active: true });
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
async function waitForTabLoaded(tabId) {
|
|
804
|
+
const current = await chrome.tabs.get(tabId);
|
|
805
|
+
if (current.status === "complete") return;
|
|
806
|
+
await new Promise((resolve) => {
|
|
807
|
+
const listener = (updatedTabId, changeInfo) => {
|
|
808
|
+
if (updatedTabId === tabId && changeInfo.status === "complete") {
|
|
809
|
+
chrome.tabs.onUpdated.removeListener(listener);
|
|
810
|
+
resolve();
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
chrome.tabs.onUpdated.addListener(listener);
|
|
814
|
+
setTimeout(() => {
|
|
815
|
+
chrome.tabs.onUpdated.removeListener(listener);
|
|
816
|
+
resolve();
|
|
817
|
+
}, 15000);
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
async function readTabStorage(tabId) {
|
|
822
|
+
const [result] = await chrome.scripting.executeScript({
|
|
823
|
+
target: { tabId },
|
|
824
|
+
func: () => ({
|
|
825
|
+
localStorage: Array.from({ length: window.localStorage.length }, (_, index) => {
|
|
826
|
+
const name = window.localStorage.key(index);
|
|
827
|
+
return name === null ? null : { name, value: window.localStorage.getItem(name) ?? "" };
|
|
828
|
+
}).filter(Boolean),
|
|
829
|
+
sessionStorage: Array.from({ length: window.sessionStorage.length }, (_, index) => {
|
|
830
|
+
const name = window.sessionStorage.key(index);
|
|
831
|
+
return name === null ? null : { name, value: window.sessionStorage.getItem(name) ?? "" };
|
|
832
|
+
}).filter(Boolean)
|
|
833
|
+
})
|
|
834
|
+
});
|
|
835
|
+
return result && result.result ? result.result : { localStorage: [], sessionStorage: [] };
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
async function fetchJson(url) {
|
|
839
|
+
const response = await fetch(url, { cache: "no-store" });
|
|
840
|
+
if (!response.ok) throw new Error("OpenRuntime connector request failed: " + response.status);
|
|
841
|
+
return await response.json();
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
async function postEvent(connectorUrl, token, status, message) {
|
|
845
|
+
await postJson(connectorUrl + "/event?token=" + encodeURIComponent(token), { status, message });
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
async function postJson(url, body) {
|
|
849
|
+
const response = await fetch(url, {
|
|
850
|
+
method: "POST",
|
|
851
|
+
headers: { "content-type": "application/json" },
|
|
852
|
+
body: JSON.stringify(body)
|
|
853
|
+
});
|
|
854
|
+
if (!response.ok) throw new Error("OpenRuntime connector post failed: " + response.status);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
async function reportError(connectorUrl, token, error) {
|
|
858
|
+
try {
|
|
859
|
+
await postJson(connectorUrl + "/error?token=" + encodeURIComponent(token), {
|
|
860
|
+
message: error instanceof Error ? error.message : String(error)
|
|
861
|
+
});
|
|
862
|
+
} catch {
|
|
863
|
+
// The CLI may have exited already.
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
`;
|
|
867
|
+
function assertValidToken(url, state) {
|
|
868
|
+
if (url.searchParams.get("token") !== state.token) {
|
|
869
|
+
throw new Error("Invalid auth connector token.");
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
function assertAuthConnectorPayload(value) {
|
|
873
|
+
const payload = getRecord(value);
|
|
874
|
+
if (payload === undefined || typeof payload.requestedUrl !== "string" || !Array.isArray(payload.cookies) || !Array.isArray(payload.origins)) {
|
|
875
|
+
throw new Error("Invalid auth connector payload.");
|
|
876
|
+
}
|
|
877
|
+
return payload;
|
|
878
|
+
}
|
|
879
|
+
async function readJsonBody(request) {
|
|
880
|
+
const chunks = [];
|
|
881
|
+
let total = 0;
|
|
882
|
+
for await (const chunk of request) {
|
|
883
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
884
|
+
total += buffer.byteLength;
|
|
885
|
+
if (total > AUTH_CONNECTOR_MAX_BODY_BYTES) {
|
|
886
|
+
throw new Error("Auth connector payload is too large.");
|
|
887
|
+
}
|
|
888
|
+
chunks.push(buffer);
|
|
889
|
+
}
|
|
890
|
+
if (chunks.length === 0)
|
|
891
|
+
return {};
|
|
892
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
893
|
+
}
|
|
894
|
+
function normalizeRequestedHttpUrl(input) {
|
|
895
|
+
let url;
|
|
896
|
+
const trimmed = input.trim();
|
|
897
|
+
const urlLike = hasUrlScheme(trimmed) ? trimmed : `https://${trimmed}`;
|
|
898
|
+
try {
|
|
899
|
+
url = new URL(urlLike);
|
|
900
|
+
}
|
|
901
|
+
catch {
|
|
902
|
+
throw new Error(`Invalid auth export URL "${input}".`);
|
|
903
|
+
}
|
|
904
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
905
|
+
throw new Error("Auth export URL must use http or https.");
|
|
906
|
+
}
|
|
907
|
+
return url.href;
|
|
908
|
+
}
|
|
909
|
+
function hasUrlScheme(input) {
|
|
910
|
+
return /^[a-zA-Z][a-zA-Z\d+.-]*:\/\//.test(input);
|
|
911
|
+
}
|
|
912
|
+
function createOpenChromeCommand(url, platform) {
|
|
913
|
+
if (platform === "darwin") {
|
|
914
|
+
return {
|
|
915
|
+
command: "open",
|
|
916
|
+
args: ["-a", "Google Chrome", url]
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
if (platform === "win32") {
|
|
920
|
+
return {
|
|
921
|
+
command: "cmd",
|
|
922
|
+
args: ["/c", "start", "", "chrome", url]
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
return {
|
|
926
|
+
command: "xdg-open",
|
|
927
|
+
args: [url]
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
async function listen(server) {
|
|
931
|
+
await new Promise((resolvePromise, reject) => {
|
|
932
|
+
server.once("error", reject);
|
|
933
|
+
server.listen(0, "127.0.0.1", () => {
|
|
934
|
+
server.off("error", reject);
|
|
935
|
+
resolvePromise();
|
|
936
|
+
});
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
function trackServerSockets(server) {
|
|
940
|
+
const sockets = new Set();
|
|
941
|
+
server.on("connection", (socket) => {
|
|
942
|
+
sockets.add(socket);
|
|
943
|
+
socket.once("close", () => {
|
|
944
|
+
sockets.delete(socket);
|
|
945
|
+
});
|
|
946
|
+
});
|
|
947
|
+
return sockets;
|
|
948
|
+
}
|
|
949
|
+
async function closeServer(server, sockets) {
|
|
950
|
+
if (!server.listening)
|
|
951
|
+
return;
|
|
952
|
+
await new Promise((resolvePromise, reject) => {
|
|
953
|
+
const forceCloseTimer = setTimeout(() => {
|
|
954
|
+
server.closeAllConnections();
|
|
955
|
+
for (const socket of sockets) {
|
|
956
|
+
socket.destroy();
|
|
957
|
+
}
|
|
958
|
+
}, 500);
|
|
959
|
+
server.close((error) => {
|
|
960
|
+
clearTimeout(forceCloseTimer);
|
|
961
|
+
if (error !== undefined) {
|
|
962
|
+
reject(error);
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
resolvePromise();
|
|
966
|
+
});
|
|
967
|
+
server.closeIdleConnections();
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
async function withTimeout(promise, timeout, createErrorForTimeout) {
|
|
971
|
+
let timer;
|
|
972
|
+
try {
|
|
973
|
+
return await Promise.race([
|
|
974
|
+
promise,
|
|
975
|
+
new Promise((_, reject) => {
|
|
976
|
+
timer = setTimeout(() => {
|
|
977
|
+
reject(createErrorForTimeout());
|
|
978
|
+
}, timeout);
|
|
979
|
+
})
|
|
980
|
+
]);
|
|
981
|
+
}
|
|
982
|
+
finally {
|
|
983
|
+
if (timer !== undefined)
|
|
984
|
+
clearTimeout(timer);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
function sendHtml(response, html) {
|
|
988
|
+
response.writeHead(200, {
|
|
989
|
+
"content-type": "text/html; charset=utf-8",
|
|
990
|
+
"cache-control": "no-store",
|
|
991
|
+
"connection": "close"
|
|
992
|
+
});
|
|
993
|
+
response.end(html);
|
|
994
|
+
}
|
|
995
|
+
function sendPng(response, png) {
|
|
996
|
+
response.writeHead(200, {
|
|
997
|
+
"content-type": "image/png",
|
|
998
|
+
"cache-control": "public, max-age=3600",
|
|
999
|
+
"connection": "close"
|
|
1000
|
+
});
|
|
1001
|
+
response.end(png);
|
|
1002
|
+
}
|
|
1003
|
+
function sendJson(response, value, status = 200) {
|
|
1004
|
+
response.writeHead(status, {
|
|
1005
|
+
"content-type": "application/json; charset=utf-8",
|
|
1006
|
+
"cache-control": "no-store",
|
|
1007
|
+
"connection": "close"
|
|
1008
|
+
});
|
|
1009
|
+
response.end(JSON.stringify(value));
|
|
1010
|
+
}
|
|
1011
|
+
async function writeJsonFile(path, value) {
|
|
1012
|
+
await mkdir(dirname(path), {
|
|
1013
|
+
recursive: true,
|
|
1014
|
+
mode: 0o700
|
|
1015
|
+
});
|
|
1016
|
+
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, {
|
|
1017
|
+
encoding: "utf8",
|
|
1018
|
+
mode: 0o600
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
function getRecord(value) {
|
|
1022
|
+
return value !== null && typeof value === "object" ? value : undefined;
|
|
1023
|
+
}
|
|
1024
|
+
function escapeHtml(value) {
|
|
1025
|
+
return value
|
|
1026
|
+
.replaceAll("&", "&")
|
|
1027
|
+
.replaceAll("<", "<")
|
|
1028
|
+
.replaceAll(">", ">")
|
|
1029
|
+
.replaceAll("\"", """);
|
|
1030
|
+
}
|
|
1031
|
+
//# sourceMappingURL=auth-connector.js.map
|