dsh-mobile 0.6.3 → 0.6.4

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/lib/cli.js CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
+ import { chmod, lstat, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
3
  import { homedir, networkInterfaces } from "node:os";
4
4
  import { basename, dirname, isAbsolute, join, resolve, win32 } from "node:path";
5
5
  import { X509Certificate, createPrivateKey, createPublicKey, randomBytes } from "node:crypto";
6
6
  import { execFile } from "node:child_process";
7
7
  import { generate } from "selfsigned";
8
+ import { performance } from "node:perf_hooks";
8
9
  import "@deepseek-ai/cordis";
9
10
  import "@deepseek-ai/schemastery";
10
11
  //#region src/exec-file.ts
@@ -34,24 +35,75 @@ function execFileText(file, args, options = {}) {
34
35
  //#endregion
35
36
  //#region src/private-file.ts
36
37
  let userSidTask;
38
+ function commandFailureDetails(error) {
39
+ if (error === null || typeof error !== "object") return {};
40
+ let descriptors;
41
+ try {
42
+ descriptors = Object.getOwnPropertyDescriptors(error);
43
+ } catch (_error) {
44
+ return {};
45
+ }
46
+ const code = descriptors.code?.value;
47
+ const signal = descriptors.signal?.value;
48
+ const killed = descriptors.killed?.value;
49
+ return {
50
+ ...code === null || typeof code === "number" && Number.isSafeInteger(code) || typeof code === "string" && /^[A-Z][A-Z0-9_]{0,63}$/u.test(code) ? { code } : {},
51
+ ...signal === null || typeof signal === "string" && /^SIG[A-Z0-9]{1,16}$/u.test(signal) ? { signal } : {},
52
+ ...typeof killed === "boolean" ? { killed } : {}
53
+ };
54
+ }
55
+ /** Phase and full await duration, including subprocess creation, without arguments, output or private paths. */
56
+ var PrivateFilePermissionError = class extends Error {
57
+ stage;
58
+ elapsedMs;
59
+ code;
60
+ signal;
61
+ killed;
62
+ constructor(stage, elapsedMs, error) {
63
+ const details = commandFailureDetails(error);
64
+ const fields = [
65
+ ...details.code === void 0 ? [] : [`code=${String(details.code)}`],
66
+ ...details.signal === void 0 ? [] : [`signal=${String(details.signal)}`],
67
+ ...details.killed === void 0 ? [] : [`killed=${String(details.killed)}`]
68
+ ];
69
+ const summary = `Windows private-file ${stage} failed after ${String(elapsedMs)} ms${fields.length === 0 ? "" : ` (${fields.join(", ")})`}`;
70
+ const cause = Object.assign(/* @__PURE__ */ new Error("Windows permission operation failed"), details);
71
+ super(summary, { cause });
72
+ this.stage = stage;
73
+ this.elapsedMs = elapsedMs;
74
+ this.name = "PrivateFilePermissionError";
75
+ this.code = details.code;
76
+ this.signal = details.signal;
77
+ this.killed = details.killed;
78
+ }
79
+ };
80
+ async function permissionOperation(stage, operation) {
81
+ const started = performance.now();
82
+ try {
83
+ return await operation();
84
+ } catch (error) {
85
+ throw new PrivateFilePermissionError(stage, Math.max(0, Math.round(performance.now() - started)), error);
86
+ }
87
+ }
37
88
  function windowsSystemTool(name) {
38
89
  const systemRoot = process.env.SystemRoot ?? process.env.WINDIR;
39
- if (systemRoot === void 0 || !win32.isAbsolute(systemRoot)) throw new Error("Windows system root is unavailable");
90
+ if (systemRoot === void 0 || !win32.isAbsolute(systemRoot)) throw Object.assign(/* @__PURE__ */ new Error("Windows system root is unavailable"), { code: "WINDOWS_SYSTEM_ROOT_UNAVAILABLE" });
40
91
  return win32.join(systemRoot, "System32", name);
41
92
  }
42
93
  async function currentWindowsUserSid() {
43
- userSidTask ??= execFileText(windowsSystemTool("whoami.exe"), [
44
- "/user",
45
- "/fo",
46
- "csv",
47
- "/nh"
48
- ], {
49
- encoding: "utf8",
50
- windowsHide: true,
51
- timeout: 1e4
52
- }).then(({ stdout }) => {
94
+ userSidTask ??= permissionOperation("sid-query", async () => {
95
+ const { stdout } = await execFileText(windowsSystemTool("whoami.exe"), [
96
+ "/user",
97
+ "/fo",
98
+ "csv",
99
+ "/nh"
100
+ ], {
101
+ encoding: "utf8",
102
+ windowsHide: true,
103
+ timeout: 1e4
104
+ });
53
105
  const match = /,"(S-\d(?:-\d+)+)"\s*$/u.exec(stdout.trim());
54
- if (match?.[1] === void 0) throw new Error("unable to resolve the current Windows user SID");
106
+ if (match?.[1] === void 0) throw Object.assign(/* @__PURE__ */ new Error("unable to resolve the current Windows user SID"), { code: "WINDOWS_USER_SID_INVALID" });
55
107
  return match[1];
56
108
  }).catch((error) => {
57
109
  userSidTask = void 0;
@@ -59,12 +111,12 @@ async function currentWindowsUserSid() {
59
111
  });
60
112
  return userSidTask;
61
113
  }
62
- /** Restrict a sensitive regular file to the current user and Windows administrators. */
114
+ /** Restrict a sensitive regular file to the current user and Windows administrators; reject Windows permission failures with redacted phase and duration. */
63
115
  async function restrictPrivateFile(file, mode = 384) {
64
116
  await chmod(file, mode);
65
117
  if (process.platform !== "win32") return;
66
118
  const userSid = await currentWindowsUserSid();
67
- await execFileText(windowsSystemTool("icacls.exe"), [
119
+ await permissionOperation("acl-apply", () => execFileText(windowsSystemTool("icacls.exe"), [
68
120
  file,
69
121
  "/inheritance:r",
70
122
  "/grant:r",
@@ -79,7 +131,7 @@ async function restrictPrivateFile(file, mode = 384) {
79
131
  encoding: "utf8",
80
132
  windowsHide: true,
81
133
  timeout: 1e4
82
- });
134
+ }));
83
135
  }
84
136
  Object.freeze([
85
137
  [0n, 8],
@@ -243,7 +295,7 @@ function assertMatchingCa(certPem, keyPem) {
243
295
  if (Date.parse(certificate.validFrom) > Date.now() || Date.parse(certificate.validTo) <= Date.now()) throw new Error("managed TLS CA certificate is not currently valid");
244
296
  return certificate;
245
297
  }
246
- async function atomicWrite(file, contents) {
298
+ async function stagePrivateFile(file, contents) {
247
299
  const directory = dirname(file);
248
300
  await mkdir(directory, {
249
301
  recursive: true,
@@ -251,17 +303,85 @@ async function atomicWrite(file, contents) {
251
303
  });
252
304
  const temporary = join(directory, `.${basename(file)}.${randomBytes(12).toString("hex")}.tmp`);
253
305
  try {
254
- await writeFile(temporary, contents, {
306
+ await writeFile(temporary, "", {
255
307
  mode: 384,
256
308
  flag: "wx"
257
309
  });
258
310
  await restrictPrivateFile(temporary);
311
+ await writeFile(temporary, contents);
312
+ return temporary;
313
+ } catch (error) {
314
+ await rm(temporary, { force: true });
315
+ throw error;
316
+ }
317
+ }
318
+ async function atomicWrite(file, contents) {
319
+ const temporary = await stagePrivateFile(file, contents);
320
+ try {
259
321
  await rename(temporary, file);
260
322
  await restrictPrivateFile(file);
261
323
  } finally {
262
324
  await rm(temporary, { force: true });
263
325
  }
264
326
  }
327
+ const certificatePublications = /* @__PURE__ */ new Map();
328
+ /** Stage both files before promotion and retain a private rollback copy until the pair is installed. */
329
+ async function installServerCertificatePair(output, cert, key) {
330
+ const identity = `${output.certFile}\0${output.keyFile}`;
331
+ const publication = (certificatePublications.get(identity) ?? Promise.resolve()).catch(() => void 0).then(async () => {
332
+ const files = [output.certFile, output.keyFile];
333
+ const contents = [cert, key];
334
+ const staged = [];
335
+ const backups = [];
336
+ let promoted = 0;
337
+ let restored = true;
338
+ const failures = [];
339
+ try {
340
+ for (let index = 0; index < files.length; index += 1) {
341
+ const file = files[index];
342
+ staged.push(await stagePrivateFile(file, contents[index]));
343
+ let old;
344
+ try {
345
+ const metadata = await lstat(file);
346
+ if (!metadata.isFile() || metadata.isSymbolicLink()) throw new Error("managed TLS output is not a regular file");
347
+ old = await readFile(file);
348
+ } catch (error) {
349
+ if (error.code !== "ENOENT") throw error;
350
+ }
351
+ backups.push(old === void 0 ? void 0 : await stagePrivateFile(file, old));
352
+ }
353
+ for (let index = 0; index < files.length; index += 1) {
354
+ await rename(staged[index], files[index]);
355
+ promoted += 1;
356
+ }
357
+ } catch (error) {
358
+ failures.push(error);
359
+ for (let index = promoted - 1; index >= 0; index -= 1) try {
360
+ const backup = backups[index];
361
+ if (backup === void 0) await rm(files[index], { force: true });
362
+ else await rename(backup, files[index]);
363
+ } catch (restoreError) {
364
+ restored = false;
365
+ failures.push(restoreError);
366
+ }
367
+ } finally {
368
+ const cleanup = await Promise.allSettled(staged.map((file) => rm(file, { force: true })));
369
+ for (const result of cleanup) if (result.status === "rejected") failures.push(result.reason);
370
+ if (restored) {
371
+ const backupCleanup = await Promise.allSettled(backups.filter((file) => file !== void 0).map((file) => rm(file, { force: true })));
372
+ for (const result of backupCleanup) if (result.status === "rejected") failures.push(result.reason);
373
+ }
374
+ }
375
+ if (failures.length > 1) throw new AggregateError(failures, "managed TLS certificate replacement, rollback or cleanup failed");
376
+ if (failures.length === 1) throw failures[0];
377
+ });
378
+ certificatePublications.set(identity, publication);
379
+ try {
380
+ await publication;
381
+ } finally {
382
+ if (certificatePublications.get(identity) === publication) certificatePublications.delete(identity);
383
+ }
384
+ }
265
385
  /** Create a long-lived CA or migrate the legacy self-signed server certificate as that CA. */
266
386
  async function ensureManagedCa(setup, legacy) {
267
387
  let certPem;
@@ -376,7 +496,7 @@ async function issueServerCertificate(authority, target, output, lifetimeDays =
376
496
  }
377
497
  ]
378
498
  });
379
- await Promise.all([atomicWrite(output.certFile, server.cert), atomicWrite(output.keyFile, server.private)]);
499
+ await installServerCertificatePair(output, server.cert, server.private);
380
500
  }
381
501
  /** Sign and atomically install a server leaf for the interface's current address. */
382
502
  async function refreshManagedServerCertificate(setup, address) {