beauticode-dsh 1.0.7 → 1.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.zh-CN.md +2 -2
- package/agent.mjs +69 -52
- package/client.js +102 -22
- package/console.js +166 -46
- package/control-client.mjs +6 -4
- package/index.mjs +22 -8
- package/package.json +1 -1
- package/themes/internal-beyond/bg-canvas.png +0 -0
- package/ui-host.mjs +459 -31
- package/vendor/adapter-dsh/bridge.js +2 -4
- package/vendor/adapter-dsh/session.js +61 -27
- package/vendor/adapter-dsh/tray-handoff.js +13 -11
- package/vendor/core/apply-transaction.js +87 -45
- package/vendor/core/background-store.js +211 -351
- package/vendor/core/error-message.js +0 -8
- package/vendor/core/file-lock.js +13 -34
- package/vendor/core/index.js +1 -2
- package/vendor/core/media-server.js +22 -40
- package/vendor/core/media-source.js +36 -0
- package/vendor/core/media-validation.js +26 -19
- package/vendor/core/paths.js +0 -1
- package/vendor/core/process-liveness.js +0 -205
- package/vendor/core/skin-pack.js +0 -64
|
@@ -126,16 +126,8 @@ export function toChineseErrorMessage(value) {
|
|
|
126
126
|
[/No active background to save\.?/i, "当前没有可保存的背景。"],
|
|
127
127
|
[/Saved theme limit reached \((\d+)\)\. Delete one before saving\.?/i, (match) => `已达到保存主题数量上限(${match[1]}),请先删除一个主题。`],
|
|
128
128
|
[/Saved theme storage limit exceeded \((\d+) bytes\)\. Delete a theme before saving\.?/i, (match) => `已超过保存主题的存储上限(${match[1]} 字节),请先删除一个主题。`],
|
|
129
|
-
[/Import requires an image or video file\.?/i, "导入需要图片或视频文件。"],
|
|
130
|
-
[/Imported media must be an image or video\.?/i, "导入的媒体必须是图片或视频。"],
|
|
131
129
|
[/Built-in theme cannot be deleted\.?/i, "内置主题不能删除。"],
|
|
132
130
|
[/Invalid saved theme id\.?/i, "已保存主题 ID 无效。"],
|
|
133
|
-
[/Invalid skin-center media path\.?/i, "皮肤中心地址无效。"],
|
|
134
|
-
[/Skin center is not configured\.?/i, "尚未配置皮肤中心地址。"],
|
|
135
|
-
[/Skin is not available for download\.?/i, "该皮肤尚未上架或已被下架。"],
|
|
136
|
-
[/Skin media download failed\.?/i, "皮肤文件下载失败。"],
|
|
137
|
-
[/Skin media download exceeded the size limit\.?/i, "皮肤文件超过大小限制。"],
|
|
138
|
-
[/Skin media download host mismatch\.?/i, "皮肤下载地址与皮肤中心域名不一致。"],
|
|
139
131
|
[/Saved theme not found\.?/i, "未找到已保存的主题。"],
|
|
140
132
|
[/Invalid video position\.?/i, "视频位置无效。"],
|
|
141
133
|
[/Theme meta unreadable\.?/i, "主题元数据无法读取。"],
|
package/vendor/core/file-lock.js
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
-
import fsSync from "node:fs";
|
|
3
2
|
import fs from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
|
-
|
|
4
|
+
function isPidAlive(pid) {
|
|
5
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
6
|
+
return false;
|
|
7
|
+
try {
|
|
8
|
+
process.kill(pid, 0);
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
6
15
|
function parseOwner(raw) {
|
|
7
16
|
try {
|
|
8
17
|
const value = JSON.parse(raw);
|
|
@@ -65,14 +74,12 @@ export async function acquireFileLock(filePath, opts = {}) {
|
|
|
65
74
|
throw error;
|
|
66
75
|
}
|
|
67
76
|
let released = false;
|
|
68
|
-
const detachExit = attachExitRelease(resolved, owner.nonce, handle.fd);
|
|
69
77
|
return {
|
|
70
78
|
owner,
|
|
71
79
|
async release() {
|
|
72
80
|
if (released)
|
|
73
81
|
return;
|
|
74
82
|
released = true;
|
|
75
|
-
detachExit();
|
|
76
83
|
try {
|
|
77
84
|
const raw = await fs.readFile(resolved, "utf8");
|
|
78
85
|
const current = parseOwner(raw);
|
|
@@ -119,15 +126,10 @@ export async function acquireFileLock(filePath, opts = {}) {
|
|
|
119
126
|
}
|
|
120
127
|
const existing = parseOwner(raw);
|
|
121
128
|
const existingPid = parseOwnerPid(raw);
|
|
122
|
-
|
|
123
|
-
if (existingPid != null &&
|
|
124
|
-
(await isRecordedPidLive(existingPid, recordedStartedAt, {
|
|
125
|
-
fresh: true,
|
|
126
|
-
...(opts.inspectPid ? { inspect: opts.inspectPid } : {}),
|
|
127
|
-
}))) {
|
|
129
|
+
if (existingPid != null && isPidAlive(existingPid)) {
|
|
128
130
|
throw new Error(`Another ${opts.purpose ?? "operation"} is running (pid ${existingPid}).`);
|
|
129
131
|
}
|
|
130
|
-
if (!existing &&
|
|
132
|
+
if (!existing && age <= staleMs) {
|
|
131
133
|
throw new Error(`Another ${opts.purpose ?? "operation"} may be starting; lock owner is not readable yet.`);
|
|
132
134
|
}
|
|
133
135
|
const quarantine = `${resolved}.stale-${process.pid}-${crypto.randomUUID()}`;
|
|
@@ -146,27 +148,4 @@ export async function acquireFileLock(filePath, opts = {}) {
|
|
|
146
148
|
}
|
|
147
149
|
throw new Error(`Could not acquire ${opts.purpose ?? "file"} lock.`);
|
|
148
150
|
}
|
|
149
|
-
function attachExitRelease(filePath, nonce, fd) {
|
|
150
|
-
const onExit = () => {
|
|
151
|
-
try {
|
|
152
|
-
fsSync.closeSync(fd);
|
|
153
|
-
}
|
|
154
|
-
catch {
|
|
155
|
-
/* already closed */
|
|
156
|
-
}
|
|
157
|
-
try {
|
|
158
|
-
const raw = fsSync.readFileSync(filePath, "utf8");
|
|
159
|
-
if (parseOwner(raw)?.nonce === nonce) {
|
|
160
|
-
fsSync.rmSync(filePath, { force: true });
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
catch {
|
|
164
|
-
/* process is dying; next acquirer will treat a leftover as stale */
|
|
165
|
-
}
|
|
166
|
-
};
|
|
167
|
-
process.on("exit", onExit);
|
|
168
|
-
return () => {
|
|
169
|
-
process.off("exit", onExit);
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
151
|
//# sourceMappingURL=file-lock.js.map
|
package/vendor/core/index.js
CHANGED
|
@@ -3,12 +3,11 @@ export * from "./types.js";
|
|
|
3
3
|
export * from "./host-session.js";
|
|
4
4
|
export * from "./media-validation.js";
|
|
5
5
|
export * from "./media-server.js";
|
|
6
|
+
export * from "./media-source.js";
|
|
6
7
|
export { defaultDataRoot, resolveDataPaths, isPathInsideRoot, } from "./paths.js";
|
|
7
8
|
export * from "./file-lock.js";
|
|
8
|
-
export * from "./process-liveness.js";
|
|
9
9
|
export * from "./error-message.js";
|
|
10
10
|
export * from "./background-store.js";
|
|
11
11
|
export * from "./bundled-gallery.js";
|
|
12
12
|
export * from "./apply-transaction.js";
|
|
13
|
-
export * from "./skin-pack.js";
|
|
14
13
|
//# sourceMappingURL=index.js.map
|
|
@@ -36,27 +36,6 @@ function parseRange(value, size) {
|
|
|
36
36
|
function makeToken() {
|
|
37
37
|
return randomUUID().replaceAll("-", "");
|
|
38
38
|
}
|
|
39
|
-
function pipeOpenedFile(handle, response, range) {
|
|
40
|
-
const stream = handle.createReadStream({
|
|
41
|
-
start: range.start,
|
|
42
|
-
...(range.end != null ? { end: range.end } : {}),
|
|
43
|
-
autoClose: false,
|
|
44
|
-
});
|
|
45
|
-
let closed = false;
|
|
46
|
-
const closeHandle = () => {
|
|
47
|
-
if (closed)
|
|
48
|
-
return;
|
|
49
|
-
closed = true;
|
|
50
|
-
void handle.close().catch(() => { });
|
|
51
|
-
};
|
|
52
|
-
stream.on("error", () => {
|
|
53
|
-
closeHandle();
|
|
54
|
-
response.destroy();
|
|
55
|
-
});
|
|
56
|
-
stream.on("close", closeHandle);
|
|
57
|
-
response.on("close", closeHandle);
|
|
58
|
-
stream.pipe(response);
|
|
59
|
-
}
|
|
60
39
|
async function hashOpenedFile(handle, size) {
|
|
61
40
|
const hash = createHash("sha256");
|
|
62
41
|
const chunk = Buffer.allocUnsafe(1024 * 1024);
|
|
@@ -71,24 +50,12 @@ async function hashOpenedFile(handle, size) {
|
|
|
71
50
|
}
|
|
72
51
|
return hash.digest("hex");
|
|
73
52
|
}
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
const url = new URL(origin);
|
|
77
|
-
return (url.protocol === "http:" &&
|
|
78
|
-
["127.0.0.1", "localhost", "[::1]"].includes(url.hostname.toLowerCase()));
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
/** Exact match, loopback HTTP, or app://* prefix (hub is loopback-only). */
|
|
53
|
+
/** Exact match or app://* prefix (hub is loopback-only). */
|
|
85
54
|
function matchTrustedOrigin(origin, trusted) {
|
|
86
55
|
if (!origin)
|
|
87
56
|
return null;
|
|
88
57
|
if (trusted.has(origin))
|
|
89
58
|
return origin;
|
|
90
|
-
if (isLoopbackHttpOrigin(origin))
|
|
91
|
-
return origin;
|
|
92
59
|
for (const prefix of TRUSTED_ORIGIN_PREFIXES) {
|
|
93
60
|
if (origin.startsWith(prefix))
|
|
94
61
|
return origin;
|
|
@@ -160,7 +127,8 @@ export class LoopbackMediaHub {
|
|
|
160
127
|
this.listenPromise = null;
|
|
161
128
|
}
|
|
162
129
|
}
|
|
163
|
-
async addFile(filePath) {
|
|
130
|
+
async addFile(filePath, opts = {}) {
|
|
131
|
+
const validation = opts.validation ?? "full";
|
|
164
132
|
const ext = path.extname(filePath).toLowerCase();
|
|
165
133
|
let kind;
|
|
166
134
|
let mime;
|
|
@@ -174,6 +142,7 @@ export class LoopbackMediaHub {
|
|
|
174
142
|
if (IMAGE_EXTENSIONS.includes(ext)) {
|
|
175
143
|
const v = await validateImageFile(filePath, {
|
|
176
144
|
maxBytes: this.maxImageBytes,
|
|
145
|
+
mode: validation,
|
|
177
146
|
});
|
|
178
147
|
kind = "image";
|
|
179
148
|
mime = v.mime;
|
|
@@ -188,6 +157,7 @@ export class LoopbackMediaHub {
|
|
|
188
157
|
else if (ext === VIDEO_EXTENSION) {
|
|
189
158
|
const v = await validateVideoFile(filePath, {
|
|
190
159
|
maxBytes: this.maxVideoBytes,
|
|
160
|
+
mode: validation,
|
|
191
161
|
});
|
|
192
162
|
kind = "video";
|
|
193
163
|
mime = VIDEO_MIME;
|
|
@@ -225,6 +195,7 @@ export class LoopbackMediaHub {
|
|
|
225
195
|
mtimeMs,
|
|
226
196
|
ctimeMs,
|
|
227
197
|
mime,
|
|
198
|
+
validation,
|
|
228
199
|
token,
|
|
229
200
|
route,
|
|
230
201
|
};
|
|
@@ -254,6 +225,7 @@ export class LoopbackMediaHub {
|
|
|
254
225
|
mtimeMs: rec.mtimeMs,
|
|
255
226
|
ctimeMs: rec.ctimeMs,
|
|
256
227
|
mime: rec.mime,
|
|
228
|
+
validation: rec.validation,
|
|
257
229
|
token: rec.token,
|
|
258
230
|
route: rec.route,
|
|
259
231
|
url,
|
|
@@ -349,7 +321,7 @@ export class LoopbackMediaHub {
|
|
|
349
321
|
queryToken = "";
|
|
350
322
|
}
|
|
351
323
|
const tokenOk = headerToken === pathToken || queryToken === pathToken;
|
|
352
|
-
if ((origin && !trustedOrigin
|
|
324
|
+
if ((origin && !trustedOrigin) || !tokenOk) {
|
|
353
325
|
response.writeHead(403, { "Cache-Control": "no-store" });
|
|
354
326
|
response.end();
|
|
355
327
|
return;
|
|
@@ -379,6 +351,9 @@ export class LoopbackMediaHub {
|
|
|
379
351
|
throw new Error("Media file changed or exceeded the safety limit");
|
|
380
352
|
}
|
|
381
353
|
if (fingerprintChanged) {
|
|
354
|
+
if (asset.validation === "fast") {
|
|
355
|
+
throw new Error("Media file changed after staging");
|
|
356
|
+
}
|
|
382
357
|
const currentHash = await hashOpenedFile(opened, stat.size);
|
|
383
358
|
if (currentHash !== asset.identity) {
|
|
384
359
|
throw new Error("Media file content changed after staging");
|
|
@@ -422,8 +397,11 @@ export class LoopbackMediaHub {
|
|
|
422
397
|
opened = null;
|
|
423
398
|
return;
|
|
424
399
|
}
|
|
425
|
-
|
|
400
|
+
const stream = opened.createReadStream({ start: 0, autoClose: true });
|
|
426
401
|
opened = null;
|
|
402
|
+
stream
|
|
403
|
+
.on("error", () => response.destroy())
|
|
404
|
+
.pipe(response);
|
|
427
405
|
return;
|
|
428
406
|
}
|
|
429
407
|
headers["Content-Range"] =
|
|
@@ -436,11 +414,15 @@ export class LoopbackMediaHub {
|
|
|
436
414
|
opened = null;
|
|
437
415
|
return;
|
|
438
416
|
}
|
|
439
|
-
|
|
417
|
+
const stream = opened.createReadStream({
|
|
440
418
|
start: range.start,
|
|
441
419
|
end: range.end,
|
|
420
|
+
autoClose: true,
|
|
442
421
|
});
|
|
443
422
|
opened = null;
|
|
423
|
+
stream
|
|
424
|
+
.on("error", () => response.destroy())
|
|
425
|
+
.pipe(response);
|
|
444
426
|
}
|
|
445
427
|
catch {
|
|
446
428
|
await opened?.close().catch(() => { });
|
|
@@ -505,10 +487,10 @@ export class MediaServerController {
|
|
|
505
487
|
get activeVideo() {
|
|
506
488
|
return this.#video;
|
|
507
489
|
}
|
|
508
|
-
async stage(filePath) {
|
|
490
|
+
async stage(filePath, opts = {}) {
|
|
509
491
|
if (!filePath || !this.#enabled)
|
|
510
492
|
return null;
|
|
511
|
-
return this.#hub.addFile(filePath);
|
|
493
|
+
return this.#hub.addFile(filePath, opts);
|
|
512
494
|
}
|
|
513
495
|
/**
|
|
514
496
|
* Promote staged image/video handles; close assets that are no longer needed.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
/** Resolve a manifest source against the directory that owns managed files. */
|
|
3
|
+
export function resolveMediaSource(source, ownerDir) {
|
|
4
|
+
if (source.kind === "local")
|
|
5
|
+
return path.resolve(source.path);
|
|
6
|
+
return path.join(ownerDir, source.file);
|
|
7
|
+
}
|
|
8
|
+
/** Resolve the primary image source, including legacy v1 manifests. */
|
|
9
|
+
export function resolveBackgroundImagePath(ownerDir, background) {
|
|
10
|
+
if (background.type === "image" && background.source) {
|
|
11
|
+
return resolveMediaSource(background.source, ownerDir);
|
|
12
|
+
}
|
|
13
|
+
return background.image ? path.join(ownerDir, background.image) : null;
|
|
14
|
+
}
|
|
15
|
+
/** Resolve the primary video source, including legacy v1 manifests. */
|
|
16
|
+
export function resolveBackgroundVideoPath(ownerDir, background) {
|
|
17
|
+
if (background.type !== "video")
|
|
18
|
+
return null;
|
|
19
|
+
if (background.source)
|
|
20
|
+
return resolveMediaSource(background.source, ownerDir);
|
|
21
|
+
return background.video ? path.join(ownerDir, background.video) : null;
|
|
22
|
+
}
|
|
23
|
+
export function isLocalBackgroundSource(background) {
|
|
24
|
+
return background.source?.kind === "local";
|
|
25
|
+
}
|
|
26
|
+
/** Return the managed primary filename when a snapshot/theme must copy it. */
|
|
27
|
+
export function managedPrimaryFile(background) {
|
|
28
|
+
if (background.source?.kind === "managed")
|
|
29
|
+
return background.source.file;
|
|
30
|
+
if (background.source?.kind === "local")
|
|
31
|
+
return null;
|
|
32
|
+
return background.type === "image"
|
|
33
|
+
? background.image ?? null
|
|
34
|
+
: background.video ?? null;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=media-source.js.map
|
|
@@ -88,7 +88,7 @@ async function assertRegularFile(filePath) {
|
|
|
88
88
|
}
|
|
89
89
|
return { resolved: realPath, size: stat.size };
|
|
90
90
|
}
|
|
91
|
-
async function inspectAndHash(filePath, expectedSize) {
|
|
91
|
+
async function inspectAndHash(filePath, expectedSize, mode = "full") {
|
|
92
92
|
const handle = await fs.open(filePath, "r");
|
|
93
93
|
try {
|
|
94
94
|
const before = await handle.stat();
|
|
@@ -97,23 +97,30 @@ async function inspectAndHash(filePath, expectedSize) {
|
|
|
97
97
|
}
|
|
98
98
|
const head = Buffer.alloc(Math.min(64, expectedSize));
|
|
99
99
|
const hash = createHash("sha256");
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
100
|
+
const { bytesRead } = await handle.read(head, 0, head.byteLength, 0);
|
|
101
|
+
if (bytesRead !== head.byteLength) {
|
|
102
|
+
throw new MediaValidationError("Media file ended during validation.");
|
|
103
|
+
}
|
|
104
|
+
if (mode === "fast") {
|
|
105
|
+
// Local-reference imports must not hash a multi-GB file. The file
|
|
106
|
+
// fingerprint is deliberately tied to metadata and the validated head;
|
|
107
|
+
// the media server rejects the asset if metadata changes afterwards.
|
|
108
|
+
hash.update(`${before.dev}:${before.ino}:${before.size}:${before.mtimeMs}:${before.ctimeMs}`);
|
|
109
|
+
hash.update(head);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
hash.update(head);
|
|
113
|
+
const chunk = Buffer.allocUnsafe(1024 * 1024);
|
|
114
|
+
let position = head.byteLength;
|
|
115
|
+
while (position < expectedSize) {
|
|
116
|
+
const length = Math.min(chunk.byteLength, expectedSize - position);
|
|
117
|
+
const read = await handle.read(chunk, 0, length, position);
|
|
118
|
+
if (read.bytesRead <= 0) {
|
|
119
|
+
throw new MediaValidationError("Media file ended during validation.");
|
|
120
|
+
}
|
|
121
|
+
hash.update(chunk.subarray(0, read.bytesRead));
|
|
122
|
+
position += read.bytesRead;
|
|
115
123
|
}
|
|
116
|
-
position += bytesRead;
|
|
117
124
|
}
|
|
118
125
|
const after = await handle.stat();
|
|
119
126
|
if (after.size !== before.size ||
|
|
@@ -172,7 +179,7 @@ export async function validateImageFile(filePath, opts = {}) {
|
|
|
172
179
|
if (size < 1 || size > maxBytes) {
|
|
173
180
|
throw new MediaValidationError(`Image must be a non-empty file no larger than ${maxBytes} bytes.`);
|
|
174
181
|
}
|
|
175
|
-
const inspected = await inspectAndHash(resolved, size);
|
|
182
|
+
const inspected = await inspectAndHash(resolved, size, opts.mode);
|
|
176
183
|
const detected = detectImageMime(inspected.head, ext, size);
|
|
177
184
|
if (!detected) {
|
|
178
185
|
throw new MediaValidationError("Image content does not match a supported JPEG/PNG/WEBP/AVIF signature.");
|
|
@@ -200,7 +207,7 @@ export async function validateVideoFile(filePath, opts = {}) {
|
|
|
200
207
|
if (size < 1 || size > maxBytes) {
|
|
201
208
|
throw new MediaValidationError(`Video background must be a non-empty MP4 no larger than ${maxBytes} bytes.`);
|
|
202
209
|
}
|
|
203
|
-
const inspected = await inspectAndHash(resolved, size);
|
|
210
|
+
const inspected = await inspectAndHash(resolved, size, opts.mode);
|
|
204
211
|
if (!isMp4Container(inspected.head, size)) {
|
|
205
212
|
throw new MediaValidationError("Video background is not a valid MP4 container.");
|
|
206
213
|
}
|
package/vendor/core/paths.js
CHANGED
|
@@ -68,7 +68,6 @@ export async function ensureDataLayout(paths) {
|
|
|
68
68
|
entry === SNAPSHOTS_DIR_NAME ||
|
|
69
69
|
entry === SAVED_DIR_NAME ||
|
|
70
70
|
entry === RUNTIME_MEDIA_DIR_NAME ||
|
|
71
|
-
entry === "tmp" ||
|
|
72
71
|
entry === "logs" ||
|
|
73
72
|
entry === "engine-launcher.log" ||
|
|
74
73
|
entry === "injector.lock" ||
|
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
|
-
import fs from "node:fs/promises";
|
|
3
|
-
import { promisify } from "node:util";
|
|
4
|
-
const execFileAsync = promisify(execFile);
|
|
5
|
-
export const PID_REUSE_GRACE_MS = 2_000;
|
|
6
|
-
const INSPECT_CACHE_MS = 3_000;
|
|
7
|
-
const WINDOWS_INSPECT_TIMEOUT_MS = 3_000;
|
|
8
|
-
/** Windows hosts that recycle PIDs and can never own a beautiCode injector. */
|
|
9
|
-
const NON_INJECTOR_PROCESS_NAMES = new Set([
|
|
10
|
-
"explorer",
|
|
11
|
-
"svchost",
|
|
12
|
-
"dwm",
|
|
13
|
-
"csrss",
|
|
14
|
-
"services",
|
|
15
|
-
"lsass",
|
|
16
|
-
"winlogon",
|
|
17
|
-
"system",
|
|
18
|
-
"idle",
|
|
19
|
-
"runtimebroker",
|
|
20
|
-
"searchhost",
|
|
21
|
-
"startmenuexperiencehost",
|
|
22
|
-
"shellexperiencehost",
|
|
23
|
-
"dllhost",
|
|
24
|
-
"conhost",
|
|
25
|
-
"taskhostw",
|
|
26
|
-
"sihost",
|
|
27
|
-
"ctfmon",
|
|
28
|
-
"securityhealthsystray",
|
|
29
|
-
"fontdrvhost",
|
|
30
|
-
"unsecapp",
|
|
31
|
-
]);
|
|
32
|
-
const inspectCache = new Map();
|
|
33
|
-
export function clearProcessInspectCache() {
|
|
34
|
-
inspectCache.clear();
|
|
35
|
-
}
|
|
36
|
-
export function isPidAlive(pid) {
|
|
37
|
-
if (!Number.isInteger(pid) || pid <= 0)
|
|
38
|
-
return false;
|
|
39
|
-
try {
|
|
40
|
-
process.kill(pid, 0);
|
|
41
|
-
return true;
|
|
42
|
-
}
|
|
43
|
-
catch (error) {
|
|
44
|
-
return Boolean(error && typeof error === "object" && "code" in error && error.code === "EPERM");
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
export function normalizeProcessName(name) {
|
|
48
|
-
if (typeof name !== "string")
|
|
49
|
-
return null;
|
|
50
|
-
const trimmed = name.trim().toLowerCase();
|
|
51
|
-
if (!trimmed)
|
|
52
|
-
return null;
|
|
53
|
-
return trimmed.endsWith(".exe") ? trimmed.slice(0, -4) : trimmed;
|
|
54
|
-
}
|
|
55
|
-
export function isDeniedInjectorName(name) {
|
|
56
|
-
const normalized = normalizeProcessName(name);
|
|
57
|
-
return normalized != null && NON_INJECTOR_PROCESS_NAMES.has(normalized);
|
|
58
|
-
}
|
|
59
|
-
export function parseRecordedStartMs(value) {
|
|
60
|
-
if (typeof value !== "string" || !value.trim())
|
|
61
|
-
return null;
|
|
62
|
-
const ms = Date.parse(value);
|
|
63
|
-
return Number.isFinite(ms) ? ms : null;
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* True when `pid` still looks like the process that wrote `recordedStartedAt`.
|
|
67
|
-
* A later start time means Windows reused the PID.
|
|
68
|
-
*/
|
|
69
|
-
export function isRecordedOwnerAlive(snapshot, recordedStartedAt) {
|
|
70
|
-
if (!snapshot.exists)
|
|
71
|
-
return false;
|
|
72
|
-
if (isDeniedInjectorName(snapshot.name))
|
|
73
|
-
return false;
|
|
74
|
-
const recordedMs = parseRecordedStartMs(recordedStartedAt);
|
|
75
|
-
if (snapshot.startMs != null && recordedMs != null) {
|
|
76
|
-
return snapshot.startMs <= recordedMs + PID_REUSE_GRACE_MS;
|
|
77
|
-
}
|
|
78
|
-
return true;
|
|
79
|
-
}
|
|
80
|
-
export async function inspectProcess(pid, opts = {}) {
|
|
81
|
-
if (!Number.isInteger(pid) || pid <= 0) {
|
|
82
|
-
return { exists: false, name: null, startMs: null };
|
|
83
|
-
}
|
|
84
|
-
if (!opts.fresh) {
|
|
85
|
-
const cached = inspectCache.get(pid);
|
|
86
|
-
if (cached && cached.expires > Date.now())
|
|
87
|
-
return cached.snap;
|
|
88
|
-
}
|
|
89
|
-
const exists = isPidAlive(pid);
|
|
90
|
-
let snap = { exists, name: null, startMs: null };
|
|
91
|
-
if (exists) {
|
|
92
|
-
try {
|
|
93
|
-
snap =
|
|
94
|
-
process.platform === "win32"
|
|
95
|
-
? await inspectWindows(pid)
|
|
96
|
-
: process.platform === "linux"
|
|
97
|
-
? await inspectLinux(pid)
|
|
98
|
-
: await inspectPosix(pid);
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
snap = { exists: true, name: null, startMs: null };
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
inspectCache.set(pid, { expires: Date.now() + INSPECT_CACHE_MS, snap });
|
|
105
|
-
return snap;
|
|
106
|
-
}
|
|
107
|
-
export async function isRecordedPidLive(pid, recordedStartedAt, opts = {}) {
|
|
108
|
-
if (!Number.isInteger(pid) || pid <= 0)
|
|
109
|
-
return false;
|
|
110
|
-
const inspect = opts.inspect ??
|
|
111
|
-
((value) => inspectProcess(value, opts.fresh === true ? { fresh: true } : {}));
|
|
112
|
-
const snapshot = await inspect(pid);
|
|
113
|
-
return isRecordedOwnerAlive(snapshot, recordedStartedAt);
|
|
114
|
-
}
|
|
115
|
-
async function inspectWindows(pid) {
|
|
116
|
-
if (!isPidAlive(pid))
|
|
117
|
-
return { exists: false, name: null, startMs: null };
|
|
118
|
-
const script = [
|
|
119
|
-
`$p = Get-Process -Id ${pid} -ErrorAction Stop`,
|
|
120
|
-
`$t = [DateTimeOffset]::new($p.StartTime).ToUnixTimeMilliseconds()`,
|
|
121
|
-
`Write-Output ($p.ProcessName + '|' + $t)`,
|
|
122
|
-
].join("; ");
|
|
123
|
-
try {
|
|
124
|
-
const { stdout } = await execFileAsync("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script], { timeout: WINDOWS_INSPECT_TIMEOUT_MS, windowsHide: true });
|
|
125
|
-
const line = String(stdout).trim().split(/\r?\n/).at(-1) ?? "";
|
|
126
|
-
const sep = line.lastIndexOf("|");
|
|
127
|
-
if (sep <= 0)
|
|
128
|
-
return { exists: true, name: null, startMs: null };
|
|
129
|
-
const name = line.slice(0, sep).trim() || null;
|
|
130
|
-
const startMs = Number(line.slice(sep + 1));
|
|
131
|
-
return {
|
|
132
|
-
exists: true,
|
|
133
|
-
name,
|
|
134
|
-
startMs: Number.isFinite(startMs) ? startMs : null,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
catch {
|
|
138
|
-
return { exists: isPidAlive(pid), name: null, startMs: null };
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
async function inspectLinux(pid) {
|
|
142
|
-
let name = null;
|
|
143
|
-
try {
|
|
144
|
-
name = (await fs.readFile(`/proc/${pid}/comm`, "utf8")).trim() || null;
|
|
145
|
-
}
|
|
146
|
-
catch {
|
|
147
|
-
return { exists: false, name: null, startMs: null };
|
|
148
|
-
}
|
|
149
|
-
const startMs = await linuxStartMs(pid);
|
|
150
|
-
return { exists: true, name, startMs };
|
|
151
|
-
}
|
|
152
|
-
async function linuxStartMs(pid) {
|
|
153
|
-
try {
|
|
154
|
-
const [stat, procStat] = await Promise.all([
|
|
155
|
-
fs.readFile("/proc/stat", "utf8"),
|
|
156
|
-
fs.readFile(`/proc/${pid}/stat`, "utf8"),
|
|
157
|
-
]);
|
|
158
|
-
const btime = /^btime (\d+)$/m.exec(stat);
|
|
159
|
-
const closeParen = procStat.lastIndexOf(")");
|
|
160
|
-
if (!btime || closeParen < 0)
|
|
161
|
-
return linuxStatFallback(pid);
|
|
162
|
-
const fields = procStat.slice(closeParen + 2).trim().split(/\s+/);
|
|
163
|
-
const startTicks = Number(fields[19]);
|
|
164
|
-
const bootSec = Number(btime[1]);
|
|
165
|
-
if (!Number.isFinite(startTicks) || !Number.isFinite(bootSec))
|
|
166
|
-
return linuxStatFallback(pid);
|
|
167
|
-
return Math.round((bootSec + startTicks / 100) * 1000);
|
|
168
|
-
}
|
|
169
|
-
catch {
|
|
170
|
-
return linuxStatFallback(pid);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
async function linuxStatFallback(pid) {
|
|
174
|
-
try {
|
|
175
|
-
const st = await fs.stat(`/proc/${pid}`);
|
|
176
|
-
const value = st.birthtimeMs > 0 ? st.birthtimeMs : st.ctimeMs;
|
|
177
|
-
return Number.isFinite(value) && value > 0 ? value : null;
|
|
178
|
-
}
|
|
179
|
-
catch {
|
|
180
|
-
return null;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
async function inspectPosix(pid) {
|
|
184
|
-
if (!isPidAlive(pid))
|
|
185
|
-
return { exists: false, name: null, startMs: null };
|
|
186
|
-
try {
|
|
187
|
-
const { stdout } = await execFileAsync("ps", ["-p", String(pid), "-o", "lstart=", "-o", "comm="], { timeout: WINDOWS_INSPECT_TIMEOUT_MS });
|
|
188
|
-
const line = String(stdout).trim();
|
|
189
|
-
if (!line)
|
|
190
|
-
return { exists: isPidAlive(pid), name: null, startMs: null };
|
|
191
|
-
const match = /^(.*\d{4})\s+(\S+)$/.exec(line);
|
|
192
|
-
if (!match)
|
|
193
|
-
return { exists: true, name: line.split(/\s+/).at(-1) ?? null, startMs: null };
|
|
194
|
-
const startMs = Date.parse(match[1] ?? "");
|
|
195
|
-
return {
|
|
196
|
-
exists: true,
|
|
197
|
-
name: match[2] ?? null,
|
|
198
|
-
startMs: Number.isFinite(startMs) ? startMs : null,
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
catch {
|
|
202
|
-
return { exists: isPidAlive(pid), name: null, startMs: null };
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
//# sourceMappingURL=process-liveness.js.map
|
package/vendor/core/skin-pack.js
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { normalizeBackgroundEffects } from "./types.js";
|
|
2
|
-
export const SKIN_SCHEMA_ID = "beauticode.skin/v1";
|
|
3
|
-
export const SKIN_ID_PATTERN = /^skin-[a-z0-9]{8,40}$/;
|
|
4
|
-
export function isSafeSkinId(id) {
|
|
5
|
-
return typeof id === "string" && SKIN_ID_PATTERN.test(id);
|
|
6
|
-
}
|
|
7
|
-
export function isLoopbackHostname(hostname) {
|
|
8
|
-
const host = String(hostname ?? "").toLowerCase();
|
|
9
|
-
return host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Public catalog origin. HTTPS required except loopback HTTP for local dev.
|
|
13
|
-
* Strips trailing slashes. Rejects credentials and unexpected protocols.
|
|
14
|
-
*/
|
|
15
|
-
export function normalizeSkinCenterUrl(value) {
|
|
16
|
-
if (typeof value !== "string")
|
|
17
|
-
return null;
|
|
18
|
-
const trimmed = value.trim();
|
|
19
|
-
if (!trimmed)
|
|
20
|
-
return null;
|
|
21
|
-
try {
|
|
22
|
-
const url = new URL(trimmed);
|
|
23
|
-
if (url.username || url.password || url.hash)
|
|
24
|
-
return null;
|
|
25
|
-
if (url.protocol !== "https:" && url.protocol !== "http:")
|
|
26
|
-
return null;
|
|
27
|
-
if (url.protocol === "http:" && !isLoopbackHostname(url.hostname))
|
|
28
|
-
return null;
|
|
29
|
-
const path = url.pathname === "/" ? "" : url.pathname.replace(/\/+$/, "");
|
|
30
|
-
return `${url.origin}${path}`;
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
export function resolveSkinCenterUrl(explicit) {
|
|
37
|
-
return (normalizeSkinCenterUrl(explicit) ??
|
|
38
|
-
normalizeSkinCenterUrl(process.env.BEAUTICODE_SKIN_CENTER));
|
|
39
|
-
}
|
|
40
|
-
export function normalizeSavedThemeSource(value) {
|
|
41
|
-
if (!value || typeof value !== "object")
|
|
42
|
-
return null;
|
|
43
|
-
const raw = value;
|
|
44
|
-
if (raw.kind !== "skin-center")
|
|
45
|
-
return null;
|
|
46
|
-
const skinId = String(raw.skinId ?? "").trim();
|
|
47
|
-
const centerUrl = normalizeSkinCenterUrl(raw.centerUrl);
|
|
48
|
-
if (!isSafeSkinId(skinId) || !centerUrl)
|
|
49
|
-
return null;
|
|
50
|
-
return { kind: "skin-center", skinId, centerUrl };
|
|
51
|
-
}
|
|
52
|
-
export function publicSkinEffects(value) {
|
|
53
|
-
return normalizeBackgroundEffects(value);
|
|
54
|
-
}
|
|
55
|
-
export function skinMediaPath(centerUrl, skinId, kind) {
|
|
56
|
-
const origin = normalizeSkinCenterUrl(centerUrl);
|
|
57
|
-
if (!origin || !isSafeSkinId(skinId)) {
|
|
58
|
-
throw new Error("Invalid skin-center media path.");
|
|
59
|
-
}
|
|
60
|
-
if (kind === "json")
|
|
61
|
-
return `${origin}/api/skins/${skinId}`;
|
|
62
|
-
return `${origin}/api/skins/${skinId}/${kind}`;
|
|
63
|
-
}
|
|
64
|
-
//# sourceMappingURL=skin-pack.js.map
|