sweetlink 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/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/README.md +237 -0
- package/dist/daemon/src/codename.d.ts +8 -0
- package/dist/daemon/src/codename.d.ts.map +1 -0
- package/dist/daemon/src/codename.js +42 -0
- package/dist/daemon/src/codename.js.map +1 -0
- package/dist/daemon/src/index.d.ts +3 -0
- package/dist/daemon/src/index.d.ts.map +1 -0
- package/dist/daemon/src/index.js +675 -0
- package/dist/daemon/src/index.js.map +1 -0
- package/dist/shared/src/env.d.ts +24 -0
- package/dist/shared/src/env.d.ts.map +1 -0
- package/dist/shared/src/env.js +39 -0
- package/dist/shared/src/env.js.map +1 -0
- package/dist/shared/src/index.d.ts +211 -0
- package/dist/shared/src/index.d.ts.map +1 -0
- package/dist/shared/src/index.js +73 -0
- package/dist/shared/src/index.js.map +1 -0
- package/dist/shared/src/node.d.ts +12 -0
- package/dist/shared/src/node.d.ts.map +1 -0
- package/dist/shared/src/node.js +67 -0
- package/dist/shared/src/node.js.map +1 -0
- package/dist/src/codex.d.ts +11 -0
- package/dist/src/codex.d.ts.map +1 -0
- package/dist/src/codex.js +67 -0
- package/dist/src/codex.js.map +1 -0
- package/dist/src/commands/click.d.ts +3 -0
- package/dist/src/commands/click.d.ts.map +1 -0
- package/dist/src/commands/click.js +93 -0
- package/dist/src/commands/click.js.map +1 -0
- package/dist/src/commands/run-js.d.ts +4 -0
- package/dist/src/commands/run-js.d.ts.map +1 -0
- package/dist/src/commands/run-js.js +28 -0
- package/dist/src/commands/run-js.js.map +1 -0
- package/dist/src/commands/trust-ca.d.ts +3 -0
- package/dist/src/commands/trust-ca.d.ts.map +1 -0
- package/dist/src/commands/trust-ca.js +43 -0
- package/dist/src/commands/trust-ca.js.map +1 -0
- package/dist/src/core/config-file.d.ts +41 -0
- package/dist/src/core/config-file.d.ts.map +1 -0
- package/dist/src/core/config-file.js +284 -0
- package/dist/src/core/config-file.js.map +1 -0
- package/dist/src/core/config.d.ts +23 -0
- package/dist/src/core/config.d.ts.map +1 -0
- package/dist/src/core/config.js +132 -0
- package/dist/src/core/config.js.map +1 -0
- package/dist/src/core/env.d.ts +11 -0
- package/dist/src/core/env.d.ts.map +1 -0
- package/dist/src/core/env.js +30 -0
- package/dist/src/core/env.js.map +1 -0
- package/dist/src/devtools-registry.d.ts +3 -0
- package/dist/src/devtools-registry.d.ts.map +1 -0
- package/dist/src/devtools-registry.js +294 -0
- package/dist/src/devtools-registry.js.map +1 -0
- package/dist/src/env.d.ts +16 -0
- package/dist/src/env.d.ts.map +1 -0
- package/dist/src/env.js +17 -0
- package/dist/src/env.js.map +1 -0
- package/dist/src/http.d.ts +2 -0
- package/dist/src/http.d.ts.map +1 -0
- package/dist/src/http.js +53 -0
- package/dist/src/http.js.map +1 -0
- package/dist/src/index.d.ts +19 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +1941 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/runtime/browser/client.d.ts +10 -0
- package/dist/src/runtime/browser/client.d.ts.map +1 -0
- package/dist/src/runtime/browser/client.js +497 -0
- package/dist/src/runtime/browser/client.js.map +1 -0
- package/dist/src/runtime/browser/commands/index.d.ts +10 -0
- package/dist/src/runtime/browser/commands/index.d.ts.map +1 -0
- package/dist/src/runtime/browser/commands/index.js +219 -0
- package/dist/src/runtime/browser/commands/index.js.map +1 -0
- package/dist/src/runtime/browser/dom-to-image-loader.d.ts +6 -0
- package/dist/src/runtime/browser/dom-to-image-loader.d.ts.map +1 -0
- package/dist/src/runtime/browser/dom-to-image-loader.js +50 -0
- package/dist/src/runtime/browser/dom-to-image-loader.js.map +1 -0
- package/dist/src/runtime/browser/index.d.ts +4 -0
- package/dist/src/runtime/browser/index.d.ts.map +1 -0
- package/dist/src/runtime/browser/index.js +4 -0
- package/dist/src/runtime/browser/index.js.map +1 -0
- package/dist/src/runtime/browser/module-loader.d.ts +6 -0
- package/dist/src/runtime/browser/module-loader.d.ts.map +1 -0
- package/dist/src/runtime/browser/module-loader.js +26 -0
- package/dist/src/runtime/browser/module-loader.js.map +1 -0
- package/dist/src/runtime/browser/screenshot/hooks.d.ts +71 -0
- package/dist/src/runtime/browser/screenshot/hooks.d.ts.map +1 -0
- package/dist/src/runtime/browser/screenshot/hooks.js +219 -0
- package/dist/src/runtime/browser/screenshot/hooks.js.map +1 -0
- package/dist/src/runtime/browser/screenshot/index.d.ts +9 -0
- package/dist/src/runtime/browser/screenshot/index.d.ts.map +1 -0
- package/dist/src/runtime/browser/screenshot/index.js +90 -0
- package/dist/src/runtime/browser/screenshot/index.js.map +1 -0
- package/dist/src/runtime/browser/screenshot/renderers/dom-to-image.d.ts +13 -0
- package/dist/src/runtime/browser/screenshot/renderers/dom-to-image.d.ts.map +1 -0
- package/dist/src/runtime/browser/screenshot/renderers/dom-to-image.js +51 -0
- package/dist/src/runtime/browser/screenshot/renderers/dom-to-image.js.map +1 -0
- package/dist/src/runtime/browser/screenshot/renderers/html2canvas.d.ts +12 -0
- package/dist/src/runtime/browser/screenshot/renderers/html2canvas.d.ts.map +1 -0
- package/dist/src/runtime/browser/screenshot/renderers/html2canvas.js +136 -0
- package/dist/src/runtime/browser/screenshot/renderers/html2canvas.js.map +1 -0
- package/dist/src/runtime/browser/screenshot/targets.d.ts +6 -0
- package/dist/src/runtime/browser/screenshot/targets.d.ts.map +1 -0
- package/dist/src/runtime/browser/screenshot/targets.js +53 -0
- package/dist/src/runtime/browser/screenshot/targets.js.map +1 -0
- package/dist/src/runtime/browser/screenshot/utils.d.ts +9 -0
- package/dist/src/runtime/browser/screenshot/utils.d.ts.map +1 -0
- package/dist/src/runtime/browser/screenshot/utils.js +471 -0
- package/dist/src/runtime/browser/screenshot/utils.js.map +1 -0
- package/dist/src/runtime/browser/selector-discovery.d.ts +9 -0
- package/dist/src/runtime/browser/selector-discovery.d.ts.map +1 -0
- package/dist/src/runtime/browser/selector-discovery.js +218 -0
- package/dist/src/runtime/browser/selector-discovery.js.map +1 -0
- package/dist/src/runtime/browser/storage/session-storage.d.ts +13 -0
- package/dist/src/runtime/browser/storage/session-storage.d.ts.map +1 -0
- package/dist/src/runtime/browser/storage/session-storage.js +119 -0
- package/dist/src/runtime/browser/storage/session-storage.js.map +1 -0
- package/dist/src/runtime/browser/types.d.ts +96 -0
- package/dist/src/runtime/browser/types.d.ts.map +1 -0
- package/dist/src/runtime/browser/types.js +6 -0
- package/dist/src/runtime/browser/types.js.map +1 -0
- package/dist/src/runtime/browser/utils/console.d.ts +5 -0
- package/dist/src/runtime/browser/utils/console.d.ts.map +1 -0
- package/dist/src/runtime/browser/utils/console.js +54 -0
- package/dist/src/runtime/browser/utils/console.js.map +1 -0
- package/dist/src/runtime/browser/utils/environment.d.ts +3 -0
- package/dist/src/runtime/browser/utils/environment.d.ts.map +1 -0
- package/dist/src/runtime/browser/utils/environment.js +13 -0
- package/dist/src/runtime/browser/utils/environment.js.map +1 -0
- package/dist/src/runtime/browser/utils/errors.d.ts +3 -0
- package/dist/src/runtime/browser/utils/errors.d.ts.map +1 -0
- package/dist/src/runtime/browser/utils/errors.js +42 -0
- package/dist/src/runtime/browser/utils/errors.js.map +1 -0
- package/dist/src/runtime/browser/utils/number.d.ts +2 -0
- package/dist/src/runtime/browser/utils/number.d.ts.map +1 -0
- package/dist/src/runtime/browser/utils/number.js +2 -0
- package/dist/src/runtime/browser/utils/number.js.map +1 -0
- package/dist/src/runtime/browser/utils/object.d.ts +3 -0
- package/dist/src/runtime/browser/utils/object.d.ts.map +1 -0
- package/dist/src/runtime/browser/utils/object.js +9 -0
- package/dist/src/runtime/browser/utils/object.js.map +1 -0
- package/dist/src/runtime/browser/utils/sanitize.d.ts +2 -0
- package/dist/src/runtime/browser/utils/sanitize.d.ts.map +1 -0
- package/dist/src/runtime/browser/utils/sanitize.js +28 -0
- package/dist/src/runtime/browser/utils/sanitize.js.map +1 -0
- package/dist/src/runtime/browser/utils/time.d.ts +2 -0
- package/dist/src/runtime/browser/utils/time.d.ts.map +1 -0
- package/dist/src/runtime/browser/utils/time.js +7 -0
- package/dist/src/runtime/browser/utils/time.js.map +1 -0
- package/dist/src/runtime/chrome/constants.d.ts +5 -0
- package/dist/src/runtime/chrome/constants.d.ts.map +1 -0
- package/dist/src/runtime/chrome/constants.js +5 -0
- package/dist/src/runtime/chrome/constants.js.map +1 -0
- package/dist/src/runtime/chrome/cookies.d.ts +20 -0
- package/dist/src/runtime/chrome/cookies.d.ts.map +1 -0
- package/dist/src/runtime/chrome/cookies.js +139 -0
- package/dist/src/runtime/chrome/cookies.js.map +1 -0
- package/dist/src/runtime/chrome/diagnostics.d.ts +6 -0
- package/dist/src/runtime/chrome/diagnostics.d.ts.map +1 -0
- package/dist/src/runtime/chrome/diagnostics.js +49 -0
- package/dist/src/runtime/chrome/diagnostics.js.map +1 -0
- package/dist/src/runtime/chrome/focus.d.ts +2 -0
- package/dist/src/runtime/chrome/focus.d.ts.map +1 -0
- package/dist/src/runtime/chrome/focus.js +41 -0
- package/dist/src/runtime/chrome/focus.js.map +1 -0
- package/dist/src/runtime/chrome/launch.d.ts +23 -0
- package/dist/src/runtime/chrome/launch.d.ts.map +1 -0
- package/dist/src/runtime/chrome/launch.js +102 -0
- package/dist/src/runtime/chrome/launch.js.map +1 -0
- package/dist/src/runtime/chrome/puppeteer.d.ts +7 -0
- package/dist/src/runtime/chrome/puppeteer.d.ts.map +1 -0
- package/dist/src/runtime/chrome/puppeteer.js +87 -0
- package/dist/src/runtime/chrome/puppeteer.js.map +1 -0
- package/dist/src/runtime/chrome/reuse/constants.d.ts +4 -0
- package/dist/src/runtime/chrome/reuse/constants.d.ts.map +1 -0
- package/dist/src/runtime/chrome/reuse/constants.js +4 -0
- package/dist/src/runtime/chrome/reuse/constants.js.map +1 -0
- package/dist/src/runtime/chrome/reuse.d.ts +13 -0
- package/dist/src/runtime/chrome/reuse.d.ts.map +1 -0
- package/dist/src/runtime/chrome/reuse.js +183 -0
- package/dist/src/runtime/chrome/reuse.js.map +1 -0
- package/dist/src/runtime/chrome/session.d.ts +13 -0
- package/dist/src/runtime/chrome/session.d.ts.map +1 -0
- package/dist/src/runtime/chrome/session.js +47 -0
- package/dist/src/runtime/chrome/session.js.map +1 -0
- package/dist/src/runtime/chrome.d.ts +9 -0
- package/dist/src/runtime/chrome.d.ts.map +1 -0
- package/dist/src/runtime/chrome.js +10 -0
- package/dist/src/runtime/chrome.js.map +1 -0
- package/dist/src/runtime/cookies.d.ts +37 -0
- package/dist/src/runtime/cookies.d.ts.map +1 -0
- package/dist/src/runtime/cookies.js +592 -0
- package/dist/src/runtime/cookies.js.map +1 -0
- package/dist/src/runtime/devstack.d.ts +14 -0
- package/dist/src/runtime/devstack.d.ts.map +1 -0
- package/dist/src/runtime/devstack.js +174 -0
- package/dist/src/runtime/devstack.js.map +1 -0
- package/dist/src/runtime/devtools/background.d.ts +5 -0
- package/dist/src/runtime/devtools/background.d.ts.map +1 -0
- package/dist/src/runtime/devtools/background.js +109 -0
- package/dist/src/runtime/devtools/background.js.map +1 -0
- package/dist/src/runtime/devtools/cdp.d.ts +16 -0
- package/dist/src/runtime/devtools/cdp.d.ts.map +1 -0
- package/dist/src/runtime/devtools/cdp.js +392 -0
- package/dist/src/runtime/devtools/cdp.js.map +1 -0
- package/dist/src/runtime/devtools/config.d.ts +12 -0
- package/dist/src/runtime/devtools/config.d.ts.map +1 -0
- package/dist/src/runtime/devtools/config.js +78 -0
- package/dist/src/runtime/devtools/config.js.map +1 -0
- package/dist/src/runtime/devtools/constants.d.ts +10 -0
- package/dist/src/runtime/devtools/constants.d.ts.map +1 -0
- package/dist/src/runtime/devtools/constants.js +18 -0
- package/dist/src/runtime/devtools/constants.js.map +1 -0
- package/dist/src/runtime/devtools/diagnostics.d.ts +8 -0
- package/dist/src/runtime/devtools/diagnostics.d.ts.map +1 -0
- package/dist/src/runtime/devtools/diagnostics.js +189 -0
- package/dist/src/runtime/devtools/diagnostics.js.map +1 -0
- package/dist/src/runtime/devtools/oauth.d.ts +9 -0
- package/dist/src/runtime/devtools/oauth.d.ts.map +1 -0
- package/dist/src/runtime/devtools/oauth.js +130 -0
- package/dist/src/runtime/devtools/oauth.js.map +1 -0
- package/dist/src/runtime/devtools/types.d.ts +108 -0
- package/dist/src/runtime/devtools/types.d.ts.map +1 -0
- package/dist/src/runtime/devtools/types.js +2 -0
- package/dist/src/runtime/devtools/types.js.map +1 -0
- package/dist/src/runtime/devtools.d.ts +8 -0
- package/dist/src/runtime/devtools.d.ts.map +1 -0
- package/dist/src/runtime/devtools.js +9 -0
- package/dist/src/runtime/devtools.js.map +1 -0
- package/dist/src/runtime/next-devtools.d.ts +2 -0
- package/dist/src/runtime/next-devtools.d.ts.map +1 -0
- package/dist/src/runtime/next-devtools.js +72 -0
- package/dist/src/runtime/next-devtools.js.map +1 -0
- package/dist/src/runtime/screenshot.d.ts +78 -0
- package/dist/src/runtime/screenshot.d.ts.map +1 -0
- package/dist/src/runtime/screenshot.js +232 -0
- package/dist/src/runtime/screenshot.js.map +1 -0
- package/dist/src/runtime/scripts.d.ts +16 -0
- package/dist/src/runtime/scripts.d.ts.map +1 -0
- package/dist/src/runtime/scripts.js +110 -0
- package/dist/src/runtime/scripts.js.map +1 -0
- package/dist/src/runtime/session.d.ts +64 -0
- package/dist/src/runtime/session.d.ts.map +1 -0
- package/dist/src/runtime/session.js +205 -0
- package/dist/src/runtime/session.js.map +1 -0
- package/dist/src/runtime/smoke.d.ts +36 -0
- package/dist/src/runtime/smoke.d.ts.map +1 -0
- package/dist/src/runtime/smoke.js +417 -0
- package/dist/src/runtime/smoke.js.map +1 -0
- package/dist/src/runtime/url.d.ts +10 -0
- package/dist/src/runtime/url.d.ts.map +1 -0
- package/dist/src/runtime/url.js +147 -0
- package/dist/src/runtime/url.js.map +1 -0
- package/dist/src/screenshot-hooks.d.ts +13 -0
- package/dist/src/screenshot-hooks.d.ts.map +1 -0
- package/dist/src/screenshot-hooks.js +56 -0
- package/dist/src/screenshot-hooks.js.map +1 -0
- package/dist/src/shared/env.d.ts +23 -0
- package/dist/src/shared/env.d.ts.map +1 -0
- package/dist/src/shared/env.js +38 -0
- package/dist/src/shared/env.js.map +1 -0
- package/dist/src/shared/index.d.ts +211 -0
- package/dist/src/shared/index.d.ts.map +1 -0
- package/dist/src/shared/index.js +73 -0
- package/dist/src/shared/index.js.map +1 -0
- package/dist/src/shared/node.d.ts +12 -0
- package/dist/src/shared/node.d.ts.map +1 -0
- package/dist/src/shared/node.js +67 -0
- package/dist/src/shared/node.js.map +1 -0
- package/dist/src/token.d.ts +4 -0
- package/dist/src/token.d.ts.map +1 -0
- package/dist/src/token.js +58 -0
- package/dist/src/token.js.map +1 -0
- package/dist/src/types.d.ts +17 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/util/app-label.d.ts +5 -0
- package/dist/src/util/app-label.d.ts.map +1 -0
- package/dist/src/util/app-label.js +21 -0
- package/dist/src/util/app-label.js.map +1 -0
- package/dist/src/util/errors.d.ts +9 -0
- package/dist/src/util/errors.d.ts.map +1 -0
- package/dist/src/util/errors.js +56 -0
- package/dist/src/util/errors.js.map +1 -0
- package/dist/src/util/path.d.ts +3 -0
- package/dist/src/util/path.d.ts.map +1 -0
- package/dist/src/util/path.js +6 -0
- package/dist/src/util/path.js.map +1 -0
- package/dist/src/util/regex.d.ts +4 -0
- package/dist/src/util/regex.d.ts.map +1 -0
- package/dist/src/util/regex.js +5 -0
- package/dist/src/util/regex.js.map +1 -0
- package/dist/src/util/time.d.ts +3 -0
- package/dist/src/util/time.d.ts.map +1 -0
- package/dist/src/util/time.js +7 -0
- package/dist/src/util/time.js.map +1 -0
- package/package.json +86 -0
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import 'dotenv/config';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { createServer as createHttpsServer, request as httpsRequest } from 'node:https';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { URL } from 'node:url';
|
|
9
|
+
import { createSweetLinkCommandId, SWEETLINK_DEFAULT_PORT, SWEETLINK_HEARTBEAT_INTERVAL_MS, SWEETLINK_HEARTBEAT_TOLERANCE_MS, SWEETLINK_WS_PATH, verifySweetLinkToken, } from '@sweetlink/shared';
|
|
10
|
+
import { readSweetLinkEnv } from '@sweetlink/shared/env';
|
|
11
|
+
import { getDefaultSweetLinkSecretPath, resolveSweetLinkSecret, } from '@sweetlink/shared/node';
|
|
12
|
+
import WebSocket, { WebSocketServer } from 'ws';
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import { generateSessionCodename } from './codename.js';
|
|
15
|
+
const SHUTDOWN_GRACE_MS = 1000;
|
|
16
|
+
const unrefTimer = (handle) => {
|
|
17
|
+
const candidate = handle;
|
|
18
|
+
if (typeof candidate === 'object' && candidate !== null && 'unref' in candidate) {
|
|
19
|
+
const unref = candidate.unref;
|
|
20
|
+
if (typeof unref === 'function') {
|
|
21
|
+
unref.call(candidate);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const toError = (value) => {
|
|
26
|
+
if (value instanceof Error) {
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
if (typeof value === 'string') {
|
|
30
|
+
return Object.assign(new Error(value), { cause: value });
|
|
31
|
+
}
|
|
32
|
+
if (value && typeof value === 'object' && 'message' in value) {
|
|
33
|
+
const candidate = value.message;
|
|
34
|
+
if (typeof candidate === 'string' && candidate.trim().length > 0) {
|
|
35
|
+
return Object.assign(new Error(candidate.trim()), { cause: value });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return Object.assign(new Error('Unknown error'), { cause: value });
|
|
39
|
+
};
|
|
40
|
+
const getErrorMessage = (value) => {
|
|
41
|
+
const error = toError(value);
|
|
42
|
+
return error.message || 'Unknown error';
|
|
43
|
+
};
|
|
44
|
+
const CERT_DIR = path.join(os.homedir(), '.sweetlink', 'certs');
|
|
45
|
+
const CERT_PATH = path.join(CERT_DIR, 'localhost-cert.pem');
|
|
46
|
+
const KEY_PATH = path.join(CERT_DIR, 'localhost-key.pem');
|
|
47
|
+
const SOCKET_STATE_LABEL = {
|
|
48
|
+
0: 'connecting',
|
|
49
|
+
1: 'open',
|
|
50
|
+
2: 'closing',
|
|
51
|
+
3: 'closed',
|
|
52
|
+
};
|
|
53
|
+
const SweetLinkConsoleLevelSchema = z.enum(['log', 'info', 'warn', 'error', 'debug']);
|
|
54
|
+
const consoleEventSchema = z
|
|
55
|
+
.object({
|
|
56
|
+
id: z.string(),
|
|
57
|
+
timestamp: z.number(),
|
|
58
|
+
level: SweetLinkConsoleLevelSchema,
|
|
59
|
+
args: z.array(z.unknown()),
|
|
60
|
+
})
|
|
61
|
+
.passthrough();
|
|
62
|
+
const commandResultSchema = z.discriminatedUnion('ok', [
|
|
63
|
+
z
|
|
64
|
+
.object({
|
|
65
|
+
ok: z.literal(true),
|
|
66
|
+
commandId: z.string(),
|
|
67
|
+
durationMs: z.number(),
|
|
68
|
+
data: z.unknown().optional(),
|
|
69
|
+
console: z.array(consoleEventSchema).optional(),
|
|
70
|
+
})
|
|
71
|
+
.passthrough(),
|
|
72
|
+
z
|
|
73
|
+
.object({
|
|
74
|
+
ok: z.literal(false),
|
|
75
|
+
commandId: z.string(),
|
|
76
|
+
durationMs: z.number(),
|
|
77
|
+
error: z.string(),
|
|
78
|
+
stack: z.string().optional(),
|
|
79
|
+
console: z.array(consoleEventSchema).optional(),
|
|
80
|
+
})
|
|
81
|
+
.passthrough(),
|
|
82
|
+
]);
|
|
83
|
+
const registerMessageSchema = z
|
|
84
|
+
.object({
|
|
85
|
+
kind: z.literal('register'),
|
|
86
|
+
token: z.string(),
|
|
87
|
+
sessionId: z.string(),
|
|
88
|
+
url: z.string(),
|
|
89
|
+
title: z.string(),
|
|
90
|
+
userAgent: z.string(),
|
|
91
|
+
topOrigin: z.string(),
|
|
92
|
+
})
|
|
93
|
+
.passthrough();
|
|
94
|
+
const heartbeatMessageSchema = z
|
|
95
|
+
.object({
|
|
96
|
+
kind: z.literal('heartbeat'),
|
|
97
|
+
sessionId: z.string(),
|
|
98
|
+
})
|
|
99
|
+
.passthrough();
|
|
100
|
+
const commandResultMessageSchema = z
|
|
101
|
+
.object({
|
|
102
|
+
kind: z.literal('commandResult'),
|
|
103
|
+
sessionId: z.string(),
|
|
104
|
+
result: commandResultSchema,
|
|
105
|
+
})
|
|
106
|
+
.passthrough();
|
|
107
|
+
const consoleMessageSchema = z
|
|
108
|
+
.object({
|
|
109
|
+
kind: z.literal('console'),
|
|
110
|
+
sessionId: z.string(),
|
|
111
|
+
events: z.array(consoleEventSchema),
|
|
112
|
+
})
|
|
113
|
+
.passthrough();
|
|
114
|
+
const clientMessageSchema = z.discriminatedUnion('kind', [
|
|
115
|
+
registerMessageSchema,
|
|
116
|
+
heartbeatMessageSchema,
|
|
117
|
+
commandResultMessageSchema,
|
|
118
|
+
consoleMessageSchema,
|
|
119
|
+
]);
|
|
120
|
+
const runScriptCommandSchema = z
|
|
121
|
+
.object({
|
|
122
|
+
type: z.literal('runScript'),
|
|
123
|
+
code: z.string(),
|
|
124
|
+
timeoutMs: z.number().finite().positive().optional(),
|
|
125
|
+
captureConsole: z.boolean().optional(),
|
|
126
|
+
})
|
|
127
|
+
.passthrough();
|
|
128
|
+
const getDomCommandSchema = z
|
|
129
|
+
.object({
|
|
130
|
+
type: z.literal('getDom'),
|
|
131
|
+
selector: z.string().optional(),
|
|
132
|
+
includeShadowDom: z.boolean().optional(),
|
|
133
|
+
})
|
|
134
|
+
.passthrough();
|
|
135
|
+
const navigateCommandSchema = z
|
|
136
|
+
.object({
|
|
137
|
+
type: z.literal('navigate'),
|
|
138
|
+
url: z.string(),
|
|
139
|
+
})
|
|
140
|
+
.passthrough();
|
|
141
|
+
const pingCommandSchema = z
|
|
142
|
+
.object({
|
|
143
|
+
type: z.literal('ping'),
|
|
144
|
+
})
|
|
145
|
+
.passthrough();
|
|
146
|
+
const screenshotCommandSchema = z
|
|
147
|
+
.object({
|
|
148
|
+
type: z.literal('screenshot'),
|
|
149
|
+
mode: z.union([z.literal('full'), z.literal('element')]).default('full'),
|
|
150
|
+
selector: z.union([z.string(), z.null()]).optional(),
|
|
151
|
+
quality: z.number().finite().min(0).max(100).optional(),
|
|
152
|
+
timeoutMs: z.number().finite().positive().optional(),
|
|
153
|
+
renderer: z
|
|
154
|
+
.union([z.literal('auto'), z.literal('puppeteer'), z.literal('html2canvas'), z.literal('html-to-image')])
|
|
155
|
+
.optional(),
|
|
156
|
+
hooks: z.array(z.unknown()).optional(),
|
|
157
|
+
})
|
|
158
|
+
.passthrough();
|
|
159
|
+
const discoverSelectorsCommandSchema = z
|
|
160
|
+
.object({
|
|
161
|
+
type: z.literal('discoverSelectors'),
|
|
162
|
+
scopeSelector: z.union([z.string(), z.null()]).optional(),
|
|
163
|
+
limit: z.number().int().positive().optional(),
|
|
164
|
+
includeHidden: z.boolean().optional(),
|
|
165
|
+
})
|
|
166
|
+
.passthrough();
|
|
167
|
+
const commandSchema = z.discriminatedUnion('type', [
|
|
168
|
+
runScriptCommandSchema,
|
|
169
|
+
getDomCommandSchema,
|
|
170
|
+
navigateCommandSchema,
|
|
171
|
+
pingCommandSchema,
|
|
172
|
+
screenshotCommandSchema,
|
|
173
|
+
discoverSelectorsCommandSchema,
|
|
174
|
+
]);
|
|
175
|
+
const timeoutOverrideSchema = z.number().finite().positive().optional();
|
|
176
|
+
const resolveDaemonPort = (value) => {
|
|
177
|
+
if (value && typeof value === 'object' && value !== null) {
|
|
178
|
+
const portCandidate = value.port;
|
|
179
|
+
if (typeof portCandidate === 'number' && Number.isFinite(portCandidate) && portCandidate > 0) {
|
|
180
|
+
return portCandidate;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return SWEETLINK_DEFAULT_PORT;
|
|
184
|
+
};
|
|
185
|
+
const isSecretResolution = (value) => {
|
|
186
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
const candidate = value;
|
|
190
|
+
if (typeof candidate.secret !== 'string' || candidate.secret.length === 0) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
if (candidate.source !== 'env' && candidate.source !== 'file' && candidate.source !== 'generated') {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
if (candidate.path !== undefined && typeof candidate.path !== 'string') {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
return true;
|
|
200
|
+
};
|
|
201
|
+
const assertSecretResolution = (value) => {
|
|
202
|
+
if (!isSecretResolution(value)) {
|
|
203
|
+
throw new TypeError('SweetLink secret resolution payload is invalid');
|
|
204
|
+
}
|
|
205
|
+
return value;
|
|
206
|
+
};
|
|
207
|
+
const daemonPort = resolveDaemonPort(readSweetLinkEnv());
|
|
208
|
+
async function main() {
|
|
209
|
+
try {
|
|
210
|
+
if (await isDaemonAlreadyRunning(daemonPort)) {
|
|
211
|
+
log(`SweetLink daemon already running on https://localhost:${daemonPort}; exiting.`);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const rawSecretResolution = await resolveSweetLinkSecret({ autoCreate: true });
|
|
215
|
+
const { secret, source, path: secretPath } = assertSecretResolution(rawSecretResolution);
|
|
216
|
+
log(`SweetLink secret source: ${source}${secretPath ? ` (${secretPath})` : ''}`);
|
|
217
|
+
ensureCertificates();
|
|
218
|
+
const { cert, key } = loadCertificates();
|
|
219
|
+
const state = new SweetLinkState(secret);
|
|
220
|
+
const server = createHttpsServer({ key, cert }, (req, res) => {
|
|
221
|
+
handleHttpRequest(state, req, res).catch((error) => {
|
|
222
|
+
console.warn('SweetLink daemon request handler failed:', getErrorMessage(error));
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
const wsServer = new WebSocketServer({ server, path: SWEETLINK_WS_PATH });
|
|
226
|
+
wsServer.on('connection', (socket) => state.handleSocket(socket));
|
|
227
|
+
server.listen(daemonPort, '127.0.0.1', () => {
|
|
228
|
+
log(`SweetLink daemon listening on https://localhost:${daemonPort}`);
|
|
229
|
+
log(`WebSocket endpoint ready at wss://localhost:${daemonPort}${SWEETLINK_WS_PATH}`);
|
|
230
|
+
log('Press Ctrl+C to stop.');
|
|
231
|
+
});
|
|
232
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
233
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
234
|
+
function shutdown(signal) {
|
|
235
|
+
log(`Received ${signal}, shutting down SweetLink daemon...`);
|
|
236
|
+
wsServer.close();
|
|
237
|
+
server.close(() => process.exit(0));
|
|
238
|
+
const shutdownTimer = setTimeout(() => process.exit(0), SHUTDOWN_GRACE_MS);
|
|
239
|
+
unrefTimer(shutdownTimer);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
const message = getErrorMessage(error);
|
|
244
|
+
console.error(`SweetLink daemon failed to start: ${message}`);
|
|
245
|
+
process.exitCode = 1;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
async function isDaemonAlreadyRunning(port) {
|
|
249
|
+
return await new Promise((resolve) => {
|
|
250
|
+
const request = httpsRequest({
|
|
251
|
+
hostname: '127.0.0.1',
|
|
252
|
+
port,
|
|
253
|
+
path: '/healthz',
|
|
254
|
+
method: 'GET',
|
|
255
|
+
rejectUnauthorized: false,
|
|
256
|
+
timeout: 750,
|
|
257
|
+
}, (response) => {
|
|
258
|
+
response.resume();
|
|
259
|
+
resolve(response.statusCode === 200);
|
|
260
|
+
});
|
|
261
|
+
request.on('error', () => resolve(false));
|
|
262
|
+
request.on('timeout', () => {
|
|
263
|
+
request.destroy();
|
|
264
|
+
resolve(false);
|
|
265
|
+
});
|
|
266
|
+
request.end();
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
class SweetLinkState {
|
|
270
|
+
#secret;
|
|
271
|
+
#sessions = new Map();
|
|
272
|
+
constructor(secret) {
|
|
273
|
+
this.#secret = secret;
|
|
274
|
+
const expiryInterval = setInterval(() => this.#expireStaleSessions(), SWEETLINK_HEARTBEAT_INTERVAL_MS);
|
|
275
|
+
unrefTimer(expiryInterval);
|
|
276
|
+
}
|
|
277
|
+
verifyCliToken(token) {
|
|
278
|
+
return verifySweetLinkToken({ secret: this.#secret, token, expectedScope: 'cli' });
|
|
279
|
+
}
|
|
280
|
+
handleSocket(socket) {
|
|
281
|
+
let sessionId = null;
|
|
282
|
+
socket.on('message', (data) => {
|
|
283
|
+
try {
|
|
284
|
+
const raw = decodeSocketPayload(data);
|
|
285
|
+
const parsedMessage = parseClientMessage(JSON.parse(raw));
|
|
286
|
+
const message = parsedMessage;
|
|
287
|
+
switch (message.kind) {
|
|
288
|
+
case 'register': {
|
|
289
|
+
sessionId = this.#handleRegister(socket, message);
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
case 'heartbeat': {
|
|
293
|
+
this.#touchSession(message.sessionId);
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
case 'commandResult': {
|
|
297
|
+
this.#handleCommandResult(message.sessionId, message.result);
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
case 'console': {
|
|
301
|
+
this.#handleConsoleEvents(message.sessionId, message.events);
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
default: {
|
|
305
|
+
const exhaustiveCheck = message;
|
|
306
|
+
// biome-ignore lint/suspicious/noExplicitAny: exhaustiveness guard for impossible state
|
|
307
|
+
const kind = exhaustiveCheck?.kind ?? String(exhaustiveCheck);
|
|
308
|
+
throw new Error(`Unhandled client message: ${kind}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
const errorMessage = getErrorMessage(error);
|
|
314
|
+
console.warn(`SweetLink socket error: ${errorMessage}`);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
socket.once('close', (code, reasonBuffer) => {
|
|
318
|
+
if (sessionId) {
|
|
319
|
+
const reasonText = reasonBuffer?.toString?.('utf8') ?? '';
|
|
320
|
+
const closeDetail = reasonText && reasonText.length > 0 ? `socket closed (${code}: ${reasonText})` : `socket closed (${code})`;
|
|
321
|
+
this.#removeSession(sessionId, closeDetail);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
listSessions() {
|
|
326
|
+
const now = Date.now();
|
|
327
|
+
return [...this.#sessions.values()].map((entry) => {
|
|
328
|
+
const consoleEventsBuffered = entry.consoleBuffer.length;
|
|
329
|
+
let consoleErrorsBuffered = 0;
|
|
330
|
+
for (const event of entry.consoleBuffer) {
|
|
331
|
+
if (event.level === 'error') {
|
|
332
|
+
consoleErrorsBuffered += 1;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const lastConsoleEventAt = entry.lastConsoleEventAt ?? null;
|
|
336
|
+
return {
|
|
337
|
+
sessionId: entry.metadata.sessionId,
|
|
338
|
+
codename: entry.metadata.codename,
|
|
339
|
+
url: entry.metadata.url,
|
|
340
|
+
title: entry.metadata.title,
|
|
341
|
+
topOrigin: entry.metadata.topOrigin,
|
|
342
|
+
createdAt: entry.metadata.createdAt,
|
|
343
|
+
lastSeenAt: entry.lastHeartbeat,
|
|
344
|
+
heartbeatMsAgo: Math.max(0, now - entry.lastHeartbeat),
|
|
345
|
+
consoleEventsBuffered,
|
|
346
|
+
consoleErrorsBuffered,
|
|
347
|
+
pendingCommandCount: entry.pending.size,
|
|
348
|
+
socketState: this.#socketStateToString(entry.socket.readyState),
|
|
349
|
+
userAgent: entry.metadata.userAgent,
|
|
350
|
+
lastConsoleEventAt,
|
|
351
|
+
};
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
sendCommand(sessionId, rawCommand, timeoutMs = 15_000) {
|
|
355
|
+
const session = this.#sessions.get(sessionId);
|
|
356
|
+
if (!session) {
|
|
357
|
+
throw new Error('Session not found or offline');
|
|
358
|
+
}
|
|
359
|
+
if (session.socket.readyState !== WebSocket.OPEN) {
|
|
360
|
+
throw new Error('Session socket is not open');
|
|
361
|
+
}
|
|
362
|
+
const commandId = createSweetLinkCommandId();
|
|
363
|
+
const command = { ...rawCommand, id: commandId };
|
|
364
|
+
const payload = {
|
|
365
|
+
kind: 'command',
|
|
366
|
+
sessionId: session.metadata.sessionId,
|
|
367
|
+
command,
|
|
368
|
+
};
|
|
369
|
+
const serialized = JSON.stringify(payload);
|
|
370
|
+
return new Promise((resolve, reject) => {
|
|
371
|
+
const timeout = setTimeout(() => {
|
|
372
|
+
session.pending.delete(commandId);
|
|
373
|
+
reject(new Error('Command timed out'));
|
|
374
|
+
}, timeoutMs);
|
|
375
|
+
session.pending.set(commandId, {
|
|
376
|
+
commandId,
|
|
377
|
+
resolve: (result) => {
|
|
378
|
+
clearTimeout(timeout);
|
|
379
|
+
resolve(result);
|
|
380
|
+
},
|
|
381
|
+
reject: (error) => {
|
|
382
|
+
clearTimeout(timeout);
|
|
383
|
+
reject(error);
|
|
384
|
+
},
|
|
385
|
+
timeout,
|
|
386
|
+
});
|
|
387
|
+
session.socket.send(serialized, (sendError) => {
|
|
388
|
+
if (sendError) {
|
|
389
|
+
clearTimeout(timeout);
|
|
390
|
+
session.pending.delete(commandId);
|
|
391
|
+
reject(sendError);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
getSessionConsole(sessionId) {
|
|
397
|
+
const session = this.#sessions.get(sessionId);
|
|
398
|
+
return session ? [...session.consoleBuffer] : [];
|
|
399
|
+
}
|
|
400
|
+
#handleRegister(socket, message) {
|
|
401
|
+
const token = message.token;
|
|
402
|
+
const sessionId = message.sessionId;
|
|
403
|
+
const payload = verifySweetLinkToken({ secret: this.#secret, token, expectedScope: 'session' });
|
|
404
|
+
if (!payload.sessionId || payload.sessionId !== sessionId) {
|
|
405
|
+
throw new Error('Session token mismatch');
|
|
406
|
+
}
|
|
407
|
+
const metadata = {
|
|
408
|
+
sessionId,
|
|
409
|
+
userAgent: message.userAgent,
|
|
410
|
+
url: message.url,
|
|
411
|
+
title: message.title,
|
|
412
|
+
topOrigin: message.topOrigin,
|
|
413
|
+
codename: generateSessionCodename(Array.from(this.#sessions.values(), (session) => session.metadata.codename)),
|
|
414
|
+
createdAt: Date.now(),
|
|
415
|
+
};
|
|
416
|
+
const existing = this.#sessions.get(sessionId);
|
|
417
|
+
if (existing) {
|
|
418
|
+
existing.socket.terminate();
|
|
419
|
+
this.#sessions.delete(sessionId);
|
|
420
|
+
}
|
|
421
|
+
const entry = {
|
|
422
|
+
metadata,
|
|
423
|
+
socket,
|
|
424
|
+
lastHeartbeat: Date.now(),
|
|
425
|
+
consoleBuffer: [],
|
|
426
|
+
pending: new Map(),
|
|
427
|
+
lastConsoleEventAt: null,
|
|
428
|
+
};
|
|
429
|
+
this.#sessions.set(sessionId, entry);
|
|
430
|
+
try {
|
|
431
|
+
const metadataMessage = {
|
|
432
|
+
kind: 'metadata',
|
|
433
|
+
sessionId,
|
|
434
|
+
codename: metadata.codename,
|
|
435
|
+
};
|
|
436
|
+
socket.send(JSON.stringify(metadataMessage));
|
|
437
|
+
log(`Sent metadata for session ${sessionId} [${metadata.codename}]`);
|
|
438
|
+
}
|
|
439
|
+
catch (error) {
|
|
440
|
+
const errorMessage = getErrorMessage(error);
|
|
441
|
+
console.warn(`[SweetLink] Failed to send session metadata for ${sessionId}: ${errorMessage}`);
|
|
442
|
+
}
|
|
443
|
+
log(`Registered SweetLink session ${sessionId} [${metadata.codename}] (${metadata.title || metadata.url})`);
|
|
444
|
+
return sessionId;
|
|
445
|
+
}
|
|
446
|
+
#touchSession(sessionId) {
|
|
447
|
+
const session = this.#sessions.get(sessionId);
|
|
448
|
+
if (session) {
|
|
449
|
+
session.lastHeartbeat = Date.now();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
#handleCommandResult(sessionId, result) {
|
|
453
|
+
const session = this.#sessions.get(sessionId);
|
|
454
|
+
if (!session) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const pending = session.pending.get(result.commandId);
|
|
458
|
+
if (!pending) {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
session.pending.delete(result.commandId);
|
|
462
|
+
pending.resolve(result);
|
|
463
|
+
}
|
|
464
|
+
#handleConsoleEvents(sessionId, events) {
|
|
465
|
+
const session = this.#sessions.get(sessionId);
|
|
466
|
+
if (!(session && events?.length)) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
session.consoleBuffer.push(...events);
|
|
470
|
+
if (session.consoleBuffer.length > 200) {
|
|
471
|
+
session.consoleBuffer.splice(0, session.consoleBuffer.length - 200);
|
|
472
|
+
}
|
|
473
|
+
const lastEvent = events.at(-1);
|
|
474
|
+
if (lastEvent) {
|
|
475
|
+
session.lastConsoleEventAt = lastEvent.timestamp ?? Date.now();
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
#socketStateToString(readyState) {
|
|
479
|
+
return SOCKET_STATE_LABEL[readyState] ?? 'unknown';
|
|
480
|
+
}
|
|
481
|
+
#removeSession(sessionId, reason) {
|
|
482
|
+
const session = this.#sessions.get(sessionId);
|
|
483
|
+
if (!session) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
this.#sessions.delete(sessionId);
|
|
487
|
+
for (const pending of session.pending.values()) {
|
|
488
|
+
clearTimeout(pending.timeout);
|
|
489
|
+
pending.reject(new Error(`Session ended before command completed: ${reason}`));
|
|
490
|
+
}
|
|
491
|
+
log(`Session ${sessionId} [${session.metadata.codename}] disconnected (${reason})`);
|
|
492
|
+
}
|
|
493
|
+
#expireStaleSessions() {
|
|
494
|
+
const now = Date.now();
|
|
495
|
+
for (const session of this.#sessions.values()) {
|
|
496
|
+
if (now - session.lastHeartbeat > SWEETLINK_HEARTBEAT_TOLERANCE_MS) {
|
|
497
|
+
session.socket.terminate();
|
|
498
|
+
this.#sessions.delete(session.metadata.sessionId);
|
|
499
|
+
log(`Session ${session.metadata.sessionId} [${session.metadata.codename}] expired due to missed heartbeats`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
async function handleHttpRequest(state, req, res) {
|
|
505
|
+
const basePort = daemonPort;
|
|
506
|
+
const requestUrl = req.url ? new URL(req.url, `https://localhost:${basePort}`) : null;
|
|
507
|
+
if (!requestUrl) {
|
|
508
|
+
respondJson(res, 400, { error: 'Invalid request URL' });
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
if (req.method === 'OPTIONS') {
|
|
512
|
+
respondCors(res);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (requestUrl.pathname === '/healthz') {
|
|
516
|
+
respondJson(res, 200, { status: 'ok' });
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
const authorization = req.headers.authorization;
|
|
520
|
+
if (!authorization?.startsWith('Bearer ')) {
|
|
521
|
+
respondJson(res, 401, { error: 'Missing SweetLink token' });
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
try {
|
|
525
|
+
const token = authorization.slice('Bearer '.length).trim();
|
|
526
|
+
state.verifyCliToken(token);
|
|
527
|
+
}
|
|
528
|
+
catch (error) {
|
|
529
|
+
const message = getErrorMessage(error);
|
|
530
|
+
respondJson(res, 401, { error: message });
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (req.method === 'GET' && requestUrl.pathname === '/sessions') {
|
|
534
|
+
const sessions = state.listSessions();
|
|
535
|
+
respondJson(res, 200, { sessions });
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (req.method === 'GET' &&
|
|
539
|
+
requestUrl.pathname.startsWith('/sessions/') &&
|
|
540
|
+
requestUrl.pathname.endsWith('/console')) {
|
|
541
|
+
const sessionId = requestUrl.pathname.split('/')[2];
|
|
542
|
+
if (sessionId == null) {
|
|
543
|
+
respondJson(res, 400, { error: 'Session id missing' });
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
const events = state.getSessionConsole(sessionId);
|
|
547
|
+
respondJson(res, 200, { sessionId, events });
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (req.method === 'POST' &&
|
|
551
|
+
requestUrl.pathname.startsWith('/sessions/') &&
|
|
552
|
+
requestUrl.pathname.endsWith('/command')) {
|
|
553
|
+
const sessionId = requestUrl.pathname.split('/')[2];
|
|
554
|
+
if (sessionId == null) {
|
|
555
|
+
respondJson(res, 400, { error: 'Session id missing' });
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
try {
|
|
559
|
+
const rawBody = await readJson(req);
|
|
560
|
+
const { command, timeoutMs } = parseCommandRequest(rawBody);
|
|
561
|
+
const result = await state.sendCommand(sessionId, command, timeoutMs ?? 15_000);
|
|
562
|
+
respondJson(res, 200, { result });
|
|
563
|
+
}
|
|
564
|
+
catch (error) {
|
|
565
|
+
const message = getErrorMessage(error);
|
|
566
|
+
respondJson(res, 400, { error: message });
|
|
567
|
+
}
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
respondJson(res, 404, { error: 'Not Found' });
|
|
571
|
+
}
|
|
572
|
+
async function readJson(req) {
|
|
573
|
+
const chunks = [];
|
|
574
|
+
for await (const chunk of req) {
|
|
575
|
+
const bufferChunk = typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : chunk;
|
|
576
|
+
chunks.push(bufferChunk);
|
|
577
|
+
}
|
|
578
|
+
if (chunks.length === 0) {
|
|
579
|
+
return {};
|
|
580
|
+
}
|
|
581
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
582
|
+
if (!text) {
|
|
583
|
+
return {};
|
|
584
|
+
}
|
|
585
|
+
return JSON.parse(text);
|
|
586
|
+
}
|
|
587
|
+
function respondJson(res, status, body) {
|
|
588
|
+
const payload = JSON.stringify(body ?? {}, null, 2);
|
|
589
|
+
res.statusCode = status;
|
|
590
|
+
res.setHeader('Content-Type', 'application/json');
|
|
591
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
592
|
+
res.setHeader('Access-Control-Allow-Headers', 'authorization, content-type');
|
|
593
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
|
|
594
|
+
res.end(payload);
|
|
595
|
+
}
|
|
596
|
+
function respondCors(res) {
|
|
597
|
+
res.statusCode = 204;
|
|
598
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
599
|
+
res.setHeader('Access-Control-Allow-Headers', 'authorization, content-type');
|
|
600
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
|
|
601
|
+
res.end();
|
|
602
|
+
}
|
|
603
|
+
function decodeSocketPayload(data) {
|
|
604
|
+
if (typeof data === 'string') {
|
|
605
|
+
return data;
|
|
606
|
+
}
|
|
607
|
+
if (Buffer.isBuffer(data)) {
|
|
608
|
+
return data.toString('utf8');
|
|
609
|
+
}
|
|
610
|
+
if (Array.isArray(data)) {
|
|
611
|
+
const buffers = data.map((chunk) => (Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
|
612
|
+
return Buffer.concat(buffers).toString('utf8');
|
|
613
|
+
}
|
|
614
|
+
if (ArrayBuffer.isView(data)) {
|
|
615
|
+
const view = data;
|
|
616
|
+
return Buffer.from(view.buffer, view.byteOffset, view.byteLength).toString('utf8');
|
|
617
|
+
}
|
|
618
|
+
if (data instanceof ArrayBuffer) {
|
|
619
|
+
return Buffer.from(data).toString('utf8');
|
|
620
|
+
}
|
|
621
|
+
return Buffer.from([]).toString('utf8');
|
|
622
|
+
}
|
|
623
|
+
function parseClientMessage(raw) {
|
|
624
|
+
return clientMessageSchema.parse(raw);
|
|
625
|
+
}
|
|
626
|
+
function parseCommandRequest(raw) {
|
|
627
|
+
const candidate = ensureObject(raw);
|
|
628
|
+
const command = commandSchema.parse(candidate);
|
|
629
|
+
const timeoutMs = timeoutOverrideSchema.parse(candidate.timeoutMs);
|
|
630
|
+
return { command, timeoutMs };
|
|
631
|
+
}
|
|
632
|
+
function ensureObject(value) {
|
|
633
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
634
|
+
throw new Error('Request body must be a JSON object');
|
|
635
|
+
}
|
|
636
|
+
return value;
|
|
637
|
+
}
|
|
638
|
+
function ensureCertificates() {
|
|
639
|
+
if (existsSync(CERT_PATH) && existsSync(KEY_PATH)) {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
log('Generating SweetLink TLS certificates via mkcert...');
|
|
643
|
+
mkdirSync(CERT_DIR, { recursive: true });
|
|
644
|
+
const mkcertLookup = spawnSync('which', ['mkcert'], { stdio: 'pipe' });
|
|
645
|
+
if (mkcertLookup.status !== 0) {
|
|
646
|
+
const secretPath = getDefaultSweetLinkSecretPath();
|
|
647
|
+
throw new Error('mkcert is required but not found. Install via "brew install mkcert nss" and rerun pnpm sweetlink. ' +
|
|
648
|
+
`Generated SweetLink secret saved at ${secretPath}.`);
|
|
649
|
+
}
|
|
650
|
+
const install = spawnSync('mkcert', ['-install'], { stdio: 'inherit' });
|
|
651
|
+
if (install.status !== 0) {
|
|
652
|
+
throw new Error('Failed to run mkcert -install');
|
|
653
|
+
}
|
|
654
|
+
const create = spawnSync('mkcert', ['-cert-file', CERT_PATH, '-key-file', KEY_PATH, 'localhost', '127.0.0.1', '::1'], { stdio: 'inherit' });
|
|
655
|
+
if (create.status !== 0) {
|
|
656
|
+
throw new Error('Failed to generate mkcert certificates');
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
function loadCertificates() {
|
|
660
|
+
const cert = readFileSync(CERT_PATH);
|
|
661
|
+
const key = readFileSync(KEY_PATH);
|
|
662
|
+
return { cert, key };
|
|
663
|
+
}
|
|
664
|
+
function log(message) {
|
|
665
|
+
console.log(`[SweetLink] ${message}`);
|
|
666
|
+
}
|
|
667
|
+
try {
|
|
668
|
+
await main();
|
|
669
|
+
}
|
|
670
|
+
catch (error) {
|
|
671
|
+
const message = getErrorMessage(error);
|
|
672
|
+
console.error(`[SweetLink] Daemon failed: ${message}`);
|
|
673
|
+
process.exit(1);
|
|
674
|
+
}
|
|
675
|
+
//# sourceMappingURL=index.js.map
|