beauticode-dsh 1.0.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.
Files changed (40) hide show
  1. package/README.zh-CN.md +48 -0
  2. package/agent.mjs +595 -0
  3. package/atmosphere.js +306 -0
  4. package/bin/beauticode-dsh +219 -0
  5. package/cli.js +219 -0
  6. package/client.js +370 -0
  7. package/console.js +303 -0
  8. package/control-client.mjs +445 -0
  9. package/cordis.patch.yml +4 -0
  10. package/host-apply.mjs +163 -0
  11. package/index.mjs +570 -0
  12. package/package.json +56 -0
  13. package/presets.mjs +73 -0
  14. package/themes/internal-beyond/NOTICE.md +9 -0
  15. package/themes/internal-beyond/bg-canvas-4k.png +0 -0
  16. package/themes/internal-beyond/bg-canvas.png +0 -0
  17. package/themes/internal-beyond/bg-infernal.jpg +0 -0
  18. package/themes/internal-beyond/bg-internal.jpg +0 -0
  19. package/ui-host.mjs +292 -0
  20. package/vendor/adapter-dsh/bridge.js +215 -0
  21. package/vendor/adapter-dsh/host-descriptor.js +15 -0
  22. package/vendor/adapter-dsh/index.js +6 -0
  23. package/vendor/adapter-dsh/injector-lock.js +27 -0
  24. package/vendor/adapter-dsh/package.json +5 -0
  25. package/vendor/adapter-dsh/session.js +527 -0
  26. package/vendor/adapter-dsh/token.js +46 -0
  27. package/vendor/adapter-dsh/tray-handoff.js +54 -0
  28. package/vendor/core/apply-transaction.js +287 -0
  29. package/vendor/core/background-store.js +1116 -0
  30. package/vendor/core/bundled-gallery.js +53 -0
  31. package/vendor/core/constants.js +47 -0
  32. package/vendor/core/error-message.js +208 -0
  33. package/vendor/core/file-lock.js +151 -0
  34. package/vendor/core/host-session.js +2 -0
  35. package/vendor/core/index.js +12 -0
  36. package/vendor/core/media-server.js +544 -0
  37. package/vendor/core/media-validation.js +244 -0
  38. package/vendor/core/package.json +5 -0
  39. package/vendor/core/paths.js +168 -0
  40. package/vendor/core/types.js +15 -0
@@ -0,0 +1,544 @@
1
+ import http from "node:http";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { createHash, randomUUID } from "node:crypto";
5
+ import { DEFAULT_TRUSTED_ORIGINS, IMAGE_EXTENSIONS, MAX_IMAGE_BYTES, MAX_VIDEO_BYTES, MEDIA_TOKEN_HEADER, MEDIA_TOKEN_HEADER_CANON, TRUSTED_ORIGIN_PREFIXES, VIDEO_EXTENSION, VIDEO_MIME, } from "./constants.js";
6
+ import { validateImageFile, validateVideoFile, } from "./media-validation.js";
7
+ function parseRange(value, size) {
8
+ if (typeof value !== "string" || !value.startsWith("bytes="))
9
+ return null;
10
+ const match = /^bytes=(\d*)-(\d*)$/.exec(value.trim());
11
+ if (!match || (!match[1] && !match[2]))
12
+ return null;
13
+ let start;
14
+ let end;
15
+ if (match[1]) {
16
+ start = Number(match[1]);
17
+ end = match[2] ? Number(match[2]) : size - 1;
18
+ }
19
+ else {
20
+ const suffix = Number(match[2]);
21
+ if (!Number.isSafeInteger(suffix) || suffix < 1)
22
+ return null;
23
+ start = Math.max(0, size - suffix);
24
+ end = size - 1;
25
+ }
26
+ if (!Number.isSafeInteger(start) ||
27
+ !Number.isSafeInteger(end) ||
28
+ start < 0 ||
29
+ end < start) {
30
+ return null;
31
+ }
32
+ if (start >= size)
33
+ return { unsatisfiable: true };
34
+ return { start, end: Math.min(end, size - 1) };
35
+ }
36
+ function makeToken() {
37
+ return randomUUID().replaceAll("-", "");
38
+ }
39
+ async function hashOpenedFile(handle, size) {
40
+ const hash = createHash("sha256");
41
+ const chunk = Buffer.allocUnsafe(1024 * 1024);
42
+ let position = 0;
43
+ while (position < size) {
44
+ const length = Math.min(chunk.byteLength, size - position);
45
+ const { bytesRead } = await handle.read(chunk, 0, length, position);
46
+ if (bytesRead <= 0)
47
+ throw new Error("Media file ended while revalidating");
48
+ hash.update(chunk.subarray(0, bytesRead));
49
+ position += bytesRead;
50
+ }
51
+ return hash.digest("hex");
52
+ }
53
+ /** Exact match or app://* prefix (hub is loopback-only). */
54
+ function matchTrustedOrigin(origin, trusted) {
55
+ if (!origin)
56
+ return null;
57
+ if (trusted.has(origin))
58
+ return origin;
59
+ for (const prefix of TRUSTED_ORIGIN_PREFIXES) {
60
+ if (origin.startsWith(prefix))
61
+ return origin;
62
+ }
63
+ return null;
64
+ }
65
+ /**
66
+ * Multi-route loopback hub. One TCP port, many /media/<token> assets.
67
+ */
68
+ export class LoopbackMediaHub {
69
+ trustedOrigins;
70
+ maxImageBytes;
71
+ maxVideoBytes;
72
+ server = null;
73
+ sockets = new Set();
74
+ assets = new Map();
75
+ closed = false;
76
+ port = 0;
77
+ listenPromise = null;
78
+ constructor(opts = {}) {
79
+ this.trustedOrigins = new Set(opts.trustedOrigins ?? DEFAULT_TRUSTED_ORIGINS);
80
+ this.maxImageBytes = opts.maxImageBytes ?? MAX_IMAGE_BYTES;
81
+ this.maxVideoBytes = opts.maxVideoBytes ?? MAX_VIDEO_BYTES;
82
+ }
83
+ get listeningPort() {
84
+ return this.port;
85
+ }
86
+ get assetCount() {
87
+ return this.assets.size;
88
+ }
89
+ async ensureListening() {
90
+ if (this.server)
91
+ return;
92
+ if (this.closed)
93
+ throw new Error("Media hub is closed");
94
+ if (this.listenPromise)
95
+ return this.listenPromise;
96
+ this.listenPromise = (async () => {
97
+ const server = http.createServer({ maxHeaderSize: 16 * 1024, requestTimeout: 30_000 }, (req, res) => {
98
+ void this.handle(req, res);
99
+ });
100
+ server.headersTimeout = 10_000;
101
+ server.keepAliveTimeout = 5_000;
102
+ server.maxRequestsPerSocket = 100;
103
+ server.on("connection", (socket) => {
104
+ this.sockets.add(socket);
105
+ socket.once("close", () => this.sockets.delete(socket));
106
+ });
107
+ await new Promise((resolve, reject) => {
108
+ server.once("error", reject);
109
+ server.listen(0, "127.0.0.1", () => resolve());
110
+ });
111
+ const address = server.address();
112
+ if (!address || typeof address === "string") {
113
+ await new Promise((resolve) => server.close(() => resolve()));
114
+ throw new Error("Local media server did not expose a TCP port.");
115
+ }
116
+ if (this.closed) {
117
+ await new Promise((resolve) => server.close(() => resolve()));
118
+ throw new Error("Media hub closed while listener was starting.");
119
+ }
120
+ this.server = server;
121
+ this.port = address.port;
122
+ })();
123
+ try {
124
+ await this.listenPromise;
125
+ }
126
+ finally {
127
+ this.listenPromise = null;
128
+ }
129
+ }
130
+ async addFile(filePath) {
131
+ const ext = path.extname(filePath).toLowerCase();
132
+ let kind;
133
+ let mime;
134
+ let size;
135
+ let identity;
136
+ let device;
137
+ let inode;
138
+ let mtimeMs;
139
+ let ctimeMs;
140
+ let resolved;
141
+ if (IMAGE_EXTENSIONS.includes(ext)) {
142
+ const v = await validateImageFile(filePath, {
143
+ maxBytes: this.maxImageBytes,
144
+ });
145
+ kind = "image";
146
+ mime = v.mime;
147
+ size = v.size;
148
+ identity = v.identity;
149
+ device = v.device;
150
+ inode = v.inode;
151
+ mtimeMs = v.mtimeMs;
152
+ ctimeMs = v.ctimeMs;
153
+ resolved = v.filePath;
154
+ }
155
+ else if (ext === VIDEO_EXTENSION) {
156
+ const v = await validateVideoFile(filePath, {
157
+ maxBytes: this.maxVideoBytes,
158
+ });
159
+ kind = "video";
160
+ mime = VIDEO_MIME;
161
+ size = v.size;
162
+ identity = v.identity;
163
+ device = v.device;
164
+ inode = v.inode;
165
+ mtimeMs = v.mtimeMs;
166
+ ctimeMs = v.ctimeMs;
167
+ resolved = v.filePath;
168
+ }
169
+ else {
170
+ throw new Error(`Unsupported media extension: ${ext || "(none)"}`);
171
+ }
172
+ // Validate first. Invalid input must not leave an unreachable listener.
173
+ await this.ensureListening();
174
+ // Reuse identical asset.
175
+ for (const existing of this.assets.values()) {
176
+ if (existing.filePath === resolved &&
177
+ existing.size === size &&
178
+ existing.identity === identity &&
179
+ existing.kind === kind) {
180
+ return this.toHandle(existing);
181
+ }
182
+ }
183
+ const token = makeToken();
184
+ const route = `/media/${token}`;
185
+ const rec = {
186
+ kind,
187
+ filePath: resolved,
188
+ size,
189
+ identity,
190
+ device,
191
+ inode,
192
+ mtimeMs,
193
+ ctimeMs,
194
+ mime,
195
+ token,
196
+ route,
197
+ };
198
+ this.assets.set(token, rec);
199
+ return this.toHandle(rec);
200
+ }
201
+ async remove(token) {
202
+ if (!token)
203
+ return;
204
+ this.assets.delete(token);
205
+ }
206
+ async close() {
207
+ this.closed = true;
208
+ this.assets.clear();
209
+ await this.listenPromise?.catch(() => { });
210
+ await this.closeServerOnly();
211
+ }
212
+ toHandle(rec) {
213
+ const url = `http://127.0.0.1:${this.port}${rec.route}`;
214
+ return {
215
+ kind: rec.kind,
216
+ filePath: rec.filePath,
217
+ size: rec.size,
218
+ identity: rec.identity,
219
+ device: rec.device,
220
+ inode: rec.inode,
221
+ mtimeMs: rec.mtimeMs,
222
+ ctimeMs: rec.ctimeMs,
223
+ mime: rec.mime,
224
+ token: rec.token,
225
+ route: rec.route,
226
+ url,
227
+ srcUrl: `${url}?t=${encodeURIComponent(rec.token)}`,
228
+ };
229
+ }
230
+ async closeServerOnly() {
231
+ const server = this.server;
232
+ this.server = null;
233
+ this.port = 0;
234
+ if (!server)
235
+ return;
236
+ for (const socket of this.sockets) {
237
+ try {
238
+ socket.destroy();
239
+ }
240
+ catch {
241
+ /* ignore */
242
+ }
243
+ }
244
+ this.sockets.clear();
245
+ // Node 18.2+: drop keep-alive / half-open clients so close() settles on Windows.
246
+ if (typeof server.closeAllConnections === "function") {
247
+ try {
248
+ server.closeAllConnections();
249
+ }
250
+ catch {
251
+ /* ignore */
252
+ }
253
+ }
254
+ await new Promise((resolve) => {
255
+ server.close(() => resolve());
256
+ // Failsafe: never hang process exit on a stuck listener.
257
+ setTimeout(resolve, 500).unref?.();
258
+ });
259
+ }
260
+ async handle(request, response) {
261
+ const originHeader = request.headers.origin;
262
+ const origin = typeof originHeader === "string" ? originHeader : undefined;
263
+ const trustedOrigin = matchTrustedOrigin(origin, this.trustedOrigins);
264
+ const corsHeaders = {
265
+ "Access-Control-Allow-Methods": "GET, HEAD, OPTIONS",
266
+ "Access-Control-Allow-Headers": [
267
+ "Range",
268
+ MEDIA_TOKEN_HEADER_CANON,
269
+ "Content-Type",
270
+ "Accept",
271
+ ].join(", "),
272
+ // Required for Chromium Private Network Access (app:// → 127.0.0.1).
273
+ "Access-Control-Allow-Private-Network": "true",
274
+ "Access-Control-Expose-Headers": "Accept-Ranges, Content-Length, Content-Range, Content-Type",
275
+ Vary: "Origin",
276
+ };
277
+ if (trustedOrigin) {
278
+ corsHeaders["Access-Control-Allow-Origin"] = trustedOrigin;
279
+ }
280
+ const rawUrl = request.url ?? "";
281
+ const urlPath = rawUrl.split("?", 1)[0] ?? "";
282
+ const pathMatch = /^\/media\/([A-Za-z0-9]{16,64})$/.exec(urlPath);
283
+ const pathToken = pathMatch?.[1] ?? null;
284
+ if (pathToken && request.method === "OPTIONS") {
285
+ if (origin && !trustedOrigin) {
286
+ response.writeHead(403, { "Cache-Control": "no-store" });
287
+ response.end();
288
+ return;
289
+ }
290
+ response.writeHead(204, corsHeaders);
291
+ response.end();
292
+ return;
293
+ }
294
+ if (this.closed ||
295
+ !request.method ||
296
+ !["GET", "HEAD"].includes(request.method) ||
297
+ !pathToken) {
298
+ response.writeHead(this.closed ? 503 : 404, {
299
+ "Cache-Control": "no-store",
300
+ });
301
+ response.end();
302
+ return;
303
+ }
304
+ // Auth: path token identifies asset; require matching header OR ?t=
305
+ const headerRaw = request.headers[MEDIA_TOKEN_HEADER];
306
+ const headerToken = typeof headerRaw === "string"
307
+ ? headerRaw
308
+ : Array.isArray(headerRaw)
309
+ ? headerRaw[0]
310
+ : "";
311
+ let queryToken = "";
312
+ try {
313
+ queryToken = new URL(rawUrl, "http://127.0.0.1").searchParams.get("t") ?? "";
314
+ }
315
+ catch {
316
+ queryToken = "";
317
+ }
318
+ const tokenOk = headerToken === pathToken || queryToken === pathToken;
319
+ if ((origin && !trustedOrigin) || !tokenOk) {
320
+ response.writeHead(403, { "Cache-Control": "no-store" });
321
+ response.end();
322
+ return;
323
+ }
324
+ const asset = this.assets.get(pathToken);
325
+ if (!asset) {
326
+ response.writeHead(404, { "Cache-Control": "no-store" });
327
+ response.end();
328
+ return;
329
+ }
330
+ let opened = null;
331
+ try {
332
+ const lstat = await fs.lstat(asset.filePath);
333
+ if (lstat.isSymbolicLink()) {
334
+ throw new Error("Media path became a symbolic link");
335
+ }
336
+ opened = await fs.open(asset.filePath, "r");
337
+ const stat = await opened.stat();
338
+ const maxBytes = asset.kind === "image" ? this.maxImageBytes : this.maxVideoBytes;
339
+ const fingerprintChanged = stat.dev !== asset.device ||
340
+ stat.ino !== asset.inode ||
341
+ stat.mtimeMs !== asset.mtimeMs ||
342
+ stat.ctimeMs !== asset.ctimeMs;
343
+ if (!stat.isFile() ||
344
+ stat.size !== asset.size ||
345
+ stat.size > maxBytes) {
346
+ throw new Error("Media file changed or exceeded the safety limit");
347
+ }
348
+ if (fingerprintChanged) {
349
+ const currentHash = await hashOpenedFile(opened, stat.size);
350
+ if (currentHash !== asset.identity) {
351
+ throw new Error("Media file content changed after staging");
352
+ }
353
+ asset.device = stat.dev;
354
+ asset.inode = stat.ino;
355
+ asset.mtimeMs = stat.mtimeMs;
356
+ asset.ctimeMs = stat.ctimeMs;
357
+ }
358
+ const rangeHeader = request.headers.range;
359
+ const range = parseRange(rangeHeader, stat.size);
360
+ const headers = {
361
+ ...corsHeaders,
362
+ "Content-Type": asset.mime,
363
+ "Accept-Ranges": "bytes",
364
+ "Cache-Control": "no-store",
365
+ "X-Content-Type-Options": "nosniff",
366
+ };
367
+ if (rangeHeader !== undefined && !range) {
368
+ headers["Content-Range"] = `bytes */${stat.size}`;
369
+ response.writeHead(416, headers);
370
+ response.end();
371
+ await opened.close();
372
+ opened = null;
373
+ return;
374
+ }
375
+ if (range && "unsatisfiable" in range) {
376
+ headers["Content-Range"] = `bytes */${stat.size}`;
377
+ response.writeHead(416, headers);
378
+ response.end();
379
+ await opened.close();
380
+ opened = null;
381
+ return;
382
+ }
383
+ if (!range) {
384
+ headers["Content-Length"] = stat.size;
385
+ response.writeHead(200, headers);
386
+ if (request.method === "HEAD") {
387
+ response.end();
388
+ await opened.close();
389
+ opened = null;
390
+ return;
391
+ }
392
+ const stream = opened.createReadStream({ start: 0, autoClose: true });
393
+ opened = null;
394
+ stream
395
+ .on("error", () => response.destroy())
396
+ .pipe(response);
397
+ return;
398
+ }
399
+ headers["Content-Range"] =
400
+ `bytes ${range.start}-${range.end}/${stat.size}`;
401
+ headers["Content-Length"] = range.end - range.start + 1;
402
+ response.writeHead(206, headers);
403
+ if (request.method === "HEAD") {
404
+ response.end();
405
+ await opened.close();
406
+ opened = null;
407
+ return;
408
+ }
409
+ const stream = opened.createReadStream({
410
+ start: range.start,
411
+ end: range.end,
412
+ autoClose: true,
413
+ });
414
+ opened = null;
415
+ stream
416
+ .on("error", () => response.destroy())
417
+ .pipe(response);
418
+ }
419
+ catch {
420
+ await opened?.close().catch(() => { });
421
+ if (response.headersSent) {
422
+ response.destroy();
423
+ }
424
+ else {
425
+ response.writeHead(404, { "Cache-Control": "no-store" });
426
+ response.end();
427
+ }
428
+ }
429
+ }
430
+ }
431
+ /**
432
+ * Back-compat single-asset helper used by older call sites/tests.
433
+ */
434
+ export async function createMediaServer(filePath, opts = {}) {
435
+ const hub = new LoopbackMediaHub(opts);
436
+ const asset = await hub.addFile(filePath);
437
+ return {
438
+ ...asset,
439
+ async close() {
440
+ await hub.close();
441
+ },
442
+ };
443
+ }
444
+ function isAssetHandle(value) {
445
+ if (!value || typeof value !== "object")
446
+ return false;
447
+ const v = value;
448
+ return ((v.kind === "image" || v.kind === "video") &&
449
+ typeof v.token === "string" &&
450
+ typeof v.srcUrl === "string" &&
451
+ !("image" in v && typeof v.image === "object") &&
452
+ !("video" in v && typeof v.video === "object"));
453
+ }
454
+ function isPairCommit(value) {
455
+ if (!value || typeof value !== "object")
456
+ return false;
457
+ // Pair shape is chosen when image/video keys are present (even if null).
458
+ return "image" in value || "video" in value;
459
+ }
460
+ /**
461
+ * Controller that can hold poster image + video assets on one hub.
462
+ */
463
+ export class MediaServerController {
464
+ #hub;
465
+ #enabled;
466
+ #image = null;
467
+ #video = null;
468
+ constructor(opts = {}) {
469
+ this.#enabled = opts.enabled ?? true;
470
+ this.#hub = new LoopbackMediaHub(opts);
471
+ }
472
+ /** @deprecated use activeVideo; kept for callers expecting a single active */
473
+ get active() {
474
+ return this.#video ?? this.#image;
475
+ }
476
+ get activeImage() {
477
+ return this.#image;
478
+ }
479
+ get activeVideo() {
480
+ return this.#video;
481
+ }
482
+ async stage(filePath) {
483
+ if (!filePath || !this.#enabled)
484
+ return null;
485
+ return this.#hub.addFile(filePath);
486
+ }
487
+ /**
488
+ * Promote staged image/video handles; close assets that are no longer needed.
489
+ */
490
+ async commit(next) {
491
+ let nextImage = null;
492
+ let nextVideo = null;
493
+ if (next == null) {
494
+ nextImage = null;
495
+ nextVideo = null;
496
+ }
497
+ else if (isPairCommit(next)) {
498
+ nextImage = next.image ?? null;
499
+ nextVideo = next.video ?? null;
500
+ }
501
+ else if (isAssetHandle(next)) {
502
+ // Legacy single-handle commit.
503
+ if (next.kind === "image")
504
+ nextImage = next;
505
+ else
506
+ nextVideo = next;
507
+ }
508
+ else {
509
+ throw new Error("Invalid media commit payload.");
510
+ }
511
+ const prevImage = this.#image;
512
+ const prevVideo = this.#video;
513
+ this.#image = nextImage;
514
+ this.#video = nextVideo;
515
+ // Drop unreferenced assets. Keep tokens that are still active (including
516
+ // the case where image+video swap roles is impossible — kinds differ).
517
+ const keep = new Set();
518
+ if (nextImage)
519
+ keep.add(nextImage.token);
520
+ if (nextVideo)
521
+ keep.add(nextVideo.token);
522
+ if (prevImage && !keep.has(prevImage.token)) {
523
+ await this.#hub.remove(prevImage.token);
524
+ }
525
+ if (prevVideo && !keep.has(prevVideo.token)) {
526
+ await this.#hub.remove(prevVideo.token);
527
+ }
528
+ }
529
+ async abort(staged) {
530
+ if (!staged)
531
+ return;
532
+ if (staged.token === this.#image?.token ||
533
+ staged.token === this.#video?.token) {
534
+ return;
535
+ }
536
+ await this.#hub.remove(staged.token);
537
+ }
538
+ async close() {
539
+ this.#image = null;
540
+ this.#video = null;
541
+ await this.#hub.close();
542
+ }
543
+ }
544
+ //# sourceMappingURL=media-server.js.map