@linxin666/dsh-remote-web-ui 0.3.24 → 0.4.1

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 (112) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +13 -10
  3. package/README.zh.md +13 -10
  4. package/icon.svg +3 -0
  5. package/lib/client.js +263 -129
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +565 -256
  8. package/lib/types/client/FenceNotice.d.ts.map +1 -1
  9. package/lib/types/client/FenceNotice.js +0 -3
  10. package/lib/types/client/PluginSettingsCard.d.ts +4 -0
  11. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  12. package/lib/types/client/PluginSettingsCard.js +3 -2
  13. package/lib/types/client/RemoteEntry.d.ts.map +1 -1
  14. package/lib/types/client/RemoteEntry.js +14 -9
  15. package/lib/types/client/RemotePanel.d.ts.map +1 -1
  16. package/lib/types/client/RemotePanel.js +13 -4
  17. package/lib/types/client/RemoteSettingsCard.d.ts +6 -6
  18. package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
  19. package/lib/types/client/RemoteSettingsCard.js +14 -8
  20. package/lib/types/client/UpdateEntry.js +2 -2
  21. package/lib/types/client/UpdatePanel.js +2 -2
  22. package/lib/types/client/deep-link.js +2 -2
  23. package/lib/types/client/index.d.ts +16 -8
  24. package/lib/types/client/index.d.ts.map +1 -1
  25. package/lib/types/client/index.js +47 -18
  26. package/lib/types/client/locales.d.ts +0 -2
  27. package/lib/types/client/locales.d.ts.map +1 -1
  28. package/lib/types/client/locales.js +6 -8
  29. package/lib/types/client/mobile-adapt.d.ts.map +1 -1
  30. package/lib/types/client/mobile-adapt.js +103 -22
  31. package/lib/types/client/pair-api.d.ts +32 -3
  32. package/lib/types/client/pair-api.d.ts.map +1 -1
  33. package/lib/types/client/pair-api.js +44 -6
  34. package/lib/types/client/plugin-card-seat.d.ts +23 -16
  35. package/lib/types/client/plugin-card-seat.d.ts.map +1 -1
  36. package/lib/types/client/plugin-card-seat.js +16 -14
  37. package/lib/types/client/remote-channel.d.ts.map +1 -1
  38. package/lib/types/client/remote-channel.js +18 -5
  39. package/lib/types/client/settings-form.d.ts +17 -17
  40. package/lib/types/client/settings-form.d.ts.map +1 -1
  41. package/lib/types/client/settings-form.js +30 -27
  42. package/lib/types/http.d.ts +18 -1
  43. package/lib/types/http.d.ts.map +1 -1
  44. package/lib/types/http.js +22 -1
  45. package/lib/types/index.d.ts +19 -11
  46. package/lib/types/index.d.ts.map +1 -1
  47. package/lib/types/index.js +91 -76
  48. package/lib/types/inner-auth.d.ts +3 -2
  49. package/lib/types/inner-auth.d.ts.map +1 -1
  50. package/lib/types/inner-auth.js +3 -2
  51. package/lib/types/pair-grant.d.ts +66 -0
  52. package/lib/types/pair-grant.d.ts.map +1 -0
  53. package/lib/types/pair-grant.js +95 -0
  54. package/lib/types/pairing.d.ts +44 -12
  55. package/lib/types/pairing.d.ts.map +1 -1
  56. package/lib/types/pairing.js +69 -20
  57. package/lib/types/pairing.persist.test.js +26 -0
  58. package/lib/types/posture.d.ts +11 -3
  59. package/lib/types/posture.d.ts.map +1 -1
  60. package/lib/types/posture.js +8 -3
  61. package/lib/types/remote-api.d.ts +10 -13
  62. package/lib/types/remote-api.d.ts.map +1 -1
  63. package/lib/types/remote-api.js +34 -34
  64. package/lib/types/remote-channel-boot.d.ts +6 -1
  65. package/lib/types/remote-channel-boot.d.ts.map +1 -1
  66. package/lib/types/remote-channel-boot.js +16 -8
  67. package/lib/types/remote-channel-rules.d.ts +14 -0
  68. package/lib/types/remote-channel-rules.d.ts.map +1 -1
  69. package/lib/types/remote-channel-rules.js +8 -0
  70. package/lib/types/remote-methods.d.ts +1 -1
  71. package/lib/types/remote-methods.d.ts.map +1 -1
  72. package/lib/types/remote-methods.js +1 -0
  73. package/lib/types/routes.d.ts +47 -5
  74. package/lib/types/routes.d.ts.map +1 -1
  75. package/lib/types/routes.js +160 -58
  76. package/lib/types/tunnel.d.ts +21 -6
  77. package/lib/types/tunnel.d.ts.map +1 -1
  78. package/lib/types/tunnel.js +58 -11
  79. package/lib/types/update.d.ts +4 -0
  80. package/lib/types/update.d.ts.map +1 -1
  81. package/lib/types/update.js +74 -26
  82. package/package.json +23 -20
  83. package/src/client/FenceNotice.tsx +0 -2
  84. package/src/client/PluginSettingsCard.tsx +7 -2
  85. package/src/client/RemoteEntry.tsx +14 -9
  86. package/src/client/RemotePanel.tsx +17 -7
  87. package/src/client/RemoteSettingsCard.tsx +16 -9
  88. package/src/client/UpdateEntry.tsx +2 -2
  89. package/src/client/UpdatePanel.tsx +3 -3
  90. package/src/client/deep-link.ts +2 -2
  91. package/src/client/index.ts +61 -27
  92. package/src/client/locales.ts +6 -8
  93. package/src/client/mobile-adapt.ts +97 -20
  94. package/src/client/pair-api.ts +47 -7
  95. package/src/client/plugin-card-seat.ts +24 -17
  96. package/src/client/remote-channel.ts +19 -4
  97. package/src/client/remote.module.css +38 -0
  98. package/src/client/settings-form.ts +34 -31
  99. package/src/http.ts +23 -1
  100. package/src/index.ts +102 -79
  101. package/src/inner-auth.ts +3 -2
  102. package/src/pair-grant.ts +131 -0
  103. package/src/pairing.persist.test.ts +36 -0
  104. package/src/pairing.ts +75 -24
  105. package/src/posture.ts +12 -4
  106. package/src/remote-api.ts +34 -41
  107. package/src/remote-channel-boot.ts +16 -8
  108. package/src/remote-channel-rules.ts +17 -0
  109. package/src/remote-methods.ts +1 -0
  110. package/src/routes.ts +165 -57
  111. package/src/tunnel.ts +56 -11
  112. package/src/update.ts +74 -24
package/src/update.ts CHANGED
@@ -12,7 +12,8 @@
12
12
  import { readFileSync } from 'node:fs'
13
13
  import { dirname, join } from 'node:path'
14
14
  import { spawn } from 'node:child_process'
15
- import { decodeConsoleBytes } from './console-output.ts'
15
+ import { createOutputCapture } from './console-output.ts'
16
+ import { withIdentityEncoding } from './http.ts'
16
17
 
17
18
  /** npm registry base used for version probes. */
18
19
  export const NPM_REGISTRY = 'https://registry.npmjs.org'
@@ -322,7 +323,7 @@ export async function fetchLatestVersion(
322
323
  const controller = new AbortController()
323
324
  const timer = setTimeout(() => { controller.abort() }, timeoutMs)
324
325
  try {
325
- const response = await fetchImpl(NPM_REGISTRY + '/' + name.replace('/', '%2F') + '/latest', { signal: controller.signal })
326
+ const response = await fetchImpl(NPM_REGISTRY + '/' + name.replace('/', '%2F') + '/latest', withIdentityEncoding({ signal: controller.signal }))
326
327
  if (!response.ok) return undefined
327
328
  const body = await response.json()
328
329
  if (typeof body !== 'object' || body === null) return undefined
@@ -405,10 +406,10 @@ export async function fetchGitHubReleaseNotes(
405
406
  try {
406
407
  const response = await fetchImpl(
407
408
  `https://api.github.com/repos/${UPDATE_RELEASE_REPO}/releases/tags/v${version}`,
408
- {
409
+ withIdentityEncoding({
409
410
  headers: { accept: 'application/vnd.github+json', 'user-agent': 'dsh-web-update' },
410
411
  signal: controller.signal,
411
- },
412
+ }),
412
413
  )
413
414
  if (!response.ok) return undefined
414
415
  const body = await response.json()
@@ -558,11 +559,24 @@ export interface UpdateRunDeps {
558
559
  timeoutMs?: number
559
560
  /** Platform override (defaults to process.platform; test seam). */
560
561
  platform?: NodeJS.Platform
562
+ /** Process-group kill seam (defaults to process.kill; test seam). */
563
+ killImpl?: KillImpl
561
564
  }
562
565
 
563
566
  /** Cap on captured pnpm output (keeps error payloads bounded). */
564
567
  const OUTPUT_CAP = 16 * 1024
565
568
 
569
+ /**
570
+ * Characters a package name may contain before it is handed to the update
571
+ * command line. Deliberately looser than the strict family shape (the tests
572
+ * and future callers use short names) but it admits no whitespace, quotes,
573
+ * shell metacharacters, or path separators beyond the scope slash.
574
+ */
575
+ const SHELL_SAFE_PACKAGE_RE = /^@?[A-Za-z0-9][A-Za-z0-9._/-]*$/
576
+
577
+ /** Seam for the process-group kill (tests inject a recorder). */
578
+ export type KillImpl = (pid: number, signal: NodeJS.Signals) => void
579
+
566
580
  /**
567
581
  * Windows cmd command-not-found stderr. With shell:true a missing shim
568
582
  * exits with code 1 or 9009 (cmd cannot report ENOENT), so the fallback chain
@@ -572,14 +586,14 @@ const OUTPUT_CAP = 16 * 1024
572
586
  const WIN_CMD_MISSING_RE = /not recognized as an internal or external command|不是内部或外部命令/i
573
587
 
574
588
  /**
575
- * Decode process output with tolerant decoding:
576
- * Defaults to UTF-8; if invalid byte sequences are encountered (common on
577
- * Windows consoles using code page 936/GBK), falls back to the code page that
578
- * loses the least text. Bytes are decoded once per whole chunk by the caller's
579
- * accumulator — decoding per \`data\` event would split multi-byte characters.
589
+ * Fold one decoded candidate output into the running total under the display
590
+ * cap (the tail carries the failure reason).
591
+ * @param output - the accumulated text.
592
+ * @param text - the candidate's decoded text.
580
593
  */
581
- function decodeProcessChunk(chunk: Buffer): string {
582
- return decodeConsoleBytes(chunk)
594
+ function capOutput(output: string, text: string): string {
595
+ const combined = output + text
596
+ return combined.length > OUTPUT_CAP ? combined.slice(combined.length - OUTPUT_CAP) : combined
583
597
  }
584
598
 
585
599
  /**
@@ -603,16 +617,24 @@ const MIN_RELEASE_AGE_OVERRIDE = '--config.minimumReleaseAge=0'
603
617
  export function runUpdate(deps: UpdateRunDeps): Promise<UpdateRunResult> {
604
618
  return new Promise((resolve) => {
605
619
  const spawnImpl = deps.spawnImpl ?? spawn
606
- const packages = deps.packages
607
620
  const platform = deps.platform ?? process.platform
621
+ // The names come from the profile/anchor manifests and the win32 candidate
622
+ // runs through a shell, so anything outside the npm name shape is dropped
623
+ // instead of being concatenated into a command line (a dependency key
624
+ // containing shell metacharacters would otherwise run as a second command).
625
+ const packages = deps.packages.filter(name => SHELL_SAFE_PACKAGE_RE.test(name))
608
626
  // Windows ships pnpm/corepack/npx as .cmd shims; Node's spawn cannot
609
627
  // start .cmd files directly (ENOENT even when installed), so route
610
628
  // them through cmd.exe there. POSIX spawns stay shell-free.
629
+ // POSIX: the child leads its own process group (detached) so the timeout
630
+ // can signal pnpm's whole tree — pnpm spawns lifecycle-script children that
631
+ // a direct SIGTERM to the wrapper leaves running against the profile.
611
632
  const spawnOptions: Record<string, unknown> = {
612
633
  cwd: deps.profileDir,
613
634
  stdio: ['ignore', 'pipe', 'pipe'],
614
- ...(platform === 'win32' ? { shell: true } : {}),
635
+ ...(platform === 'win32' ? { shell: true } : { detached: true }),
615
636
  }
637
+ const killImpl: KillImpl = deps.killImpl ?? process.kill
616
638
  // Ordered fallback chain: each is tried only when the previous one is
617
639
  // missing on PATH (ENOENT on the spawn error event, never on close).
618
640
  // `--latest` is required: dsh plugin writes exact-version specs (e.g.
@@ -633,14 +655,21 @@ export function runUpdate(deps: UpdateRunDeps): Promise<UpdateRunResult> {
633
655
  // (its tail is subject to OUTPUT_CAP). The win32 missing-command test must
634
656
  // run against `currentOutput` so a previous candidate's 'not recognized'
635
657
  // stderr cannot misclassify a real failure in the next one.
658
+ // Each candidate accumulates raw bytes and is decoded exactly once
659
+ // (console-output.ts): decoding per `data` event splits a multi-byte
660
+ // character that straddles two reads into replacement characters, which is
661
+ // what the panel then shows as garbled pnpm diagnostics.
636
662
  let output = ''
637
- let currentOutput = ''
638
- const append = (chunk: Buffer): void => {
639
- const text = decodeProcessChunk(chunk)
640
- output += text
641
- if (output.length > OUTPUT_CAP) output = output.slice(output.length - OUTPUT_CAP)
642
- currentOutput += text
643
- if (currentOutput.length > OUTPUT_CAP) currentOutput = currentOutput.slice(currentOutput.length - OUTPUT_CAP)
663
+ let capture = createOutputCapture(OUTPUT_CAP)
664
+ let candidateFolded = false
665
+ const append = (chunk: Buffer): void => { capture.push(chunk) }
666
+ /** This candidate's decoded text (idempotent). */
667
+ const candidateText = (): string => capture.read()
668
+ /** Fold this candidate's text into the running output exactly once. */
669
+ const foldCandidate = (): void => {
670
+ if (candidateFolded) return
671
+ candidateFolded = true
672
+ output = capOutput(output, capture.read())
644
673
  }
645
674
  let currentChild: ReturnType<typeof spawnImpl> | undefined
646
675
  // Terminal guard: once a result is produced the promise is settled and no
@@ -661,14 +690,31 @@ export function runUpdate(deps: UpdateRunDeps): Promise<UpdateRunResult> {
661
690
  const pid = (currentChild as { pid?: number } | undefined)?.pid
662
691
  if (pid !== undefined && pid > 0) {
663
692
  try {
664
- spawnImpl('taskkill', ['/pid', String(pid), '/t', '/f'], { stdio: 'ignore' })
693
+ // A spawn failure arrives as an asynchronous 'error' event, which
694
+ // the try/catch cannot see; an emitter with no error listener throws
695
+ // and would take the host process down on this path.
696
+ const killer = spawnImpl('taskkill', ['/pid', String(pid), '/t', '/f'], { stdio: 'ignore' })
697
+ killer.on?.('error', () => {})
665
698
  } catch {
666
699
  // Best-effort kill; fall through to the timeout result.
667
700
  }
668
701
  }
669
702
  } else {
670
- currentChild?.kill('SIGTERM')
703
+ // Detached child: its process-group id is its pid, so the negative pid
704
+ // reaches pnpm's grandchildren too. A group kill throws when the child
705
+ // already exited; fall back to the direct kill.
706
+ const pid = (currentChild as { pid?: number } | undefined)?.pid
707
+ if (pid !== undefined && pid > 0) {
708
+ try {
709
+ killImpl(-pid, 'SIGTERM')
710
+ } catch {
711
+ try { currentChild?.kill('SIGTERM') } catch { /* already gone */ }
712
+ }
713
+ } else {
714
+ currentChild?.kill('SIGTERM')
715
+ }
671
716
  }
717
+ foldCandidate()
672
718
  finish({ ok: false, exitCode: null, output, error: 'update timed out; install process killed', errorCode: 'timeout' })
673
719
  }, deps.timeoutMs ?? 10 * 60_000)
674
720
  const runCandidate = (index: number): void => {
@@ -684,7 +730,8 @@ export function runUpdate(deps: UpdateRunDeps): Promise<UpdateRunResult> {
684
730
  return
685
731
  }
686
732
  const candidate = candidates[index]
687
- currentOutput = ''
733
+ capture = createOutputCapture(OUTPUT_CAP)
734
+ candidateFolded = false
688
735
  const child = spawnImpl(candidate.command, candidate.args, spawnOptions)
689
736
  currentChild = child
690
737
  let settled = false
@@ -697,6 +744,7 @@ export function runUpdate(deps: UpdateRunDeps): Promise<UpdateRunResult> {
697
744
  child.stderr?.on('data', append)
698
745
  child.on('error', (error: Error) => {
699
746
  once(() => {
747
+ foldCandidate()
700
748
  if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
701
749
  // Command missing: try the next candidate in the chain.
702
750
  runCandidate(index + 1)
@@ -711,11 +759,13 @@ export function runUpdate(deps: UpdateRunDeps): Promise<UpdateRunResult> {
711
759
  // 'not recognized' or localized message instead of ENOENT — keep the
712
760
  // chain going. Checked against this candidate's own output so a prior
713
761
  // fallback's stderr cannot misclassify a real failure here.
714
- if (platform === 'win32' && (code === 9009 || (code !== 0 && WIN_CMD_MISSING_RE.test(currentOutput)))) {
762
+ if (platform === 'win32' && (code === 9009 || (code !== 0 && WIN_CMD_MISSING_RE.test(candidateText())))) {
763
+ foldCandidate()
715
764
  runCandidate(index + 1)
716
765
  return
717
766
  }
718
767
  settled = true
768
+ foldCandidate()
719
769
  finish({
720
770
  ok: code === 0,
721
771
  exitCode: code,