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.
@@ -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, "主题元数据无法读取。"],
@@ -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
- import { isRecordedPidLive, } from "./process-liveness.js";
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
- const recordedStartedAt = existing?.startedAt ?? new Date(Date.now() - age).toISOString();
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 && existingPid == null && age <= staleMs) {
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
@@ -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
- function isLoopbackHttpOrigin(origin) {
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 && !isLoopbackHttpOrigin(origin)) || !tokenOk) {
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
- pipeOpenedFile(opened, response, { start: 0 });
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
- pipeOpenedFile(opened, response, {
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 chunk = Buffer.allocUnsafe(1024 * 1024);
101
- let position = 0;
102
- let headOffset = 0;
103
- while (position < expectedSize) {
104
- const length = Math.min(chunk.byteLength, expectedSize - position);
105
- const { bytesRead } = await handle.read(chunk, 0, length, position);
106
- if (bytesRead <= 0) {
107
- throw new MediaValidationError("Media file ended during validation.");
108
- }
109
- const bytes = chunk.subarray(0, bytesRead);
110
- hash.update(bytes);
111
- if (headOffset < head.byteLength) {
112
- const take = Math.min(bytesRead, head.byteLength - headOffset);
113
- bytes.copy(head, headOffset, 0, take);
114
- headOffset += take;
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
  }
@@ -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
@@ -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