@dsh-enhanced/plugin-control-plane 0.1.48 → 0.1.49

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 (167) hide show
  1. package/README.md +183 -15
  2. package/bin/dsh-live-qualification-authority.js +15 -0
  3. package/bin/dsh-local-release-adapter.js +328 -8
  4. package/bin/dsh-systemd-host-attestor.js +359 -9
  5. package/bin/dsh-systemd-host-authority.js +12 -0
  6. package/lib/adoption-coordinator.d.ts +3 -1
  7. package/lib/adoption-coordinator.d.ts.map +1 -1
  8. package/lib/adoption-coordinator.js +24 -9
  9. package/lib/adoption-coordinator.js.map +1 -1
  10. package/lib/cli.d.ts.map +1 -1
  11. package/lib/cli.js +33 -14
  12. package/lib/cli.js.map +1 -1
  13. package/lib/creation-capability-feedback.d.ts +23 -0
  14. package/lib/creation-capability-feedback.d.ts.map +1 -0
  15. package/lib/creation-capability-feedback.js +132 -0
  16. package/lib/creation-capability-feedback.js.map +1 -0
  17. package/lib/creation-capability-journal.d.ts +66 -0
  18. package/lib/creation-capability-journal.d.ts.map +1 -0
  19. package/lib/creation-capability-journal.js +751 -0
  20. package/lib/creation-capability-journal.js.map +1 -0
  21. package/lib/creation-capability-runtime.d.ts +63 -0
  22. package/lib/creation-capability-runtime.d.ts.map +1 -0
  23. package/lib/creation-capability-runtime.js +564 -0
  24. package/lib/creation-capability-runtime.js.map +1 -0
  25. package/lib/creation-capability-source.d.ts +37 -0
  26. package/lib/creation-capability-source.d.ts.map +1 -0
  27. package/lib/creation-capability-source.js +328 -0
  28. package/lib/creation-capability-source.js.map +1 -0
  29. package/lib/creation-capability-types.d.ts +252 -0
  30. package/lib/creation-capability-types.d.ts.map +1 -0
  31. package/lib/creation-capability-types.js +2 -0
  32. package/lib/creation-capability-types.js.map +1 -0
  33. package/lib/deployment-readiness.d.ts +16 -0
  34. package/lib/deployment-readiness.d.ts.map +1 -1
  35. package/lib/deployment-readiness.js +48 -1
  36. package/lib/deployment-readiness.js.map +1 -1
  37. package/lib/effect-blocked-replay.js +2 -2
  38. package/lib/effect-blocked-replay.js.map +1 -1
  39. package/lib/foreground-deployment-runtime.d.ts +3 -0
  40. package/lib/foreground-deployment-runtime.d.ts.map +1 -1
  41. package/lib/foreground-deployment-runtime.js +55 -13
  42. package/lib/foreground-deployment-runtime.js.map +1 -1
  43. package/lib/foreground-deployment.d.ts.map +1 -1
  44. package/lib/foreground-deployment.js +5 -0
  45. package/lib/foreground-deployment.js.map +1 -1
  46. package/lib/host-attestor.d.ts +2 -0
  47. package/lib/host-attestor.d.ts.map +1 -1
  48. package/lib/host-attestor.js +8 -1
  49. package/lib/host-attestor.js.map +1 -1
  50. package/lib/host-inputs.d.ts +13 -0
  51. package/lib/host-inputs.d.ts.map +1 -0
  52. package/lib/host-inputs.js +162 -0
  53. package/lib/host-inputs.js.map +1 -0
  54. package/lib/host-maintenance.d.ts +61 -0
  55. package/lib/host-maintenance.d.ts.map +1 -0
  56. package/lib/host-maintenance.js +158 -0
  57. package/lib/host-maintenance.js.map +1 -0
  58. package/lib/host-readiness.d.ts +5 -0
  59. package/lib/host-readiness.d.ts.map +1 -0
  60. package/lib/host-readiness.js +22 -0
  61. package/lib/host-readiness.js.map +1 -0
  62. package/lib/index.d.ts +16 -0
  63. package/lib/index.d.ts.map +1 -1
  64. package/lib/index.js +7 -0
  65. package/lib/index.js.map +1 -1
  66. package/lib/live-qualification-authority.d.ts +32 -0
  67. package/lib/live-qualification-authority.d.ts.map +1 -0
  68. package/lib/live-qualification-authority.js +280 -0
  69. package/lib/live-qualification-authority.js.map +1 -0
  70. package/lib/live-qualification-client.d.ts +4 -0
  71. package/lib/live-qualification-client.d.ts.map +1 -0
  72. package/lib/live-qualification-client.js +22 -0
  73. package/lib/live-qualification-client.js.map +1 -0
  74. package/lib/live-qualification-runtime.d.ts +78 -0
  75. package/lib/live-qualification-runtime.d.ts.map +1 -0
  76. package/lib/live-qualification-runtime.js +367 -0
  77. package/lib/live-qualification-runtime.js.map +1 -0
  78. package/lib/live-qualification.d.ts +73 -0
  79. package/lib/live-qualification.d.ts.map +1 -0
  80. package/lib/live-qualification.js +164 -0
  81. package/lib/live-qualification.js.map +1 -0
  82. package/lib/owner-task-gaps.d.ts +2 -0
  83. package/lib/owner-task-gaps.d.ts.map +1 -1
  84. package/lib/owner-task-gaps.js +9 -1
  85. package/lib/owner-task-gaps.js.map +1 -1
  86. package/lib/runtime-epoch.d.ts +69 -0
  87. package/lib/runtime-epoch.d.ts.map +1 -0
  88. package/lib/runtime-epoch.js +139 -0
  89. package/lib/runtime-epoch.js.map +1 -0
  90. package/lib/runtime-observer.d.ts.map +1 -1
  91. package/lib/runtime-observer.js +42 -6
  92. package/lib/runtime-observer.js.map +1 -1
  93. package/lib/service.d.ts +247 -4
  94. package/lib/service.d.ts.map +1 -1
  95. package/lib/service.js +1332 -27
  96. package/lib/service.js.map +1 -1
  97. package/lib/source-adoption-authority.d.ts +3 -0
  98. package/lib/source-adoption-authority.d.ts.map +1 -1
  99. package/lib/source-adoption-authority.js +13 -3
  100. package/lib/source-adoption-authority.js.map +1 -1
  101. package/lib/source-adoption-runner.d.ts +3 -0
  102. package/lib/source-adoption-runner.d.ts.map +1 -1
  103. package/lib/source-adoption-runner.js +16 -4
  104. package/lib/source-adoption-runner.js.map +1 -1
  105. package/lib/source-baseline.d.ts +37 -0
  106. package/lib/source-baseline.d.ts.map +1 -0
  107. package/lib/source-baseline.js +250 -0
  108. package/lib/source-baseline.js.map +1 -0
  109. package/lib/source-build.d.ts +17 -1
  110. package/lib/source-build.d.ts.map +1 -1
  111. package/lib/source-build.js +501 -7
  112. package/lib/source-build.js.map +1 -1
  113. package/lib/source-context.d.ts +18 -0
  114. package/lib/source-context.d.ts.map +1 -1
  115. package/lib/source-context.js +85 -11
  116. package/lib/source-context.js.map +1 -1
  117. package/lib/source-creation.d.ts +106 -0
  118. package/lib/source-creation.d.ts.map +1 -0
  119. package/lib/source-creation.js +664 -0
  120. package/lib/source-creation.js.map +1 -0
  121. package/lib/source-job-types.d.ts +19 -0
  122. package/lib/source-job-types.d.ts.map +1 -1
  123. package/lib/source-jobs.d.ts +30 -3
  124. package/lib/source-jobs.d.ts.map +1 -1
  125. package/lib/source-jobs.js +382 -40
  126. package/lib/source-jobs.js.map +1 -1
  127. package/lib/source-maintenance.d.ts +66 -0
  128. package/lib/source-maintenance.d.ts.map +1 -0
  129. package/lib/source-maintenance.js +177 -0
  130. package/lib/source-maintenance.js.map +1 -0
  131. package/lib/source-revision.d.ts +57 -0
  132. package/lib/source-revision.d.ts.map +1 -0
  133. package/lib/source-revision.js +175 -0
  134. package/lib/source-revision.js.map +1 -0
  135. package/lib/source-workspace.d.ts +19 -14
  136. package/lib/source-workspace.d.ts.map +1 -1
  137. package/lib/source-workspace.js +90 -17
  138. package/lib/source-workspace.js.map +1 -1
  139. package/lib/sqlite.d.ts +13 -3
  140. package/lib/sqlite.d.ts.map +1 -1
  141. package/lib/sqlite.js +362 -1
  142. package/lib/sqlite.js.map +1 -1
  143. package/lib/store.d.ts +258 -3
  144. package/lib/store.d.ts.map +1 -1
  145. package/lib/store.js +2211 -95
  146. package/lib/store.js.map +1 -1
  147. package/lib/systemd-host-authority.d.ts +68 -0
  148. package/lib/systemd-host-authority.d.ts.map +1 -0
  149. package/lib/systemd-host-authority.js +424 -0
  150. package/lib/systemd-host-authority.js.map +1 -0
  151. package/lib/task-observation-evidence.d.ts +32 -0
  152. package/lib/task-observation-evidence.d.ts.map +1 -0
  153. package/lib/task-observation-evidence.js +62 -0
  154. package/lib/task-observation-evidence.js.map +1 -0
  155. package/lib/task-observation-runtime.d.ts +2 -3
  156. package/lib/task-observation-runtime.d.ts.map +1 -1
  157. package/lib/task-observation-runtime.js +7 -39
  158. package/lib/task-observation-runtime.js.map +1 -1
  159. package/lib/task-observation-store.js +1 -1
  160. package/lib/trust.d.ts.map +1 -1
  161. package/lib/trust.js +0 -0
  162. package/lib/trust.js.map +1 -1
  163. package/lib/types.d.ts +43 -6
  164. package/lib/types.d.ts.map +1 -1
  165. package/lib/version.d.ts +1 -1
  166. package/lib/version.js +1 -1
  167. package/package.json +21 -14
@@ -25,6 +25,7 @@ import {
25
25
  existsSync,
26
26
  fsyncSync,
27
27
  lstatSync,
28
+ mkdtempSync,
28
29
  mkdirSync,
29
30
  openSync,
30
31
  readFileSync,
@@ -33,6 +34,7 @@ import {
33
34
  readdirSync,
34
35
  readSync,
35
36
  renameSync,
37
+ rmdirSync,
36
38
  rmSync,
37
39
  fstatSync,
38
40
  statSync,
@@ -43,6 +45,7 @@ import {
43
45
  } from 'node:fs'
44
46
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
45
47
  import { fileURLToPath, pathToFileURL } from 'node:url'
48
+ import { gunzipSync, gzipSync } from 'node:zlib'
46
49
 
47
50
  export const LOCAL_RELEASE_ADAPTER_VERSION = 'dsh-local-release-adapter-1'
48
51
  const PHASES = new Set(['pr', 'review', 'merge', 'build', 'sign', 'publish', 'registry-verify', 'catalog-admission'])
@@ -411,7 +414,8 @@ function catalogConfig(value, npm = false) {
411
414
  }
412
415
  function buildConfig(value) {
413
416
  const item = object(value, 'build config')
414
- exactKeys(item, ['sandboxExecutable', 'tarExecutable', 'nodeExecutable', 'pnpmExecutable', 'pnpmRoot', 'storeRoot'], 'build config')
417
+ const expected = ['sandboxExecutable', 'tarExecutable', 'nodeExecutable', 'pnpmExecutable', 'pnpmRoot', 'storeRoot']
418
+ exactKeys(item, item.cacheRoot === undefined ? expected : [...expected, 'cacheRoot'], 'build config')
415
419
  const sandboxExecutable = inspectExecutable(item.sandboxExecutable, 'build sandbox')
416
420
  const tarExecutable = inspectExecutable(item.tarExecutable, 'tar')
417
421
  if (sandboxExecutable.path !== '/usr/bin/bwrap') fail('local build adapter requires the pinned Linux bubblewrap sandbox')
@@ -419,6 +423,7 @@ function buildConfig(value) {
419
423
  const pnpmExecutable = inspectExecutable(item.pnpmExecutable, 'pnpm executable')
420
424
  const pnpmRoot = pinnedDirectory(item.pnpmRoot, 'pnpm root')
421
425
  const storeRoot = pinnedDirectory(item.storeRoot, 'pnpm store')
426
+ const cacheRoot = item.cacheRoot === undefined ? undefined : pinnedDirectory(item.cacheRoot, 'pnpm cache')
422
427
  if (nodeExecutable.path !== join(pnpmRoot.path, 'node')) fail('Node executable must be the pinned pnpm root node entrypoint')
423
428
  if (pnpmExecutable.path !== join(pnpmRoot.path, 'pnpm')) fail('pnpm executable must be the pinned pnpm root entrypoint')
424
429
  let pnpmManifest
@@ -428,14 +433,40 @@ function buildConfig(value) {
428
433
  const storeVersion = Number.parseInt(pnpmVersion.split('.')[0], 10)
429
434
  const projectsPath = join(storeRoot.path, `v${storeVersion}`, 'projects')
430
435
  if (!existsSync(projectsPath) || !lstatSync(projectsPath).isDirectory()) fail('pnpm store project registry is unavailable')
431
- return { sandboxExecutable, tarExecutable, nodeExecutable, pnpmExecutable, pnpmRoot, storeRoot, storeVersion }
436
+ return { sandboxExecutable, tarExecutable, nodeExecutable, pnpmExecutable, pnpmRoot, storeRoot, cacheRoot, storeVersion }
432
437
  }
433
438
  function verifyBuildPins(build) {
434
- for (const [item, label] of [[build.pnpmRoot, 'pnpm root'], [build.storeRoot, 'pnpm store']]) {
439
+ const pinned = [[build.pnpmRoot, 'pnpm root'], [build.storeRoot, 'pnpm store']]
440
+ if (build.cacheRoot !== undefined) pinned.push([build.cacheRoot, 'pnpm cache'])
441
+ for (const [item, label] of pinned) {
435
442
  if (directoryInventory(item.path, label).sha256 !== item.sha256) fail(`${label} changed after configuration validation`)
436
443
  }
437
444
  }
438
445
 
446
+ export function inspectLocalReleaseBuildEnvironment(value) {
447
+ const input = object(value, 'local release build environment')
448
+ exactKeys(input, input.cacheRoot === undefined ? ['pnpmRoot', 'storeRoot'] : ['pnpmRoot', 'storeRoot', 'cacheRoot'],
449
+ 'local release build environment')
450
+ const executable = (path, label) => {
451
+ const pin = { path, sha256: sha256Bytes(stableBytes(path, label)) }
452
+ return inspectExecutable(pin, label)
453
+ }
454
+ const pnpmRoot = directoryInventory(input.pnpmRoot, 'pnpm root')
455
+ const storeRoot = directoryInventory(input.storeRoot, 'pnpm store')
456
+ const cacheRoot = input.cacheRoot === undefined ? undefined : directoryInventory(input.cacheRoot, 'pnpm cache')
457
+ const pins = {
458
+ sandboxExecutable: executable('/usr/bin/bwrap', 'build sandbox'),
459
+ tarExecutable: executable('/usr/bin/tar', 'tar'),
460
+ nodeExecutable: executable(join(pnpmRoot.path, 'node'), 'build Node executable'),
461
+ pnpmExecutable: executable(join(pnpmRoot.path, 'pnpm'), 'pnpm executable'),
462
+ pnpmRoot: { path: pnpmRoot.path, sha256: pnpmRoot.sha256 },
463
+ storeRoot: { path: storeRoot.path, sha256: storeRoot.sha256 },
464
+ ...(cacheRoot === undefined ? {} : { cacheRoot: { path: cacheRoot.path, sha256: cacheRoot.sha256 } }),
465
+ }
466
+ buildConfig(pins)
467
+ return pins
468
+ }
469
+
439
470
  function command(executable, args, cwd, extraEnvironment = {}, input, inherited = [], hooks = {}) {
440
471
  if (process.platform !== 'linux' || !existsSync('/proc/self/fd')) fail('descriptor-pinned commands require Linux /proc/self/fd')
441
472
  const pinned = openPinnedFile(executable, `pinned ${basename(executable.path)} executable`, MAX_ARTIFACT_BYTES, true)
@@ -488,14 +519,17 @@ function openSandboxContext(build, workspace, output, runRoot) {
488
519
  const storeProjects = join(runRoot, 'store-projects')
489
520
  if (!existsSync(storeProjects)) mkdirSync(storeProjects, { mode: 0o700 })
490
521
  const toolchainSnapshot = join(runRoot, 'toolchain-snapshot'); const storeSnapshot = join(runRoot, 'store-snapshot')
522
+ const cacheSnapshot = build.cacheRoot === undefined ? undefined : join(runRoot, 'cache-snapshot')
491
523
  if (!existsSync(toolchainSnapshot)) copyPinnedTree(build.pnpmRoot, toolchainSnapshot, 'pnpm root')
492
524
  if (!existsSync(storeSnapshot)) copyPinnedTree(build.storeRoot, storeSnapshot, 'pnpm store')
525
+ if (cacheSnapshot !== undefined && !existsSync(cacheSnapshot)) copyPinnedTree(build.cacheRoot, cacheSnapshot, 'pnpm cache')
493
526
  const pnpmRoot = openPinnedDirectory({ path: toolchainSnapshot, sha256: build.pnpmRoot.sha256 }, 'pnpm snapshot')
494
527
  const storeRoot = openPinnedDirectory({ path: storeSnapshot, sha256: build.storeRoot.sha256 }, 'pnpm store snapshot')
495
528
  const workspaceRoot = openPinnedDirectory({ path: workspace }, 'build workspace')
496
529
  const outputRoot = openPinnedDirectory({ path: output }, 'build output')
497
530
  const storeProjectsRoot = openPinnedDirectory({ path: storeProjects }, 'pnpm project registry')
498
531
  const mounts = [pnpmRoot, storeRoot, workspaceRoot, outputRoot, storeProjectsRoot]
532
+ if (cacheSnapshot !== undefined) mounts.push(openPinnedDirectory({ path: cacheSnapshot, sha256: build.cacheRoot.sha256 }, 'pnpm cache snapshot'))
499
533
  return { mounts, runRoot }
500
534
  }
501
535
  function closeSandboxContext(context) { for (const item of context.mounts) closePinnedDirectory(item) }
@@ -505,15 +539,92 @@ function sandboxCommand(build, args, context, hooks = {}) {
505
539
  const mountFd = index => String(3 + index)
506
540
  const sandboxArgs = ['--unshare-all', '--die-with-parent', '--new-session', '--clearenv',
507
541
  '--ro-bind', '/usr', '/usr', '--ro-bind', '/lib', '/lib', '--ro-bind', '/lib64', '/lib64',
542
+ '--symlink', 'usr/bin', '/bin',
508
543
  '--ro-bind-fd', mountFd(0), '/toolchain', '--ro-bind-fd', mountFd(1), '/store',
544
+ ...(build.cacheRoot === undefined ? [] : ['--ro-bind-fd', mountFd(5), '/cache']),
509
545
  '--bind-fd', mountFd(4), `/store/v${build.storeVersion}/projects`, '--proc', '/proc', '--dev', '/dev', '--tmpfs', '/tmp', '--dir', '/home',
510
546
  '--bind-fd', mountFd(2), '/workspace', '--bind-fd', mountFd(3), '/output',
511
547
  '--chdir', '/workspace', '--setenv', 'HOME', '/home', '--setenv', 'TMPDIR', '/tmp', '--setenv', 'SOURCE_DATE_EPOCH', '0',
512
- '--setenv', 'PATH', '/toolchain:/usr/bin:/bin', '/toolchain/pnpm', ...args]
548
+ '--setenv', 'PATH', '/toolchain:/usr/bin:/bin',
549
+ ...(build.cacheRoot === undefined ? [] : ['--setenv', 'pnpm_config_cache_dir', '/cache']),
550
+ '/toolchain/pnpm', ...args]
513
551
  command(build.sandboxExecutable, sandboxArgs, runRoot, {}, undefined, mounts.map(item => item.descriptor),
514
552
  { beforeSpawn: hooks.beforeSandboxSpawn, afterSpawn: hooks.afterSandboxSpawn, afterFinally: hooks.afterSandboxFinally })
515
553
  for (const item of mounts) verifyPinnedDirectory(item)
516
554
  }
555
+
556
+ /** Build one frozen local cohort with the release adapter's pinned, offline
557
+ * sandbox. The caller supplies private staging directories, never a live Home.
558
+ * All candidate code sees is a copy without Git metadata or installed modules. */
559
+ export function buildLocalUpdateCohort(value) {
560
+ const input = object(value, 'local update cohort')
561
+ exactKeys(input, ['build', 'workspace', 'output', 'packagePaths'], 'local update cohort')
562
+ const build = buildConfig(input.build)
563
+ const workspace = canonicalPath(input.workspace, 'local update workspace')
564
+ const output = canonicalPath(input.output, 'local update output')
565
+ if (workspace === output || dirname(workspace) !== dirname(output)) fail('local update workspace and output must be private siblings')
566
+ privateDirectory(dirname(workspace), 'local update staging parent')
567
+ privateDirectory(workspace, 'local update workspace')
568
+ privateDirectory(output, 'local update output')
569
+ if (readdirSync(output).length !== 0) fail('local update output must be empty')
570
+ const outputIdentity = statSync(output, { bigint: true })
571
+ if (!Array.isArray(input.packagePaths) || input.packagePaths.length < 1 || input.packagePaths.length > 128) {
572
+ fail('local update package paths exceed their bound')
573
+ }
574
+ const paths = input.packagePaths.map(path => text(path, 'local update package path', /^(?:plugins|packages)\/[a-z0-9][a-z0-9-]{0,63}$/u))
575
+ const slugs = paths.map(path => basename(path))
576
+ if (new Set(paths).size !== paths.length || new Set(slugs).size !== slugs.length) fail('local update package paths are ambiguous')
577
+ verifyBuildPins(build)
578
+ const runRoot = mkdtempSync(join(dirname(workspace), '.local-cohort-build-'))
579
+ try {
580
+ chmodSync(runRoot, 0o700)
581
+ const isolatedWorkspace = join(runRoot, 'workspace')
582
+ copyBuildWorkspace(workspace, isolatedWorkspace)
583
+ normalizeBuildBinModes(isolatedWorkspace)
584
+ const sourceInputs = snapshotBuildInputs(isolatedWorkspace)
585
+ for (const path of paths) {
586
+ const packageRoot = join(isolatedWorkspace, path)
587
+ if (!existsSync(packageRoot) || !lstatSync(packageRoot).isDirectory()
588
+ || realpathSync(packageRoot) !== resolve(packageRoot)) fail(`local update package is unavailable: ${path}`)
589
+ }
590
+ const packed = join(runRoot, 'packed'); mkdirSync(packed, { mode: 0o700 })
591
+ for (const slug of slugs) mkdirSync(join(packed, slug), { mode: 0o700 })
592
+ const sandbox = openSandboxContext(build, isolatedWorkspace, packed, runRoot)
593
+ try {
594
+ sandboxCommand(build, ['install', '--offline', '--frozen-lockfile', '--frozen-store', '--ignore-scripts',
595
+ '--package-import-method=copy', '--store-dir=/store'], sandbox)
596
+ sandboxCommand(build, ['--workspace-root', '--if-present', 'run', 'build'], sandbox)
597
+ for (const [index, path] of paths.entries()) {
598
+ const packageRoot = join(isolatedWorkspace, path)
599
+ if (!lstatSync(packageRoot).isDirectory() || realpathSync(packageRoot) !== resolve(packageRoot)) {
600
+ fail(`local update package changed before pack: ${path}`)
601
+ }
602
+ sandboxCommand(build, ['--dir', `/workspace/${path}`, 'pack', '--pack-destination', `/output/${slugs[index]}`], sandbox)
603
+ }
604
+ } finally { closeSandboxContext(sandbox) }
605
+ verifyBuildInputs(isolatedWorkspace, sourceInputs)
606
+ verifyBuildPins(build)
607
+ if (readdirSync(packed).sort().join('\0') !== [...slugs].sort().join('\0')) fail('local update pack output has unexpected directories')
608
+ let totalBytes = 0
609
+ for (const slug of slugs) {
610
+ const directory = join(packed, slug)
611
+ privateDirectory(directory, `local update ${slug} output`)
612
+ const names = readdirSync(directory)
613
+ if (names.length !== 1 || !names[0].endsWith('.tgz')) fail(`local update pack output is ambiguous: ${slug}`)
614
+ const tarball = safeRegularFile(join(directory, names[0]), `local update ${slug} tarball`)
615
+ totalBytes += lstatSync(tarball).size
616
+ if (totalBytes > 2_147_483_648) fail('local update cohort output exceeds its bound')
617
+ }
618
+ // Output is a private, empty preparation directory; make the complete
619
+ // validated set visible in one rename, or leave it absent on failure.
620
+ const currentOutput = lstatSync(output, { bigint: true })
621
+ if (!currentOutput.isDirectory() || currentOutput.dev !== outputIdentity.dev
622
+ || currentOutput.ino !== outputIdentity.ino || readdirSync(output).length !== 0) fail('local update output changed during build')
623
+ rmdirSync(output)
624
+ renameSync(packed, output)
625
+ fsyncDirectory(dirname(output))
626
+ } finally { rmSync(runRoot, { recursive: true, force: true }) }
627
+ }
517
628
  function git(gitValue, args, cwd, environment = {}, hooks = {}) {
518
629
  return command(gitValue.executable, [...SAFE_GIT_CONFIG, ...args], cwd,
519
630
  { GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: '/dev/null', ...environment }, undefined, [],
@@ -964,6 +1075,92 @@ function copyBuildWorkspace(source, destination) {
964
1075
  }
965
1076
  inspect(destination)
966
1077
  }
1078
+ function normalizeBuildBinModes(root) {
1079
+ for (const collection of ['plugins', 'packages']) {
1080
+ const collectionRoot = join(root, collection)
1081
+ if (!existsSync(collectionRoot)) continue
1082
+ for (const name of readdirSync(collectionRoot)) {
1083
+ const packageRoot = join(collectionRoot, name)
1084
+ if (!lstatSync(packageRoot).isDirectory()) continue
1085
+ const manifestPath = join(packageRoot, 'package.json')
1086
+ if (!existsSync(manifestPath)) continue
1087
+ const manifestFile = lstatSync(manifestPath)
1088
+ if (!manifestFile.isFile() || manifestFile.size > MAX_INPUT_BYTES) fail('local update package manifest is unsafe')
1089
+ let manifest
1090
+ try { manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) }
1091
+ catch { fail('local update package manifest is invalid') }
1092
+ if (manifest === null || typeof manifest !== 'object' || Array.isArray(manifest)) fail('local update package manifest is invalid')
1093
+ const bin = manifest.bin
1094
+ if (bin === undefined) continue
1095
+ const binPaths = typeof bin === 'string' ? [bin]
1096
+ : bin !== null && typeof bin === 'object' && !Array.isArray(bin) ? Object.values(bin) : null
1097
+ if (binPaths === null) fail('local update package bin is invalid')
1098
+ for (const binPath of binPaths) {
1099
+ if (typeof binPath !== 'string' || binPath.length === 0 || binPath.length > 2_000
1100
+ || binPath.includes('\0') || binPath.includes('\\') || isAbsolute(binPath)
1101
+ || binPath.split('/').includes('..')) fail('local update bin path is invalid')
1102
+ const path = resolve(packageRoot, binPath)
1103
+ const inside = relative(packageRoot, path)
1104
+ if (!inside || inside === '..' || inside.startsWith(`..${sep}`) || isAbsolute(inside)) fail('local update bin path escapes its package')
1105
+ const parts = inside.split(sep)
1106
+ let current = packageRoot
1107
+ let missing = false
1108
+ for (const [index, part] of parts.entries()) {
1109
+ current = join(current, part)
1110
+ let metadata
1111
+ try { metadata = lstatSync(current) }
1112
+ catch (error) {
1113
+ if (error.code !== 'ENOENT') throw error
1114
+ missing = true
1115
+ break
1116
+ }
1117
+ if (metadata.isSymbolicLink() || (index === parts.length - 1 ? !metadata.isFile() : !metadata.isDirectory())) {
1118
+ fail('local update bin path is unsafe')
1119
+ }
1120
+ }
1121
+ if (missing) continue
1122
+ if (realpathSync(path) !== path) fail('local update bin path is unsafe')
1123
+ chmodSync(path, 0o755)
1124
+ }
1125
+ }
1126
+ }
1127
+ }
1128
+ function snapshotBuildInputs(root) {
1129
+ const directories = [], files = []
1130
+ let entries = 0, bytes = 0
1131
+ const visit = (path, relativePath = '') => {
1132
+ const metadata = lstatSync(path)
1133
+ if (++entries > 100_000) fail('local update source has too many entries')
1134
+ if (metadata.isDirectory() && !metadata.isSymbolicLink()) {
1135
+ directories.push({ path: relativePath, mode: metadata.mode & 0o777 })
1136
+ for (const name of readdirSync(path).sort()) visit(join(path, name), relativePath ? `${relativePath}/${name}` : name)
1137
+ } else if (metadata.isFile() && !metadata.isSymbolicLink()) {
1138
+ bytes += metadata.size
1139
+ if (metadata.size > MAX_ARTIFACT_BYTES || bytes > 4_294_967_296) fail('local update source exceeds its size bound')
1140
+ files.push({ path: relativePath, mode: metadata.mode & 0o777, size: metadata.size,
1141
+ sha256: sha256Bytes(readFileSync(path)) })
1142
+ } else fail('local update source contains an unsupported entry')
1143
+ }
1144
+ visit(root)
1145
+ return { directories, files }
1146
+ }
1147
+ function verifyBuildInputs(root, before) {
1148
+ for (const item of before.directories) {
1149
+ const path = item.path ? join(root, item.path) : root
1150
+ const metadata = lstatSync(path)
1151
+ if (!metadata.isDirectory() || metadata.isSymbolicLink() || (metadata.mode & 0o777) !== item.mode) {
1152
+ fail(`local update source directory changed during build: ${item.path}`)
1153
+ }
1154
+ }
1155
+ for (const item of before.files) {
1156
+ const metadata = lstatSync(join(root, item.path))
1157
+ if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size !== item.size
1158
+ || (metadata.mode & 0o777) !== item.mode
1159
+ || sha256Bytes(readFileSync(join(root, item.path))) !== item.sha256) {
1160
+ fail(`local update source file changed during build: ${item.path}`)
1161
+ }
1162
+ }
1163
+ }
967
1164
  function fileInventory(root) {
968
1165
  const result = []
969
1166
  const visit = (directory, prefix = '') => {
@@ -976,6 +1173,92 @@ function fileInventory(root) {
976
1173
  }
977
1174
  visit(root); return result
978
1175
  }
1176
+ const TAR_BLOCK_BYTES = 512
1177
+ const DEPENDENCY_MAPS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']
1178
+ function tarOctal(header, start, length, label) {
1179
+ const field = header.subarray(start, start + length)
1180
+ if (field.some(byte => byte !== 0 && byte !== 32 && (byte < 48 || byte > 55))) {
1181
+ fail(`${label} uses unsupported binary or non-octal tar encoding`)
1182
+ }
1183
+ const octal = field.toString('ascii').replaceAll('\0', ' ').trim()
1184
+ if (!/^[0-7]+$/u.test(octal)) fail(`${label} is not an octal tar field`)
1185
+ const value = Number.parseInt(octal, 8)
1186
+ if (!Number.isSafeInteger(value)) fail(`${label} exceeds the safe tar size`)
1187
+ return value
1188
+ }
1189
+ function tarString(header, start, length) {
1190
+ const field = header.subarray(start, start + length)
1191
+ const terminator = field.indexOf(0)
1192
+ if (terminator !== -1 && field.subarray(terminator).some(byte => byte !== 0)) fail('tar pathname field has nonzero padding')
1193
+ return field.subarray(0, terminator === -1 ? field.length : terminator).toString('utf8')
1194
+ }
1195
+
1196
+ export function normalizePackedDependencyOrder(packed) {
1197
+ if (!Buffer.isBuffer(packed) || packed.length < 1 || packed.length > MAX_ARTIFACT_BYTES) fail('packed archive size is invalid')
1198
+ let archive
1199
+ try { archive = gunzipSync(packed, { maxOutputLength: MAX_ARTIFACT_BYTES }) }
1200
+ catch { fail('packed archive is invalid or exceeds the extraction limit') }
1201
+ if (archive.length < TAR_BLOCK_BYTES * 3 || archive.length % TAR_BLOCK_BYTES !== 0) fail('packed tar block layout is invalid')
1202
+ let manifestOffset; let manifestSize; let offset = 0; let terminated = false
1203
+ while (offset < archive.length) {
1204
+ const header = archive.subarray(offset, offset + TAR_BLOCK_BYTES)
1205
+ if (header.every(byte => byte === 0)) {
1206
+ if (archive.length - offset < TAR_BLOCK_BYTES * 2 || archive.subarray(offset).some(byte => byte !== 0)) {
1207
+ fail('packed tar terminator is invalid')
1208
+ }
1209
+ terminated = true; break
1210
+ }
1211
+ const expectedChecksum = tarOctal(header, 148, 8, 'tar header checksum')
1212
+ let checksum = 0
1213
+ for (let index = 0; index < TAR_BLOCK_BYTES; index += 1) checksum += index >= 148 && index < 156 ? 32 : header[index]
1214
+ if (checksum !== expectedChecksum) fail('packed tar header checksum is invalid')
1215
+ const size = tarOctal(header, 124, 12, 'tar entry size')
1216
+ const type = header[156]
1217
+ if (type !== 0 && type !== 48 && type !== 53) fail('packed tar uses an unsupported entry representation')
1218
+ if (type === 53 && size !== 0) fail('packed tar directory has a payload')
1219
+ const name = tarString(header, 0, 100); const prefix = tarString(header, 345, 155)
1220
+ const entryPath = prefix === '' ? name : `${prefix}/${name}`
1221
+ const logicalPath = type === 53 && entryPath.endsWith('/') ? entryPath.slice(0, -1) : entryPath
1222
+ if ((logicalPath !== 'package' && !logicalPath.startsWith('package/'))
1223
+ || logicalPath.includes('//') || logicalPath.split('/').some(part => part === '' || part === '.' || part === '..')) {
1224
+ fail('packed tar entry path is not canonical')
1225
+ }
1226
+ const dataOffset = offset + TAR_BLOCK_BYTES
1227
+ const nextOffset = dataOffset + Math.ceil(size / TAR_BLOCK_BYTES) * TAR_BLOCK_BYTES
1228
+ if (nextOffset > archive.length) fail('packed tar entry is truncated')
1229
+ if (entryPath === 'package/package.json') {
1230
+ if (manifestOffset !== undefined || prefix !== '' || (type !== 0 && type !== 48)) {
1231
+ fail('packed manifest has a duplicate or unsupported representation')
1232
+ }
1233
+ manifestOffset = dataOffset; manifestSize = size
1234
+ }
1235
+ offset = nextOffset
1236
+ }
1237
+ if (!terminated || manifestOffset === undefined || manifestSize === undefined) fail('packed tar has no regular package manifest')
1238
+ const sourceBytes = archive.subarray(manifestOffset, manifestOffset + manifestSize)
1239
+ const source = sourceBytes.toString('utf8')
1240
+ if (!Buffer.from(source, 'utf8').equals(sourceBytes)) fail('packed manifest is not valid UTF-8')
1241
+ let manifest
1242
+ try { manifest = JSON.parse(source) } catch { fail('packed manifest is not valid JSON') }
1243
+ if (typeof manifest !== 'object' || manifest === null || Array.isArray(manifest)) fail('packed manifest must be an object')
1244
+ const lineEnding = source.endsWith('\n') ? '\n' : ''
1245
+ if (`${JSON.stringify(manifest, null, 2)}${lineEnding}` !== source) fail('packed manifest is not in the supported canonical JSON format')
1246
+ const normalized = { ...manifest }
1247
+ for (const name of DEPENDENCY_MAPS) {
1248
+ if (!Object.hasOwn(manifest, name)) continue
1249
+ const entries = manifest[name]
1250
+ if (typeof entries !== 'object' || entries === null || Array.isArray(entries)
1251
+ || Object.values(entries).some(value => typeof value !== 'string')) fail(`packed manifest ${name} is invalid`)
1252
+ normalized[name] = Object.fromEntries(Object.entries(entries).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0))
1253
+ }
1254
+ const normalizedBytes = Buffer.from(`${JSON.stringify(normalized, null, 2)}${lineEnding}`)
1255
+ if (normalizedBytes.length !== manifestSize) fail('packed manifest normalization changed its tar entry size')
1256
+ const normalizedArchive = Buffer.from(archive)
1257
+ normalizedBytes.copy(normalizedArchive, manifestOffset)
1258
+ const result = gzipSync(normalizedArchive, { mtime: 0 })
1259
+ if (result.length > MAX_ARTIFACT_BYTES) fail('normalized packed archive exceeds the artifact limit')
1260
+ return result
1261
+ }
979
1262
  function prepareBuildSource(request, context, gitValue, hooks) {
980
1263
  const bundle = join(context.directory, 'source.bundle'); rmSync(bundle, { force: true })
981
1264
  const temporaryRef = `refs/dsh-release/build/${sha256Bytes(request.operationId).slice(0, 32)}`
@@ -1012,15 +1295,18 @@ function runIsolatedBuild(request, context, runNumber, gitValue, build, packageP
1012
1295
  try {
1013
1296
  sandboxCommand(build, ['install', '--offline', '--frozen-lockfile', '--frozen-store', '--ignore-scripts',
1014
1297
  '--package-import-method=copy', '--store-dir=/store'], sandbox, hooks)
1298
+ sandboxCommand(build, ['--workspace-root', '--if-present', 'run', 'build'], sandbox, hooks)
1015
1299
  sandboxCommand(build, ['--dir', `/workspace/${packagePath}`, 'run', 'build'], sandbox, hooks)
1016
1300
  sandboxCommand(build, ['--dir', `/workspace/${packagePath}`, 'pack', '--pack-destination', '/output'], sandbox, hooks)
1017
1301
  } finally { closeSandboxContext(sandbox) }
1018
1302
  const packed = readdirSync(output).filter(name => name.endsWith('.tgz'))
1019
1303
  if (packed.length !== 1 || readdirSync(output).length !== 1) fail('pnpm pack must produce exactly one tarball')
1020
1304
  const tarball = safeRegularFile(join(output, packed[0]), `isolated build ${runNumber} tarball`)
1021
- const bytes = readBounded(tarball, `isolated build ${runNumber} tarball`)
1022
- const unpacked = unpackPackage(build, tarball, join(runRoot, 'packed'), runRoot, hooks)
1023
- return { runRoot, checkout, workspace, packageRoot: unpacked.packageRoot, inventory: unpacked.inventory, tarball, bytes, sha256: sha256Bytes(bytes) }
1305
+ const bytes = normalizePackedDependencyOrder(readBounded(tarball, `isolated build ${runNumber} tarball`))
1306
+ const normalizedTarball = join(runRoot, 'normalized-package.tgz'); writeSynced(normalizedTarball, bytes, 0o600)
1307
+ const unpacked = unpackPackage(build, normalizedTarball, join(runRoot, 'packed'), runRoot, hooks)
1308
+ return { runRoot, checkout, workspace, packageRoot: unpacked.packageRoot, inventory: unpacked.inventory,
1309
+ tarball: normalizedTarball, bytes, sha256: sha256Bytes(bytes) }
1024
1310
  }
1025
1311
  function temporaryBuildRef(request) { return `refs/dsh-release/build/${sha256Bytes(request.operationId).slice(0, 32)}` }
1026
1312
  function buildPhase(request, config, context, hooks) {
@@ -1069,7 +1355,8 @@ function buildPhase(request, config, context, hooks) {
1069
1355
  const sbomPath = join(outputs, 'sbom.cdx.json'); immutableCopy(sbomPath, Buffer.from(`${canonicalReleaseValue(sbom)}\n`), 0o600)
1070
1356
  const provenance = { _type: 'https://in-toto.io/Statement/v1', subject: [{ name: packageName, digest: { sha256: tarballSha256 } }],
1071
1357
  predicateType: 'https://slsa.dev/provenance/v1', predicate: { buildDefinition: { buildType: 'https://dsh-enhanced.dev/build/local-isolated/v1',
1072
- externalParameters: { packagePath, packageVersion }, resolvedDependencies: [{ uri: pathToFileURL(gitValue.remote).href, digest: { gitCommit: mergeCommit } }] },
1358
+ externalParameters: { packagePath, packageVersion, manifestNormalization: 'sort-direct-dependency-maps-v1' },
1359
+ resolvedDependencies: [{ uri: pathToFileURL(gitValue.remote).href, digest: { gitCommit: mergeCommit } }] },
1073
1360
  runDetails: { builder: { id: `dsh-local-release-adapter:${config.authority}:${config.keyId}` }, metadata: { invocationId: request.operationId },
1074
1361
  byproducts: [{ name: 'sbom', digest: { sha256: sha256Bytes(readFileSync(sbomPath)) } }] } } }
1075
1362
  const provenancePath = join(outputs, 'provenance.intoto.jsonl'); immutableCopy(provenancePath, Buffer.from(`${canonicalReleaseValue(provenance)}\n`), 0o600)
@@ -1384,6 +1671,39 @@ function persistReviewReceipt(receipt, config) {
1384
1671
  } else immutableJson(path, receipt, 0o600)
1385
1672
  }
1386
1673
 
1674
+ /** Read-only installation preflight. No request, receipt, lock, subprocess or
1675
+ * release phase is created; return public identity only, never loaded keys. */
1676
+ export function inspectLocalReleaseAdapterConfiguration(environment, phase) {
1677
+ if (!PHASES.has(phase)) fail('adapter inspection phase is invalid')
1678
+ const config = loadConfig(environment, phase)
1679
+ if (config.phase !== phase) fail('adapter config differs from inspection phase')
1680
+ const publicKey = createPublicKey(config.privateKey)
1681
+ if (config.authorizationAuthority.publicKey.equals(publicKey)
1682
+ || (config.authorizationAuthority.authority === config.authority && config.authorizationAuthority.keyId === config.keyId)) {
1683
+ fail('release authorization and adapter identities must be independent')
1684
+ }
1685
+ if (['pr', 'review', 'merge', 'build'].includes(phase)) {
1686
+ const git = gitConfig(config.git)
1687
+ if (phase === 'review' && (!git.reviewStore || !git.reviewDecisionRoot)) fail('review requires independent decision and receipt stores')
1688
+ if (phase === 'merge' && (!git.reviewStore || !git.reviewAuthority || git.reviewAuthority.publicKey.equals(publicKey))) {
1689
+ fail('merge requires an independent review identity and receipt store')
1690
+ }
1691
+ }
1692
+ if (phase === 'build') buildConfig(config.build)
1693
+ if (['publish', 'registry-verify', 'catalog-admission'].includes(phase)) {
1694
+ const registry = registryConfig(config.registry)
1695
+ if (!registry.signer) fail('registry requires an artifact signer')
1696
+ if (phase === 'registry-verify' && !registry.downloadRoot) fail('registry verification requires a separate download root')
1697
+ if (registry.signer.publicKey.equals(publicKey)) fail('registry phase and artifact signer must be independent')
1698
+ }
1699
+ if (phase === 'catalog-admission') {
1700
+ const catalog = catalogConfig(config.catalog)
1701
+ const helper = openPinnedFile(catalog.helper, 'catalog helper', 1_048_576, false)
1702
+ try { privateFile(catalog.path, 'catalog file') } finally { closePinnedFile(helper) }
1703
+ }
1704
+ return { id: config.id, phase, executablePath: config.executablePath, authority: config.authority, keyId: config.keyId }
1705
+ }
1706
+
1387
1707
  export async function runLocalReleaseAdapter(argv = process.argv.slice(2), environment = process.env, hooks = {}) {
1388
1708
  if (argv.length === 1 && argv[0] === '--version') { process.stdout.write(`${LOCAL_RELEASE_ADAPTER_VERSION}\n`); return }
1389
1709
  if (argv.length === 1 && argv[0] === '--capabilities') {