windows-signing-state.mjs 3.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
  1. /** Persist a per-user hardware-signing interlock; failures and interrupted attempts never unlock automatically. */
  2. import { closeSync, existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'
  3. import { homedir } from 'node:os'
  4. import { join, isAbsolute } from 'node:path'
  5. import { randomUUID } from 'node:crypto'
  6. import { failPackagingRun, recordPackagingEvent } from './packaging-run.mjs'
  7. /**
  8. * Acquire the one hardware-signing attempt slot before launching SignTool.
  9. * @param {{runDirectory?: string, stateDirectory?: string, target: string}} options Run evidence and test-only isolated state directory.
  10. * @returns {{started: (pid: number|null) => void, success: () => void, failure: (code: number|string|null, diagnostic: string) => void}} Process evidence and completion callbacks; failure retains the interlock.
  11. */
  12. export function beginWindowsSigningAttempt(options) {
  13. const runDirectory = options.runDirectory ?? process.env.DSH_DESKTOP_PACKAGING_RUN_DIR
  14. if (!runDirectory || !isAbsolute(runDirectory) || !existsSync(join(runDirectory, 'run.json'))) {
  15. throw new Error('Windows hardware signing requires a supervised packaging run with retained records')
  16. }
  17. if (existsSync(join(runDirectory, 'fatal.json'))) throw new Error('Windows signing refused: packaging run already failed')
  18. const root = options.stateDirectory ?? join(homedir(), '.dsh-desktop-signing')
  19. const lock = join(root, 'attempt.json')
  20. const attemptId = randomUUID()
  21. mkdirSync(root, { recursive: true })
  22. let descriptor
  23. try { descriptor = openSync(lock, 'wx', 0o600) }
  24. catch (error) {
  25. failPackagingRun(runDirectory, 'hardware-signing-interlock-unavailable')
  26. throw new Error(`Windows signing refused: interlock unavailable at ${lock}; inspect the previous attempt before administrator-approved recovery (${error.code})`)
  27. }
  28. try {
  29. writeFileSync(descriptor, `${JSON.stringify({ attemptId, runDirectory, pid: process.pid, startedAt: new Date().toISOString(), target: options.target })}\n`, { flush: true })
  30. } finally { closeSync(descriptor) }
  31. try { recordPackagingEvent(runDirectory, { type: 'sign-start', attemptId, target: options.target }) }
  32. catch (error) { failPackagingRun(runDirectory, 'signing-audit-write-failed'); throw error }
  33. return {
  34. started(pid) {
  35. recordPackagingEvent(runDirectory, { type: 'sign-command-start', attemptId, commandPid: pid })
  36. },
  37. success() {
  38. recordPackagingEvent(runDirectory, { type: 'sign-success', attemptId, target: options.target })
  39. if (JSON.parse(readFileSync(lock, 'utf8')).attemptId !== attemptId) throw new Error('Windows signing interlock ownership changed')
  40. unlinkSync(lock)
  41. },
  42. failure(code, diagnostic) {
  43. // The attempt file stays in place even if recording the failure or notifying the parent fails.
  44. try { recordPackagingEvent(runDirectory, { type: 'sign-failure', attemptId, target: options.target, code, diagnostic }) }
  45. finally { failPackagingRun(runDirectory, 'hardware-signing-failed') }
  46. },
  47. }
  48. }