core-package-set.ts 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. /** Signed local npm package set that supplies the Desktop-owned dsh runtime and private Host. */
  2. import { createHash } from 'node:crypto'
  3. import { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs'
  4. import { join } from 'node:path'
  5. /** Descriptor copied beside every Desktop profile's local core tarballs. */
  6. export const DESKTOP_PACKAGE_SET_FILE = 'desktop-packages.json'
  7. /** Profile-relative directory containing immutable core npm tarballs. */
  8. export const DESKTOP_PACKAGES_DIR = 'desktop-packages'
  9. /** Private package installed beside dsh to boot the Desktop Host process. */
  10. export const DESKTOP_HOST_PACKAGE = '@deepseek-ai/dsh-desktop-host'
  11. /** Package-relative Desktop Host files required before a profile can boot. */
  12. export const DESKTOP_HOST_RUNTIME_FILES = [
  13. 'lib/index.js',
  14. 'config/desktop.cordis.patch.yml',
  15. ] as const
  16. /** One immutable npm tarball in the Desktop core package set. */
  17. export interface DesktopCorePackageRecord {
  18. readonly name: string
  19. readonly version: string
  20. readonly file: string
  21. readonly bytes: number
  22. readonly integrity: string
  23. }
  24. /** Complete union of the first-party package closures rooted at dsh and its private Desktop Host. */
  25. export interface DesktopCorePackageSet {
  26. readonly schemaVersion: 1
  27. readonly packages: readonly DesktopCorePackageRecord[]
  28. }
  29. const PACKAGE_NAME_PATTERN = /^(?:@[a-z0-9][a-z0-9._~-]*\/[a-z0-9][a-z0-9._~-]*|[a-z0-9][a-z0-9._~-]*)$/u
  30. const VERSION_PATTERN = /^[0-9A-Za-z][0-9A-Za-z.+_-]*$/u
  31. const FILE_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*\.tgz$/u
  32. const INTEGRITY_PATTERN = /^sha512-[A-Za-z0-9+/]+={0,2}$/u
  33. const DSH_PACKAGE = '@deepseek-ai/dsh'
  34. const RELEASE_PACKAGES = [DSH_PACKAGE, DESKTOP_HOST_PACKAGE] as const
  35. function isRecord(value: unknown): value is Record<string, unknown> {
  36. return typeof value === 'object' && value !== null && !Array.isArray(value)
  37. }
  38. /**
  39. * Validate package-set data read from a release artifact or active profile.
  40. * @param value - Parsed descriptor JSON.
  41. * @param expectedReleaseVersion - Required dsh and Desktop Host version when validating one release.
  42. * @returns The normalized package set in deterministic name order.
  43. */
  44. export function parseDesktopCorePackageSet(
  45. value: unknown,
  46. expectedReleaseVersion?: string,
  47. ): DesktopCorePackageSet {
  48. if (!isRecord(value) || value.schemaVersion !== 1 || !Array.isArray(value.packages)) {
  49. throw new Error('desktop package set: invalid descriptor')
  50. }
  51. const packages = value.packages.map((entry): DesktopCorePackageRecord => {
  52. if (!isRecord(entry) || typeof entry.name !== 'string' || !PACKAGE_NAME_PATTERN.test(entry.name)
  53. || typeof entry.version !== 'string' || !VERSION_PATTERN.test(entry.version)
  54. || typeof entry.file !== 'string' || !FILE_PATTERN.test(entry.file)
  55. || typeof entry.bytes !== 'number' || !Number.isSafeInteger(entry.bytes) || entry.bytes < 0
  56. || typeof entry.integrity !== 'string' || !INTEGRITY_PATTERN.test(entry.integrity)) {
  57. throw new Error('desktop package set: invalid package record')
  58. }
  59. return {
  60. name: entry.name,
  61. version: entry.version,
  62. file: entry.file,
  63. bytes: entry.bytes,
  64. integrity: entry.integrity,
  65. }
  66. })
  67. const names = new Set(packages.map(entry => entry.name))
  68. const files = new Set(packages.map(entry => entry.file))
  69. if (names.size !== packages.length || files.size !== packages.length) {
  70. throw new Error('desktop package set: duplicate package name or filename')
  71. }
  72. const sorted = [...packages].sort((left, right) => left.name.localeCompare(right.name))
  73. if (JSON.stringify(sorted) !== JSON.stringify(packages)) {
  74. throw new Error('desktop package set: packages must be sorted by name')
  75. }
  76. for (const name of RELEASE_PACKAGES) {
  77. const entry = packages.find(candidate => candidate.name === name)
  78. if (entry === undefined) throw new Error(`desktop package set: missing ${name}`)
  79. if (expectedReleaseVersion !== undefined && entry.version !== expectedReleaseVersion) {
  80. throw new Error(`desktop package set: ${name}@${entry.version} does not match Desktop ${expectedReleaseVersion}`)
  81. }
  82. }
  83. return { schemaVersion: 1, packages }
  84. }
  85. /** Read and structurally validate one profile's core package descriptor. */
  86. export function readDesktopCorePackageSet(projectDir: string, expectedReleaseVersion?: string): DesktopCorePackageSet {
  87. const path = join(projectDir, DESKTOP_PACKAGE_SET_FILE)
  88. let value: unknown
  89. try {
  90. value = JSON.parse(readFileSync(path, 'utf8'))
  91. } catch (error) {
  92. throw new Error(`desktop package set: failed to read ${path}: ${String(error)}`)
  93. }
  94. return parseDesktopCorePackageSet(value, expectedReleaseVersion)
  95. }
  96. /** Return the project-relative `file:` spec for one local core tarball. */
  97. export function desktopCorePackageSpec(record: DesktopCorePackageRecord): string {
  98. return `file:./${DESKTOP_PACKAGES_DIR}/${record.file}`
  99. }
  100. /** Return the exact pnpm override map that keeps every core package off registries. */
  101. export function desktopCorePackageOverrides(packageSet: DesktopCorePackageSet): Record<string, string> {
  102. return Object.fromEntries(packageSet.packages.map(record => [record.name, desktopCorePackageSpec(record)]))
  103. }
  104. /** Return the local direct dependency spec for the dsh package. */
  105. export function desktopDshPackageSpec(packageSet: DesktopCorePackageSet): string {
  106. const record = packageSet.packages.find(entry => entry.name === DSH_PACKAGE)
  107. if (record === undefined) throw new Error(`desktop package set: missing ${DSH_PACKAGE}`)
  108. return desktopCorePackageSpec(record)
  109. }
  110. /**
  111. * Verify every local tarball and reject extra package files before pnpm executes them.
  112. * @param projectDir - Seed or profile directory containing the package set.
  113. * @param expectedReleaseVersion - Exact dsh and Desktop Host version bound to Electron.
  114. * @returns The verified package set.
  115. */
  116. export function verifyDesktopCorePackageSet(
  117. projectDir: string,
  118. expectedReleaseVersion: string,
  119. ): DesktopCorePackageSet {
  120. const packageSet = readDesktopCorePackageSet(projectDir, expectedReleaseVersion)
  121. const packageDir = join(projectDir, DESKTOP_PACKAGES_DIR)
  122. const expectedFiles = packageSet.packages.map(entry => entry.file).sort()
  123. let actualFiles: string[]
  124. try {
  125. actualFiles = readdirSync(packageDir).sort()
  126. } catch (error) {
  127. throw new Error(`desktop package set: failed to read ${packageDir}: ${String(error)}`)
  128. }
  129. if (JSON.stringify(actualFiles) !== JSON.stringify(expectedFiles)) {
  130. throw new Error('desktop package set: package directory does not match its descriptor')
  131. }
  132. for (const record of packageSet.packages) {
  133. const path = join(packageDir, record.file)
  134. if (!existsSync(path) || !lstatSync(path).isFile()) {
  135. throw new Error(`desktop package set: ${record.file} is not a regular file`)
  136. }
  137. const body = readFileSync(path)
  138. const integrity = `sha512-${createHash('sha512').update(body).digest('base64')}`
  139. if (body.byteLength !== record.bytes || integrity !== record.integrity) {
  140. throw new Error(`desktop package set: integrity check failed for ${record.file}`)
  141. }
  142. }
  143. return packageSet
  144. }
  145. function escapeRegExp(value: string): string {
  146. return value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&')
  147. }
  148. /**
  149. * Reject a lockfile that resolved any packaged core name through a registry version.
  150. * @param lockfile - Generated pnpm lockfile text.
  151. * @param packageSet - Verified local core package set.
  152. */
  153. export function verifyDesktopCoreLockfile(
  154. lockfile: string,
  155. packageSet: DesktopCorePackageSet,
  156. ): void {
  157. for (const record of packageSet.packages) {
  158. const registryResolution = new RegExp(
  159. `^ ['"]?${escapeRegExp(record.name)}@${escapeRegExp(record.version)}(?:\\([^\\r\\n]*\\))?['"]?:`,
  160. 'mu',
  161. )
  162. if (registryResolution.test(lockfile)) {
  163. throw new Error(`desktop package set: lockfile resolved ${record.name}@${record.version} outside the local package set`)
  164. }
  165. }
  166. }