@dsh-enhanced/plugin-control-plane 0.1.31 → 0.1.34

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 (199) hide show
  1. package/README.md +352 -4
  2. package/bin/dsh-local-release-adapter.js +80 -16
  3. package/bin/dsh-npm-registry-adapter.js +613 -0
  4. package/bin/dsh-source-adoption-authority.js +12 -0
  5. package/bin/dsh-source-approval-authority.js +16 -0
  6. package/bin/dsh-source-release-authority.js +12 -0
  7. package/bin/dsh-systemd-host-attestor.js +707 -0
  8. package/bin/dsh-task-observation-authority.js +15 -0
  9. package/lib/adapter-process.d.ts +23 -0
  10. package/lib/adapter-process.d.ts.map +1 -0
  11. package/lib/adapter-process.js +305 -0
  12. package/lib/adapter-process.js.map +1 -0
  13. package/lib/adoption-coordinator.d.ts +63 -0
  14. package/lib/adoption-coordinator.d.ts.map +1 -0
  15. package/lib/adoption-coordinator.js +241 -0
  16. package/lib/adoption-coordinator.js.map +1 -0
  17. package/lib/adoption-handoff.d.ts +16 -0
  18. package/lib/adoption-handoff.d.ts.map +1 -0
  19. package/lib/adoption-handoff.js +11 -0
  20. package/lib/adoption-handoff.js.map +1 -0
  21. package/lib/attestation.d.ts.map +1 -1
  22. package/lib/attestation.js +49 -1
  23. package/lib/attestation.js.map +1 -1
  24. package/lib/catalog.d.ts.map +1 -1
  25. package/lib/catalog.js +29 -2
  26. package/lib/catalog.js.map +1 -1
  27. package/lib/cli.d.ts +33 -9
  28. package/lib/cli.d.ts.map +1 -1
  29. package/lib/cli.js +624 -178
  30. package/lib/cli.js.map +1 -1
  31. package/lib/deployment-readiness.d.ts +38 -0
  32. package/lib/deployment-readiness.d.ts.map +1 -0
  33. package/lib/deployment-readiness.js +154 -0
  34. package/lib/deployment-readiness.js.map +1 -0
  35. package/lib/effect-blocked-replay.d.ts +81 -0
  36. package/lib/effect-blocked-replay.d.ts.map +1 -0
  37. package/lib/effect-blocked-replay.js +259 -0
  38. package/lib/effect-blocked-replay.js.map +1 -0
  39. package/lib/errors.d.ts +6 -0
  40. package/lib/errors.d.ts.map +1 -0
  41. package/lib/errors.js +9 -0
  42. package/lib/errors.js.map +1 -0
  43. package/lib/foreground-deployment-runtime.d.ts +20 -0
  44. package/lib/foreground-deployment-runtime.d.ts.map +1 -0
  45. package/lib/foreground-deployment-runtime.js +79 -0
  46. package/lib/foreground-deployment-runtime.js.map +1 -0
  47. package/lib/foreground-deployment.d.ts +17 -0
  48. package/lib/foreground-deployment.d.ts.map +1 -0
  49. package/lib/foreground-deployment.js +43 -0
  50. package/lib/foreground-deployment.js.map +1 -0
  51. package/lib/host-attestor.d.ts +1 -1
  52. package/lib/host-attestor.d.ts.map +1 -1
  53. package/lib/host-attestor.js +51 -57
  54. package/lib/host-attestor.js.map +1 -1
  55. package/lib/index.d.ts +25 -2
  56. package/lib/index.d.ts.map +1 -1
  57. package/lib/index.js +11 -2
  58. package/lib/index.js.map +1 -1
  59. package/lib/npm-publish.d.ts +25 -0
  60. package/lib/npm-publish.d.ts.map +1 -0
  61. package/lib/npm-publish.js +280 -0
  62. package/lib/npm-publish.js.map +1 -0
  63. package/lib/owner-task-gap-types.d.ts +21 -0
  64. package/lib/owner-task-gap-types.d.ts.map +1 -0
  65. package/lib/owner-task-gap-types.js +2 -0
  66. package/lib/owner-task-gap-types.js.map +1 -0
  67. package/lib/owner-task-gaps.d.ts +24 -0
  68. package/lib/owner-task-gaps.d.ts.map +1 -0
  69. package/lib/owner-task-gaps.js +74 -0
  70. package/lib/owner-task-gaps.js.map +1 -0
  71. package/lib/post-activation.d.ts +33 -0
  72. package/lib/post-activation.d.ts.map +1 -0
  73. package/lib/post-activation.js +219 -0
  74. package/lib/post-activation.js.map +1 -0
  75. package/lib/registry-fetch.d.ts +50 -0
  76. package/lib/registry-fetch.d.ts.map +1 -0
  77. package/lib/registry-fetch.js +267 -0
  78. package/lib/registry-fetch.js.map +1 -0
  79. package/lib/release.d.ts +4 -4
  80. package/lib/release.d.ts.map +1 -1
  81. package/lib/release.js +142 -56
  82. package/lib/release.js.map +1 -1
  83. package/lib/replay-endpoint-protocol.d.ts +65 -0
  84. package/lib/replay-endpoint-protocol.d.ts.map +1 -0
  85. package/lib/replay-endpoint-protocol.js +183 -0
  86. package/lib/replay-endpoint-protocol.js.map +1 -0
  87. package/lib/replay-endpoint.d.ts +7 -0
  88. package/lib/replay-endpoint.d.ts.map +1 -0
  89. package/lib/replay-endpoint.js +193 -0
  90. package/lib/replay-endpoint.js.map +1 -0
  91. package/lib/replay-grant.d.ts +62 -0
  92. package/lib/replay-grant.d.ts.map +1 -0
  93. package/lib/replay-grant.js +208 -0
  94. package/lib/replay-grant.js.map +1 -0
  95. package/lib/replay-journal.d.ts +25 -0
  96. package/lib/replay-journal.d.ts.map +1 -0
  97. package/lib/replay-journal.js +275 -0
  98. package/lib/replay-journal.js.map +1 -0
  99. package/lib/runtime-observer-protocol.d.ts +63 -0
  100. package/lib/runtime-observer-protocol.d.ts.map +1 -0
  101. package/lib/runtime-observer-protocol.js +280 -0
  102. package/lib/runtime-observer-protocol.js.map +1 -0
  103. package/lib/runtime-observer.d.ts +8 -0
  104. package/lib/runtime-observer.d.ts.map +1 -0
  105. package/lib/runtime-observer.js +182 -0
  106. package/lib/runtime-observer.js.map +1 -0
  107. package/lib/service.d.ts +145 -1
  108. package/lib/service.d.ts.map +1 -1
  109. package/lib/service.js +734 -6
  110. package/lib/service.js.map +1 -1
  111. package/lib/source-adoption-authority.d.ts +65 -0
  112. package/lib/source-adoption-authority.d.ts.map +1 -0
  113. package/lib/source-adoption-authority.js +271 -0
  114. package/lib/source-adoption-authority.js.map +1 -0
  115. package/lib/source-adoption-runner.d.ts +28 -0
  116. package/lib/source-adoption-runner.d.ts.map +1 -0
  117. package/lib/source-adoption-runner.js +197 -0
  118. package/lib/source-adoption-runner.js.map +1 -0
  119. package/lib/source-approval-authority.d.ts +50 -0
  120. package/lib/source-approval-authority.d.ts.map +1 -0
  121. package/lib/source-approval-authority.js +412 -0
  122. package/lib/source-approval-authority.js.map +1 -0
  123. package/lib/source-approval-client.d.ts +32 -0
  124. package/lib/source-approval-client.d.ts.map +1 -0
  125. package/lib/source-approval-client.js +175 -0
  126. package/lib/source-approval-client.js.map +1 -0
  127. package/lib/source-build.d.ts +60 -0
  128. package/lib/source-build.d.ts.map +1 -0
  129. package/lib/source-build.js +311 -0
  130. package/lib/source-build.js.map +1 -0
  131. package/lib/source-context.d.ts +24 -0
  132. package/lib/source-context.d.ts.map +1 -0
  133. package/lib/source-context.js +99 -0
  134. package/lib/source-context.js.map +1 -0
  135. package/lib/source-job-types.d.ts +87 -0
  136. package/lib/source-job-types.d.ts.map +1 -0
  137. package/lib/source-job-types.js +2 -0
  138. package/lib/source-job-types.js.map +1 -0
  139. package/lib/source-jobs.d.ts +113 -0
  140. package/lib/source-jobs.d.ts.map +1 -0
  141. package/lib/source-jobs.js +535 -0
  142. package/lib/source-jobs.js.map +1 -0
  143. package/lib/source-release-authority.d.ts +35 -0
  144. package/lib/source-release-authority.d.ts.map +1 -0
  145. package/lib/source-release-authority.js +271 -0
  146. package/lib/source-release-authority.js.map +1 -0
  147. package/lib/source-release-client.d.ts +10 -0
  148. package/lib/source-release-client.d.ts.map +1 -0
  149. package/lib/source-release-client.js +24 -0
  150. package/lib/source-release-client.js.map +1 -0
  151. package/lib/source-release-runner.d.ts +31 -0
  152. package/lib/source-release-runner.d.ts.map +1 -0
  153. package/lib/source-release-runner.js +125 -0
  154. package/lib/source-release-runner.js.map +1 -0
  155. package/lib/source-versioning.d.ts +24 -0
  156. package/lib/source-versioning.d.ts.map +1 -0
  157. package/lib/source-versioning.js +213 -0
  158. package/lib/source-versioning.js.map +1 -0
  159. package/lib/source-workspace.d.ts +117 -0
  160. package/lib/source-workspace.d.ts.map +1 -0
  161. package/lib/source-workspace.js +458 -0
  162. package/lib/source-workspace.js.map +1 -0
  163. package/lib/sqlite.d.ts +1 -1
  164. package/lib/sqlite.d.ts.map +1 -1
  165. package/lib/sqlite.js +509 -7
  166. package/lib/sqlite.js.map +1 -1
  167. package/lib/store.d.ts +229 -6
  168. package/lib/store.d.ts.map +1 -1
  169. package/lib/store.js +2431 -383
  170. package/lib/store.js.map +1 -1
  171. package/lib/task-observation-authority.d.ts +29 -0
  172. package/lib/task-observation-authority.d.ts.map +1 -0
  173. package/lib/task-observation-authority.js +370 -0
  174. package/lib/task-observation-authority.js.map +1 -0
  175. package/lib/task-observation-client.d.ts +4 -0
  176. package/lib/task-observation-client.d.ts.map +1 -0
  177. package/lib/task-observation-client.js +20 -0
  178. package/lib/task-observation-client.js.map +1 -0
  179. package/lib/task-observation-runtime.d.ts +59 -0
  180. package/lib/task-observation-runtime.d.ts.map +1 -0
  181. package/lib/task-observation-runtime.js +331 -0
  182. package/lib/task-observation-runtime.js.map +1 -0
  183. package/lib/task-observation-store.d.ts +17 -0
  184. package/lib/task-observation-store.d.ts.map +1 -0
  185. package/lib/task-observation-store.js +115 -0
  186. package/lib/task-observation-store.js.map +1 -0
  187. package/lib/task-observation-types.d.ts +67 -0
  188. package/lib/task-observation-types.d.ts.map +1 -0
  189. package/lib/task-observation-types.js +2 -0
  190. package/lib/task-observation-types.js.map +1 -0
  191. package/lib/trust.d.ts +3 -0
  192. package/lib/trust.d.ts.map +1 -1
  193. package/lib/trust.js +0 -0
  194. package/lib/trust.js.map +1 -1
  195. package/lib/types.d.ts +275 -8
  196. package/lib/types.d.ts.map +1 -1
  197. package/lib/version.d.ts +1 -1
  198. package/lib/version.js +1 -1
  199. package/package.json +40 -5
@@ -0,0 +1,613 @@
1
+ #!/usr/bin/node
2
+ /** Owner-configured npm publish or independent verification. No installation authority. */
3
+ import { createHash, createPrivateKey, createPublicKey, sign, verify } from 'node:crypto'
4
+ import { closeSync, constants as fsConstants, existsSync, fsyncSync, fstatSync, linkSync, lstatSync, mkdirSync, openSync, readSync, realpathSync, unlinkSync, writeFileSync } from 'node:fs'
5
+ import { dirname, isAbsolute, join, resolve } from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+
8
+ export const NPM_REGISTRY_ADAPTER_VERSION = 'dsh-npm-registry-adapter-1'
9
+ const PHASES = new Set(['registry-verify', 'publish'])
10
+ const DIGEST = /^[a-f0-9]{64}$/u
11
+ const COMMIT = /^[a-f0-9]{40}$/u
12
+ const ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/u
13
+ const PACKAGE = /^@[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/u
14
+ const VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?$/u
15
+ const MAX_ARTIFACT_BYTES = 268_435_456
16
+ const ARTIFACT_FDS = Object.freeze({ tarball: ['DSH_RELEASE_TARBALL_FD', 3, MAX_ARTIFACT_BYTES],
17
+ sbom: ['DSH_RELEASE_SBOM_FD', 4, 16_777_216], provenance: ['DSH_RELEASE_PROVENANCE_FD', 5, 16_777_216] })
18
+
19
+ export function canonicalReleaseValue(value) {
20
+ if (Array.isArray(value)) return `[${value.map(canonicalReleaseValue).join(',')}]`
21
+ if (typeof value === 'object' && value !== null) {
22
+ return `{${Object.entries(value).filter(([, item]) => item !== undefined)
23
+ .sort(([left], [right]) => left.localeCompare(right))
24
+ .map(([key, item]) => `${JSON.stringify(key)}:${canonicalReleaseValue(item)}`).join(',')}}`
25
+ }
26
+ return JSON.stringify(value)
27
+ }
28
+ function sha256Bytes(value) { return createHash('sha256').update(value).digest('hex') }
29
+ function sha512Integrity(value) { return `sha512-${createHash('sha512').update(value).digest('base64')}` }
30
+ function digest(value) { return sha256Bytes(canonicalReleaseValue(value)) }
31
+ function fail(message) { throw new Error(`npm registry adapter: ${message}`) }
32
+ function object(value, label) {
33
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) fail(`${label} must be an object`)
34
+ return value
35
+ }
36
+ function text(value, label, pattern = /^.+$/u, maximum = 2_000) {
37
+ if (typeof value !== 'string' || Buffer.byteLength(value) > maximum || value.includes('\0')
38
+ || value.includes('\r') || value.includes('\n') || !pattern.test(value)) fail(`${label} is invalid`)
39
+ return value
40
+ }
41
+ function exactKeys(value, expected, label) {
42
+ if (Object.keys(value).sort().join('\0') !== [...expected].sort().join('\0')) fail(`${label} has unknown or missing fields`)
43
+ }
44
+ function canonicalPath(value, label) {
45
+ const path = text(value, label)
46
+ if (!isAbsolute(path) || path === '/' || realpathSync(path) !== resolve(path)) fail(`${label} must be an existing canonical absolute path`)
47
+ return path
48
+ }
49
+ function privateDirectory(path, label) {
50
+ const metadata = lstatSync(path); const uid = process.getuid?.()
51
+ if (!metadata.isDirectory() || metadata.isSymbolicLink() || (metadata.mode & 0o077) !== 0
52
+ || (uid !== undefined && metadata.uid !== uid) || realpathSync(path) !== resolve(path)) fail(`${label} must be an owner-private canonical directory`)
53
+ }
54
+ function ensurePrivateSubdirectory(root, components, label) {
55
+ let current = root; privateDirectory(current, `${label} root`)
56
+ for (const component of components) {
57
+ if (!/^[A-Za-z0-9%._-]+$/u.test(component) || component === '.' || component === '..') fail(`${label} component is invalid`)
58
+ current = join(current, component)
59
+ if (!existsSync(current)) mkdirSync(current, { mode: 0o700 })
60
+ privateDirectory(current, label)
61
+ // Persist every ancestor directory entry before an irreversible dispatch.
62
+ // Also sync existing entries: a previous attempt may have stopped at mkdir.
63
+ fsyncDirectory(dirname(current))
64
+ }
65
+ return current
66
+ }
67
+ function fsyncDirectory(path) { const descriptor = openSync(path, fsConstants.O_RDONLY | fsConstants.O_DIRECTORY); try { fsyncSync(descriptor) } finally { closeSync(descriptor) } }
68
+ function privateFile(path, label, maximum = 65_536) {
69
+ const canonical = canonicalPath(path, label); const metadata = lstatSync(canonical); const uid = process.getuid?.()
70
+ if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.nlink !== 1 || metadata.size < 1 || metadata.size > maximum
71
+ || (metadata.mode & 0o077) !== 0 || (uid !== undefined && metadata.uid !== uid)) fail(`${label} must be an owner-private regular file`)
72
+ privateDirectory(dirname(canonical), `${label} directory`)
73
+ return canonical
74
+ }
75
+ function safeRegularFile(path, label, maximum = MAX_ARTIFACT_BYTES) {
76
+ const canonical = canonicalPath(path, label); const metadata = lstatSync(canonical); const uid = process.getuid?.()
77
+ if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.nlink !== 1 || metadata.size < 1 || metadata.size > maximum
78
+ || (metadata.mode & 0o022) !== 0 || (uid !== undefined && metadata.uid !== uid && metadata.uid !== 0)) fail(`${label} is unsafe`)
79
+ return canonical
80
+ }
81
+ function stableBytes(path, label, maximum = MAX_ARTIFACT_BYTES) {
82
+ const canonical = safeRegularFile(path, label, maximum)
83
+ const descriptor = openSync(canonical, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW)
84
+ let bytes; let before; let after
85
+ try { before = fstatSync(descriptor, { bigint: true }); bytes = inheritedDescriptorBytes(descriptor, label, maximum); after = fstatSync(descriptor, { bigint: true }) }
86
+ finally { closeSync(descriptor) }
87
+ const pathAfter = lstatSync(canonical, { bigint: true })
88
+ if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeNs !== after.mtimeNs
89
+ || before.ctimeNs !== after.ctimeNs || pathAfter.dev !== before.dev || pathAfter.ino !== before.ino
90
+ || BigInt(bytes.length) !== before.size) fail(`${label} changed during read`)
91
+ return bytes
92
+ }
93
+ function openPinnedFile(spec, label, maximum = MAX_ARTIFACT_BYTES, executable = false) {
94
+ const item = object(spec, label); exactKeys(item, ['path', 'sha256'], label)
95
+ const path = canonicalPath(item.path, `${label}.path`); const expected = text(item.sha256, `${label}.sha256`, DIGEST)
96
+ const descriptor = openSync(path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW); const uid = process.getuid?.()
97
+ try {
98
+ const before = fstatSync(descriptor, { bigint: true }); const pathBefore = lstatSync(path, { bigint: true })
99
+ const expectedUid = uid === undefined ? undefined : BigInt(uid)
100
+ if (!before.isFile() || before.nlink !== 1n || before.size < 1n || before.size > BigInt(maximum)
101
+ || (before.mode & 0o022n) !== 0n || (executable && (before.mode & 0o111n) === 0n)
102
+ || (expectedUid !== undefined && before.uid !== expectedUid && before.uid !== 0n)
103
+ || pathBefore.isSymbolicLink() || pathBefore.dev !== before.dev || pathBefore.ino !== before.ino) fail(`${label} is unsafe`)
104
+ const bytes = inheritedDescriptorBytes(descriptor, label, maximum); const after = fstatSync(descriptor, { bigint: true })
105
+ if (sha256Bytes(bytes) !== expected || after.dev !== before.dev || after.ino !== before.ino || after.size !== before.size
106
+ || after.mtimeNs !== before.mtimeNs || after.ctimeNs !== before.ctimeNs) fail(`${label} digest or identity changed`)
107
+ return { path, sha256: expected, descriptor, device: before.dev, inode: before.ino }
108
+ } catch (error) { closeSync(descriptor); throw error }
109
+ }
110
+ function verifyPinnedFile(value, label, maximum = MAX_ARTIFACT_BYTES) {
111
+ const metadata = fstatSync(value.descriptor, { bigint: true })
112
+ if (metadata.dev !== value.device || metadata.ino !== value.inode
113
+ || sha256Bytes(inheritedDescriptorBytes(value.descriptor, label, maximum)) !== value.sha256) fail(`${label} changed during use`)
114
+ }
115
+ function closePinnedFile(value) { if (value?.descriptor !== undefined) closeSync(value.descriptor) }
116
+ function readBounded(path, label, maximum = MAX_ARTIFACT_BYTES) { return stableBytes(path, label, maximum) }
117
+ function inheritedArtifactBytes(kind) {
118
+ const [environmentName, expectedFd, maximum] = ARTIFACT_FDS[kind]
119
+ if (process.env[environmentName] !== String(expectedFd)) fail(`${environmentName} must bind inherited fd ${expectedFd}`)
120
+ return inheritedDescriptorBytes(expectedFd, `inherited ${kind}`, maximum)
121
+ }
122
+ function inheritedDescriptorBytes(descriptor, label, maximum) {
123
+ const before = fstatSync(descriptor, { bigint: true }); const uid = process.getuid?.(); const expectedUid = uid === undefined ? undefined : BigInt(uid)
124
+ if (!before.isFile() || before.nlink !== 1n || before.size < 1n || before.size > BigInt(maximum) || (before.mode & 0o022n) !== 0n
125
+ || (expectedUid !== undefined && before.uid !== expectedUid && before.uid !== 0n)) fail(`${label} fd is unsafe`)
126
+ const bytes = Buffer.alloc(Number(before.size)); let offset = 0
127
+ while (offset < bytes.length) { const count = readSync(descriptor, bytes, offset, bytes.length - offset, offset); if (count === 0) fail(`${label} ended early`); offset += count }
128
+ const after = fstatSync(descriptor, { bigint: true })
129
+ if (after.dev !== before.dev || after.ino !== before.ino || after.size !== before.size || after.mtimeNs !== before.mtimeNs
130
+ || after.ctimeNs !== before.ctimeNs) fail(`${label} changed during read`)
131
+ return bytes
132
+ }
133
+ function runningAdapterDigest() {
134
+ const argument = process.argv[1]
135
+ if (typeof argument !== 'string') fail('adapter executable argument is missing')
136
+ const descriptor = argument.match(/^\/proc\/self\/fd\/(\d+)$/u)
137
+ return descriptor === null ? sha256Bytes(stableBytes(argument, 'running adapter executable', MAX_ARTIFACT_BYTES))
138
+ : sha256Bytes(inheritedDescriptorBytes(Number(descriptor[1]), 'running adapter executable', MAX_ARTIFACT_BYTES))
139
+ }
140
+ function runningInterpreterDigest() {
141
+ const descriptor = process.execPath.match(/^\/proc\/self\/fd\/(\d+)$/u)
142
+ return descriptor === null ? sha256Bytes(stableBytes(process.execPath, 'running adapter interpreter', MAX_ARTIFACT_BYTES))
143
+ : sha256Bytes(inheritedDescriptorBytes(Number(descriptor[1]), 'running adapter interpreter', MAX_ARTIFACT_BYTES))
144
+ }
145
+ function relativePath(value, label) {
146
+ const path = text(value, label, /^[A-Za-z0-9._/@+-]+$/u)
147
+ if (isAbsolute(path) || path === '.' || path.split('/').some(part => part === '' || part === '.' || part === '..')) fail(`${label} is not a safe relative path`)
148
+ return path
149
+ }
150
+ function readOwnerJson(path, label) {
151
+ const source = readBounded(path, label, 65_536).toString('utf8')
152
+ try { return object(JSON.parse(source), label) } catch (error) { if (error instanceof SyntaxError) fail(`${label} is not valid JSON`); throw error }
153
+ }
154
+ function loadPublicIdentity(value, label) {
155
+ const item = object(value, label); exactKeys(item, ['authority', 'keyId', 'publicKeyPath'], label)
156
+ const authority = text(item.authority, `${label}.authority`, ID); const keyId = text(item.keyId, `${label}.keyId`, ID)
157
+ const publicKeyPath = privateFile(item.publicKeyPath, `${label}.public key`, 16_384)
158
+ const publicKey = createPublicKey(readBounded(publicKeyPath, `${label}.public key`, 16_384))
159
+ if (publicKey.asymmetricKeyType !== 'ed25519') fail(`${label} public key must be Ed25519`)
160
+ return { authority, keyId, publicKeyPath, publicKey }
161
+ }
162
+ function safeScope(value) {
163
+ if (!Array.isArray(value) || value.length === 0 || value.length > 64) fail('source scope is invalid')
164
+ const scope = [...new Set(value.map((entry, index) => relativePath(entry.normalize('NFC').trim(), `scope[${index}]`)))].sort()
165
+ return scope
166
+ }
167
+ function validateAuthorization(request) {
168
+ const authorization = object(request.authorization, 'source authorization')
169
+ exactKeys(authorization, ['schemaVersion', 'kind', 'authorizationId', 'authority', 'keyId', 'planId', 'planDigest', 'baseCommit',
170
+ 'checkedTreeDigest', 'checkedPatchDigest', 'scope', 'releasePolicy', 'authorizedAt', 'expiresAt', 'signature', 'signatureDigest'], 'source authorization')
171
+ const policy = object(authorization.releasePolicy, 'source release policy')
172
+ exactKeys(policy, ['targetBranch', 'candidateId', 'packageName', 'packageVersion', 'packagePath', 'dshBaseline', 'capabilities',
173
+ 'authorities', 'requires', 'registryId', 'registryLocator', 'registryReference', 'catalogId', 'catalogPath',
174
+ 'minimumReproducibleBuilds'], 'source release policy')
175
+ text(authorization.signatureDigest, 'authorization signature digest', DIGEST)
176
+ const authorizationSignature = text(authorization.signature, 'authorization signature', /^[A-Za-z0-9+/]+={0,2}$/u, 16_384)
177
+ const authorizationSignatureBytes = Buffer.from(authorizationSignature, 'base64')
178
+ if (authorization.schemaVersion !== 1 || authorization.kind !== 'dsh-source-release-authorization'
179
+ || !ID.test(authorization.authorizationId) || !ID.test(authorization.authority) || !ID.test(authorization.keyId)
180
+ || authorization.planId !== request.plan.id || authorization.planDigest !== request.plan.digest
181
+ || authorization.baseCommit === undefined || !COMMIT.test(authorization.baseCommit)
182
+ || !DIGEST.test(authorization.checkedTreeDigest) || !DIGEST.test(authorization.checkedPatchDigest)
183
+ || digest(authorization.scope) !== digest(safeScope(authorization.scope))
184
+ || !Number.isSafeInteger(authorization.authorizedAt) || !Number.isSafeInteger(authorization.expiresAt)
185
+ || authorization.authorizedAt > request.requestedAt || request.requestedAt > authorization.expiresAt
186
+ || text(policy.targetBranch, 'authorized target branch') === '' || !/^[a-z0-9][a-z0-9-]{0,63}$/u.test(policy.candidateId)
187
+ || !PACKAGE.test(policy.packageName) || !VERSION.test(policy.packageVersion)
188
+ || relativePath(policy.packagePath, 'authorized package path') !== policy.packagePath
189
+ || !VERSION.test(policy.dshBaseline) || !Number.isSafeInteger(policy.minimumReproducibleBuilds) || policy.minimumReproducibleBuilds < 2
190
+ || policy.minimumReproducibleBuilds > 16
191
+ || authorizationSignatureBytes.length !== 64 || authorizationSignatureBytes.toString('base64') !== authorizationSignature
192
+ || sha256Bytes(authorizationSignatureBytes) !== authorization.signatureDigest
193
+ || policy.registryId !== request.registry.id || policy.registryLocator !== request.registry.locator
194
+ || ('catalog' in request && (policy.catalogId !== request.catalog.id || policy.catalogPath !== request.catalog.path))
195
+ || typeof policy.registryReference !== 'string' || policy.registryReference === '') fail('source authorization is not bound to this request')
196
+ const normalizeStrings = (value, label) => {
197
+ if (!Array.isArray(value) || value.length === 0 || value.some(entry => typeof entry !== 'string' || entry.normalize('NFC').trim() === '')) fail(`${label} is invalid`)
198
+ const normalized = [...new Set(value.map(entry => entry.normalize('NFC').trim()))].sort()
199
+ if (digest(value) !== digest(normalized)) fail(`${label} is not canonical`)
200
+ return normalized
201
+ }
202
+ normalizeStrings(policy.capabilities, 'authorized capabilities'); normalizeStrings(policy.authorities, 'authorized authorities')
203
+ if (!Array.isArray(policy.requires)) fail('authorized requirements are invalid')
204
+ for (const requirement of policy.requires) {
205
+ const item = object(requirement, 'authorized requirement'); exactKeys(item, ['package', 'version', 'integrity'], 'authorized requirement')
206
+ text(item.package, 'authorized required package', PACKAGE); text(item.version, 'authorized required version', VERSION)
207
+ text(item.integrity, 'authorized required integrity', /^sha512-[A-Za-z0-9+/]+={0,2}$/u)
208
+ }
209
+ return authorization
210
+ }
211
+ function verifyAuthorizationSignature(authorization, config) {
212
+ if (authorization.authority !== config.authorizationAuthority.authority || authorization.keyId !== config.authorizationAuthority.keyId) {
213
+ fail('source authorization uses an untrusted authority')
214
+ }
215
+ const { signature, signatureDigest: _signatureDigest, ...unsigned } = authorization
216
+ if (!verify(null, Buffer.from(canonicalReleaseValue(unsigned)), config.authorizationAuthority.publicKey, Buffer.from(signature, 'base64'))) {
217
+ fail('source authorization signature is invalid')
218
+ }
219
+ }
220
+ function validateRequest(request, config) {
221
+ exactKeys(object(request, 'release request'), ['schemaVersion', 'kind', 'operationId', 'attempt', 'requestedAt', 'receiptTtlMs',
222
+ 'installationId', 'ledger', 'plan', 'release', 'authorization', 'adapter', 'registry', 'catalog', 'phase', 'input'], 'release request')
223
+ if (request.schemaVersion !== 1 || request.kind !== 'dsh-source-release-request' || request.phase !== config.phase
224
+ || !PHASES.has(request.phase) || !ID.test(request.operationId) || !Number.isSafeInteger(request.requestedAt)
225
+ || !Number.isSafeInteger(request.attempt) || request.attempt < 1 || !Number.isSafeInteger(request.receiptTtlMs)
226
+ || request.receiptTtlMs < 1_000 || request.receiptTtlMs > 300_000) fail('release request envelope is invalid')
227
+ const ledger = object(request.ledger, 'release ledger'); exactKeys(ledger, ['id', 'path'], 'release ledger')
228
+ const plan = object(request.plan, 'release plan'); exactKeys(plan, ['id', 'digest', 'revision'], 'release plan')
229
+ const release = object(request.release, 'release fence'); exactKeys(release, ['id', 'fence'], 'release fence')
230
+ const registry = object(request.registry, 'release registry'); exactKeys(registry, ['id', 'locator'], 'release registry')
231
+ const catalog = object(request.catalog, 'release catalog'); exactKeys(catalog, ['id', 'path'], 'release catalog')
232
+ const adapter = object(request.adapter, 'adapter identity'); exactKeys(adapter, ['id', 'version', 'path', 'sha256', 'interpreter', 'authority', 'keyId'], 'adapter identity')
233
+ const interpreter = object(adapter.interpreter, 'adapter interpreter'); exactKeys(interpreter, ['path', 'sha256'], 'adapter interpreter')
234
+ if (!ID.test(plan.id) || !DIGEST.test(plan.digest) || !Number.isSafeInteger(plan.revision) || plan.revision < 1
235
+ || !ID.test(release.id) || !Number.isSafeInteger(release.fence) || release.fence < 1
236
+ || adapter.id !== config.id || adapter.version !== NPM_REGISTRY_ADAPTER_VERSION || adapter.authority !== config.authority || adapter.keyId !== config.keyId
237
+ || adapter.path !== config.executablePath || runningAdapterDigest() !== adapter.sha256
238
+ || runningInterpreterDigest() !== interpreter.sha256) {
239
+ fail('release request is not bound to this adapter')
240
+ }
241
+ const authorization = validateAuthorization(request)
242
+ verifyAuthorizationSignature(authorization, config)
243
+ const input = object(request.input, `${request.phase} input`)
244
+ exactKeys(input, request.phase === 'publish' ? ['artifact', 'artifactStatementDigest', 'artifactSignature', 'signEvidenceDigest']
245
+ : ['artifact', 'artifactStatementDigest', 'artifactSignature', 'registryReference', 'publishEvidenceDigest'], 'registry phase input')
246
+ return { authorization }
247
+ }
248
+ function verifierContext(request, config, reconcile) {
249
+ const requestDigest = digest(request); const operationKey = sha256Bytes(request.operationId)
250
+ const namespace = reconcile ? 'reconciliations' : 'operations'
251
+ const directory = ensurePrivateSubdirectory(config.stateRoot, [namespace, operationKey], 'verifier operation directory')
252
+ const releaseLock = acquireProcessLock(join(directory, 'execution.lock'), 'registry verifier')
253
+ try {
254
+ const bindingPath = join(directory, 'binding.json')
255
+ const binding = { operationId: request.operationId, requestDigest, configurationDigest: config.configurationDigest }
256
+ if (!existsSync(bindingPath)) immutableJson(bindingPath, binding, 0o600)
257
+ if (digest(readOwnerJson(bindingPath, 'operation binding')) !== digest(binding)) fail('operation id was reused with a different request or configuration')
258
+ const receiptPath = join(directory, 'receipt.json')
259
+ if (existsSync(receiptPath)) {
260
+ const cached = readOwnerJson(receiptPath, 'cached receipt')
261
+ const receipt = object(cached.receipt, 'cached signed receipt')
262
+ const { signature, ...unsigned } = receipt
263
+ if (cached.requestDigest !== requestDigest || receipt.requestDigest !== requestDigest
264
+ || receipt.operationId !== request.operationId || receipt.evidenceDigest !== digest(receipt.evidence)
265
+ || !verify(null, Buffer.from(canonicalReleaseValue(unsigned)), createPublicKey(config.privateKey), Buffer.from(signature, 'base64'))) fail('cached receipt no longer verifies')
266
+ releaseLock(); return { directory, requestDigest, receiptPath, cached: receipt }
267
+ }
268
+ return { directory, requestDigest, receiptPath, releaseLock }
269
+ } catch (error) { releaseLock(); throw error }
270
+ }
271
+ function acquireProcessLock(path, label, retried = false) {
272
+ try {
273
+ writeSynced(path, Buffer.from(`${JSON.stringify({ pid: process.pid })}\n`), 0o600)
274
+ } catch (error) {
275
+ if (error?.code !== 'EEXIST') throw error
276
+ let owner
277
+ try { owner = readOwnerJson(path, `${label} execution lock`) } catch { fail(`${label} execution lock is invalid`) }
278
+ if (!Number.isSafeInteger(owner.pid) || owner.pid < 1) fail(`${label} execution lock is invalid`)
279
+ try { process.kill(owner.pid, 0); fail(`${label} is already executing`) } catch (probe) {
280
+ if (probe?.code !== 'ESRCH') throw probe
281
+ }
282
+ if (retried) fail(`${label} stale execution lock raced`)
283
+ unlinkSync(path); return acquireProcessLock(path, label, true)
284
+ }
285
+ let released = false
286
+ return () => {
287
+ if (released) return
288
+ const owner = readOwnerJson(path, `${label} execution lock`)
289
+ if (owner.pid !== process.pid) fail(`${label} execution lock ownership changed`)
290
+ unlinkSync(path); released = true
291
+ }
292
+ }
293
+ function writeSynced(path, bytes, mode = 0o600) {
294
+ const descriptor = openSync(path, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY, mode)
295
+ try { writeFileSync(descriptor, bytes); fsyncSync(descriptor) } finally { closeSync(descriptor) }
296
+ }
297
+ function immutableJson(path, value, mode = 0o600) {
298
+ const bytes = Buffer.from(`${canonicalReleaseValue(value)}\n`)
299
+ const temporary = `${path}.tmp-${process.pid}-${Date.now()}`
300
+ writeSynced(temporary, bytes, mode)
301
+ try { linkSync(temporary, path); fsyncDirectory(dirname(path)) } catch (error) { if (error?.code !== 'EEXIST') throw error } finally { unlinkSync(temporary); fsyncDirectory(dirname(path)) }
302
+ }
303
+ function assertUnexpired(request, label) { if (Date.now() > request.authorization.expiresAt) fail(`${label} crossed the authorization expiry`) }
304
+ function artifactSigningPayload(artifact) { return canonicalReleaseValue({ schemaVersion: 1, kind: 'dsh-release-artifact', artifact }) }
305
+ function verifyArtifactFiles(artifact) {
306
+ const tarball = inheritedArtifactBytes('tarball'); const sbom = inheritedArtifactBytes('sbom'); const provenance = inheritedArtifactBytes('provenance')
307
+ if (tarball.length !== artifact.tarballBytes || sha256Bytes(tarball) !== artifact.tarballSha256 || sha512Integrity(tarball) !== artifact.tarballIntegrity
308
+ || sha256Bytes(sbom) !== artifact.sbomSha256 || sha256Bytes(provenance) !== artifact.provenanceSha256) fail('inherited artifact files do not match build evidence')
309
+ return tarball
310
+ }
311
+ function verifySignedArtifact(input, registry, verifyFiles = true) {
312
+ if (registry.signer === undefined) fail('artifact signer is not configured')
313
+ const artifact = object(input.artifact, 'release artifact'); const statementDigest = text(input.artifactStatementDigest, 'artifact statement digest', DIGEST)
314
+ const signature = text(input.artifactSignature, 'artifact signature', /^[A-Za-z0-9+/]+={0,2}$/u, 16_384)
315
+ if (statementDigest !== digest(artifact)
316
+ || !verify(null, Buffer.from(artifactSigningPayload(artifact)), registry.signer.publicKey, Buffer.from(signature, 'base64'))) fail('artifact signature is invalid')
317
+ return { artifact, statementDigest, signature, signatureDigest: sha256Bytes(Buffer.from(signature, 'base64')),
318
+ ...(verifyFiles ? { tarball: verifyArtifactFiles(artifact) } : {}) }
319
+ }
320
+ function signedReceipt(request, config, requestDigest, evidence, outcome = 'passed') {
321
+ const observedAt = Date.now()
322
+ if (Date.now() < observedAt || Date.now() > request.authorization.expiresAt) fail('request is outside its authorization interval')
323
+ const expiresAt = Math.min(observedAt + request.receiptTtlMs, request.authorization.expiresAt)
324
+ if (expiresAt <= observedAt) fail('source release authorization expired before receipt issuance')
325
+ const unsigned = { schemaVersion: 1, receiptId: `receipt-${sha256Bytes(request.operationId).slice(0, 32)}`,
326
+ authority: config.authority, keyId: config.keyId, installationId: request.installationId, planId: request.plan.id,
327
+ planDigest: request.plan.digest, releaseId: request.release.id, fence: request.release.fence, operationId: request.operationId,
328
+ requestDigest, phase: request.phase, outcome, evidence, evidenceDigest: digest(evidence), observedAt, expiresAt }
329
+ return { ...unsigned, signature: sign(null, Buffer.from(canonicalReleaseValue(unsigned)), config.privateKey).toString('base64') }
330
+ }
331
+ function signedReconciliationReceipt(request, config, requestDigest, evidence) {
332
+ const observedAt = Date.now()
333
+ if (Date.now() < observedAt || Date.now() > request.authorization.expiresAt) fail('reconciliation request is outside its authorization interval')
334
+ const expiresAt = Math.min(observedAt + request.receiptTtlMs, request.authorization.expiresAt)
335
+ if (expiresAt <= observedAt) fail('source release authorization expired before reconciliation receipt issuance')
336
+ const unsigned = { schemaVersion: 2, kind: 'dsh-source-publish-reconciliation-receipt',
337
+ receiptId: `reconciliation-${sha256Bytes(request.operationId).slice(0, 32)}`, authority: config.authority, keyId: config.keyId,
338
+ installationId: request.installationId, planId: request.plan.id, planDigest: request.plan.digest, releaseId: request.release.id,
339
+ fence: request.release.fence, operationId: request.operationId, requestDigest, evidence, evidenceDigest: digest(evidence),
340
+ observedAt, expiresAt }
341
+ return { ...unsigned, signature: sign(null, Buffer.from(canonicalReleaseValue(unsigned)), config.privateKey).toString('base64') }
342
+ }
343
+ function validateReconciliationRequest(request, config) {
344
+ const expected = ['schemaVersion', 'kind', 'operationId', 'attempt', 'requestedAt', 'receiptTtlMs', 'installationId', 'ledger', 'plan',
345
+ 'release', 'authorization', 'adapter', 'registry', 'ambiguousPublish', 'artifact', 'expectedRegistryReference',
346
+ 'expectedArtifactStatementDigest', 'expectedArtifactSignatureDigest']
347
+ exactKeys(object(request, 'reconciliation request'), expected, 'reconciliation request')
348
+ if (request.schemaVersion !== 1 || request.kind !== 'dsh-source-publish-reconciliation-request' || !ID.test(request.operationId)
349
+ || !Number.isSafeInteger(request.attempt) || request.attempt < 1
350
+ || !Number.isSafeInteger(request.requestedAt) || !Number.isSafeInteger(request.receiptTtlMs) || request.receiptTtlMs < 1_000
351
+ || request.receiptTtlMs > 300_000) fail('reconciliation request envelope is invalid')
352
+ const authorization = validateAuthorization(request); const adapter = object(request.adapter, 'adapter identity')
353
+ const ledger = object(request.ledger, 'reconciliation ledger'); exactKeys(ledger, ['id', 'path'], 'reconciliation ledger')
354
+ const plan = object(request.plan, 'reconciliation plan'); exactKeys(plan, ['id', 'digest', 'revision'], 'reconciliation plan')
355
+ const release = object(request.release, 'reconciliation release'); exactKeys(release, ['id', 'fence'], 'reconciliation release')
356
+ const registryInput = object(request.registry, 'reconciliation registry'); exactKeys(registryInput, ['id', 'locator'], 'reconciliation registry')
357
+ exactKeys(adapter, ['id', 'version', 'path', 'sha256', 'interpreter', 'authority', 'keyId'], 'adapter identity')
358
+ const interpreter = object(adapter.interpreter, 'adapter interpreter')
359
+ exactKeys(interpreter, ['path', 'sha256'], 'adapter interpreter')
360
+ if (config.phase !== 'registry-verify' || adapter.id !== config.id || adapter.version !== NPM_REGISTRY_ADAPTER_VERSION
361
+ || adapter.authority !== config.authority || adapter.keyId !== config.keyId || adapter.path !== config.executablePath
362
+ || runningAdapterDigest() !== adapter.sha256 || runningInterpreterDigest() !== interpreter.sha256) {
363
+ fail('reconciliation request is not bound to this verifier')
364
+ }
365
+ const registry = registryConfig(config.registry)
366
+ if (registry.id !== request.registry.id || registry.locator !== request.registry.locator
367
+ || request.expectedRegistryReference !== authorization.releasePolicy.registryReference) fail('reconciliation request targets a different registry')
368
+ const ambiguous = object(request.ambiguousPublish, 'ambiguous publish'); exactKeys(ambiguous, ['operationId', 'receiptId', 'receiptDigest', 'evidenceDigest'], 'ambiguous publish')
369
+ const artifact = object(request.artifact, 'reconciliation artifact'); exactKeys(artifact, ['packageName', 'packageVersion', 'tarballSha256', 'tarballIntegrity'], 'reconciliation artifact')
370
+ for (const value of [ambiguous.receiptDigest, ambiguous.evidenceDigest, artifact.tarballSha256]) text(value, 'reconciliation digest', DIGEST)
371
+ text(request.expectedArtifactStatementDigest, 'expected artifact statement digest', DIGEST)
372
+ text(request.expectedArtifactSignatureDigest, 'expected artifact signature digest', DIGEST)
373
+ text(artifact.packageName, 'reconciliation package', PACKAGE); text(artifact.packageVersion, 'reconciliation version', VERSION)
374
+ text(artifact.tarballIntegrity, 'reconciliation integrity', /^sha512-[A-Za-z0-9+/]+={0,2}$/u)
375
+ if (artifact.packageName !== authorization.releasePolicy.packageName || artifact.packageVersion !== authorization.releasePolicy.packageVersion) {
376
+ fail('reconciliation artifact is not owner-authorized')
377
+ }
378
+ verifyAuthorizationSignature(authorization, config)
379
+ return { authorization, registry }
380
+ }
381
+
382
+ function httpsBase(value, label) {
383
+ const raw = text(value, label)
384
+ if (/[\\?#\s]/u.test(raw) || [...raw].some(c => c.charCodeAt(0) <= 0x20 || c.charCodeAt(0) === 0x7f) || /%(?:2e|2f|5c|25)|%(?![a-f0-9]{2})/iu.test(raw)) fail(`${label} is ambiguous`)
385
+ let url
386
+ try { url = new URL(raw) } catch { fail(`${label} is invalid`) }
387
+ if (url.protocol !== 'https:' || url.username !== '' || url.password !== '' || url.href !== raw) fail(`${label} must be canonical bare HTTPS`)
388
+ return url
389
+ }
390
+ function registryConfig(value, phase = 'registry-verify') {
391
+ const item = object(value, 'npm registry config')
392
+ exactKeys(item, ['protocol', 'id', 'locator', 'signer', 'helper', 'caPins', 'timeoutMs',
393
+ ...(phase === 'publish' ? ['tokenPath', 'tag'] : [])], 'npm registry config')
394
+ if (item.protocol !== 'npm') fail('registry protocol must be explicit npm')
395
+ text(item.id, 'registry id', ID); httpsBase(item.locator, 'registry locator')
396
+ if (!Number.isSafeInteger(item.timeoutMs) || item.timeoutMs < 1 || item.timeoutMs > 120_000) fail('registry timeout is invalid')
397
+ if (!Array.isArray(item.caPins) || item.caPins.length > 16 || item.caPins.some(pin => typeof pin !== 'string' || pin.length > 16_384 || !pin.includes('BEGIN CERTIFICATE'))) fail('registry CA pins are invalid')
398
+ const signer = loadPublicIdentity(item.signer, 'artifact signer')
399
+ if (phase === 'publish') {
400
+ text(item.tokenPath, 'publisher token path')
401
+ if (!isAbsolute(item.tokenPath)) fail('publisher token path must be absolute')
402
+ text(item.tag, 'owner publish tag', /^[a-z][a-z0-9-]{0,63}$/u)
403
+ }
404
+ const opened = openPinnedFile(item.helper, 'registry helper', 1_048_576); closePinnedFile(opened)
405
+ return { ...item, signer }
406
+ }
407
+ function loadConfig(environment, phase) {
408
+ const path = privateFile(phase === 'publish' ? environment.DSH_RELEASE_PUBLISH_CONFIG : environment.DSH_RELEASE_REGISTRY_VERIFY_CONFIG, 'registry adapter config')
409
+ const config = readOwnerJson(path, 'registry verifier config')
410
+ exactKeys(config, ['schemaVersion', 'id', 'phase', 'executablePath', 'authority', 'keyId', 'privateKeyPath', 'authorizationAuthority', 'stateRoot', 'registry'], 'registry verifier config')
411
+ if (config.schemaVersion !== 1 || config.phase !== phase || !PHASES.has(phase)) fail('unsupported registry adapter configuration')
412
+ for (const key of ['id', 'authority', 'keyId']) text(config[key], key, ID)
413
+ canonicalPath(config.executablePath, 'adapter executable')
414
+ const stateRoot = canonicalPath(config.stateRoot, 'state root'); privateDirectory(stateRoot, 'state root')
415
+ const privateKeyPath = privateFile(config.privateKeyPath, 'verifier signing key', 16_384)
416
+ const privateKey = createPrivateKey(readBounded(privateKeyPath, 'verifier signing key', 16_384))
417
+ if (privateKey.asymmetricKeyType !== 'ed25519') fail('verifier signing key must be Ed25519')
418
+ const authorizationAuthority = loadPublicIdentity(config.authorizationAuthority, 'release authorization authority')
419
+ const registry = registryConfig(config.registry, phase)
420
+ const publicKey = createPublicKey(privateKey)
421
+ if (publicKey.equals(authorizationAuthority.publicKey) || publicKey.equals(registry.signer.publicKey)
422
+ || (config.authority === registry.signer.authority && config.keyId === registry.signer.keyId)
423
+ || (config.authority === authorizationAuthority.authority && config.keyId === authorizationAuthority.keyId)) fail('verifier must be independent of artifact signer and owner authorizer')
424
+ return { ...config, privateKey, authorizationAuthority, registryValue: registry, configurationDigest: digest({ ...config,
425
+ ownerPublicKey: authorizationAuthority.publicKey.export({ type: 'spki', format: 'pem' }),
426
+ signerPublicKey: registry.signer.publicKey.export({ type: 'spki', format: 'pem' }),
427
+ verifierPublicKey: publicKey.export({ type: 'spki', format: 'pem' }) }) }
428
+ }
429
+ function validateBindings(request, config, reconcile) {
430
+ const now = Date.now(); const authorization = request.authorization; const registry = config.registryValue
431
+ if (!/^[a-f0-9]{8}-[a-f0-9]{4}-[1-8][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/u.test(request.installationId)
432
+ || !ID.test(request.ledger.id) || !isAbsolute(request.ledger.path) || request.ledger.path === '/'
433
+ || !ID.test(request.plan.id) || !DIGEST.test(request.plan.digest) || !Number.isSafeInteger(request.plan.revision) || request.plan.revision < 1
434
+ || !ID.test(request.release.id) || !Number.isSafeInteger(request.release.fence) || request.release.fence < 1
435
+ || request.requestedAt > now || authorization.authorizedAt > now || authorization.expiresAt <= now
436
+ || authorization.expiresAt <= authorization.authorizedAt || authorization.expiresAt - authorization.authorizedAt > 86_400_000
437
+ || request.registry.id !== registry.id || request.registry.locator !== registry.locator) fail('request binding or validity interval is invalid')
438
+ const interpreter = request.adapter.interpreter
439
+ if (realpathSync(interpreter.path) !== realpathSync(process.execPath) || interpreter.sha256 !== runningInterpreterDigest()) fail('request interpreter path differs from running interpreter')
440
+ const reference = reconcile ? request.expectedRegistryReference : request.phase === 'publish'
441
+ ? authorization.releasePolicy.registryReference : request.input.registryReference
442
+ const target = httpsBase(reference, 'registry reference'); const base = httpsBase(registry.locator, 'registry locator')
443
+ if (target.origin !== base.origin || !target.pathname.startsWith(`${base.pathname.replace(/\/+$/u, '')}/`)
444
+ || reference !== authorization.releasePolicy.registryReference) fail('registry reference escapes the authorized registry')
445
+ if (reconcile) {
446
+ text(request.ambiguousPublish.operationId, 'ambiguous operation', ID); text(request.ambiguousPublish.receiptId, 'ambiguous receipt', ID)
447
+ canonicalIntegrity(request.artifact.tarballIntegrity)
448
+ } else {
449
+ const artifact = request.input.artifact; const policy = authorization.releasePolicy
450
+ if (artifact.packageName !== policy.packageName || artifact.packageVersion !== policy.packageVersion
451
+ || artifact.packagePath !== policy.packagePath || artifact.candidateId !== policy.candidateId
452
+ || artifact.dshBaseline !== policy.dshBaseline || digest(artifact.capabilities) !== digest(policy.capabilities)
453
+ || digest(artifact.authorities) !== digest(policy.authorities) || digest(artifact.requires) !== digest(policy.requires)) fail('artifact differs from owner-authorized release policy')
454
+ if (!Number.isSafeInteger(artifact.tarballBytes) || artifact.tarballBytes < 1 || artifact.tarballBytes > MAX_ARTIFACT_BYTES) fail('artifact byte count is invalid')
455
+ canonicalIntegrity(artifact.tarballIntegrity); text(artifact.tarballSha256, 'artifact digest', DIGEST)
456
+ if (request.phase === 'publish') text(request.input.signEvidenceDigest, 'sign evidence digest', DIGEST)
457
+ else text(request.input.publishEvidenceDigest, 'publish evidence digest', DIGEST)
458
+ }
459
+ }
460
+ function canonicalIntegrity(value) {
461
+ if (typeof value !== 'string' || !/^sha512-[A-Za-z0-9+/]{86}==$/u.test(value)) fail('artifact integrity must be canonical SHA-512')
462
+ const encoded = value.slice(7); const bytes = Buffer.from(encoded, 'base64')
463
+ if (bytes.length !== 64 || bytes.toString('base64') !== encoded) fail('artifact integrity must be canonical SHA-512')
464
+ return value
465
+ }
466
+ async function observation(request, config) {
467
+ const registry = config.registryValue
468
+ const artifact = request.kind === 'dsh-source-publish-reconciliation-request' ? request.artifact : request.input.artifact
469
+ const timeoutMs = Math.min(registry.timeoutMs, request.authorization.expiresAt - Date.now())
470
+ if (timeoutMs < 1) fail('authorization expired before registry observation')
471
+ const input = { registry: { id: registry.id, protocol: 'npm', locator: registry.locator, caPins: registry.caPins, tokenEnvironment: null },
472
+ packageName: artifact.packageName, version: artifact.packageVersion, timeoutMs }
473
+ const pinned = openPinnedFile(registry.helper, 'registry helper', 1_048_576)
474
+ let value
475
+ try {
476
+ const bytes = inheritedDescriptorBytes(pinned.descriptor, 'registry helper', 1_048_576)
477
+ if (sha256Bytes(bytes) !== pinned.sha256) fail('registry helper changed before import')
478
+ // Import the checked bytes themselves: importing a filesystem URL could
479
+ // resolve a pathname again after its descriptor was pinned.
480
+ const helper = await import(`data:text/javascript;base64,${bytes.toString('base64')}`)
481
+ if (typeof helper.observeNpmRegistryArtifact !== 'function') fail('npm observer helper is unavailable')
482
+ let observed
483
+ try { observed = await helper.observeNpmRegistryArtifact(input, {}) } catch { return null }
484
+ if (!Buffer.isBuffer(observed.bytes) || observed.bytes.length < 1 || observed.bytes.length > MAX_ARTIFACT_BYTES) fail('registry observer returned invalid bytes')
485
+ value = { ok: true, registryReference: observed.reference, metadataReference: observed.metadataReference,
486
+ metadataIntegrity: observed.metadataIntegrity, downloadedBytes: observed.bytes.length,
487
+ observedTarballSha256: sha256Bytes(observed.bytes), observedTarballIntegrity: sha512Integrity(observed.bytes) }
488
+ } finally { try { verifyPinnedFile(pinned, 'registry helper', 1_048_576) } finally { closePinnedFile(pinned) } }
489
+ assertUnexpired(request, 'registry observation')
490
+ exactKeys(value, ['ok', 'registryReference', 'metadataReference', 'metadataIntegrity', 'downloadedBytes', 'observedTarballSha256', 'observedTarballIntegrity'], 'registry observation')
491
+ if (value.ok !== true || !Number.isSafeInteger(value.downloadedBytes) || value.downloadedBytes < 1 || value.downloadedBytes > MAX_ARTIFACT_BYTES) fail('registry observation byte count is invalid')
492
+ text(value.observedTarballSha256, 'observed digest', DIGEST); canonicalIntegrity(value.observedTarballIntegrity)
493
+ if (value.metadataIntegrity !== value.observedTarballIntegrity) fail('registry metadata and downloaded integrity differ')
494
+ const base = httpsBase(registry.locator, 'registry locator')
495
+ const metadata = new URL(base); metadata.pathname = `${base.pathname.replace(/\/+$/u, '')}/${encodeURIComponent(artifact.packageName)}/${artifact.packageVersion}`
496
+ const expectedReference = request.kind === 'dsh-source-publish-reconciliation-request' ? request.expectedRegistryReference : request.input.registryReference
497
+ if (value.metadataReference !== metadata.href || value.registryReference !== expectedReference) fail('registry observation reference differs from exact owner binding')
498
+ return value
499
+ }
500
+ function reconciliationEvidence(request, observed) {
501
+ const artifact = request.artifact
502
+ const outcome = observed === null ? 'unknown' : observed.observedTarballSha256 === artifact.tarballSha256
503
+ && observed.observedTarballIntegrity === artifact.tarballIntegrity ? 'exists-match' : 'digest-conflict'
504
+ const evidence = { kind: 'npm-publish-reconciliation', outcome, registryId: request.registry.id,
505
+ registryReference: observed?.registryReference ?? null, packageName: artifact.packageName, packageVersion: artifact.packageVersion,
506
+ expectedTarballSha256: artifact.tarballSha256, expectedTarballIntegrity: artifact.tarballIntegrity,
507
+ expectedArtifactStatementDigest: request.expectedArtifactStatementDigest, expectedArtifactSignatureDigest: request.expectedArtifactSignatureDigest,
508
+ observedTarballSha256: observed?.observedTarballSha256 ?? null, observedTarballIntegrity: observed?.observedTarballIntegrity ?? null,
509
+ metadataReference: observed?.metadataReference ?? null, metadataIntegrity: observed?.metadataIntegrity ?? null,
510
+ downloadedBytes: observed?.downloadedBytes ?? null, ambiguousPublishOperationId: request.ambiguousPublish.operationId,
511
+ ambiguousPublishReceiptDigest: request.ambiguousPublish.receiptDigest }
512
+ return { ...evidence, detailDigest: digest(evidence) }
513
+ }
514
+ async function publishReceipt(request, config, context, signed) {
515
+ const artifact = signed.artifact; const registry = config.registryValue
516
+ const reference = request.authorization.releasePolicy.registryReference
517
+ const ambiguous = reason => signedReceipt(request, config, context.requestDigest, {
518
+ kind: 'publish-ambiguity', registryId: registry.id, packageName: artifact.packageName, packageVersion: artifact.packageVersion,
519
+ tarballSha256: artifact.tarballSha256, detailDigest: digest({ reason }),
520
+ }, 'ambiguous')
521
+ const markerPath = join(context.directory, 'publish-dispatched.json')
522
+ if (existsSync(markerPath)) {
523
+ const marker = readOwnerJson(markerPath, 'publication dispatch marker')
524
+ if (marker.schemaVersion !== 1 || marker.operationId !== request.operationId || marker.requestDigest !== context.requestDigest
525
+ || marker.registryReference !== reference || !DIGEST.test(marker.bodySha256)) fail('publication dispatch marker is not bound to the request')
526
+ return ambiguous('previous-dispatch-requires-independent-reconciliation')
527
+ }
528
+ const pinned = openPinnedFile(registry.helper, 'npm publication helper', 1_048_576)
529
+ try {
530
+ const bytes = inheritedDescriptorBytes(pinned.descriptor, 'npm publication helper', 1_048_576)
531
+ if (sha256Bytes(bytes) !== pinned.sha256) fail('publication helper changed before import')
532
+ const helper = await import(`data:text/javascript;base64,${bytes.toString('base64')}`)
533
+ if (typeof helper.prepareNpmPublish !== 'function' || typeof helper.sendNpmPublish !== 'function') fail('npm publication helper contract is unavailable')
534
+ const prepared = await helper.prepareNpmPublish({ locator: registry.locator, packageName: artifact.packageName,
535
+ version: artifact.packageVersion, tarball: signed.tarball, tag: registry.tag, expectedRegistryReference: reference })
536
+ if (!Buffer.isBuffer(prepared.body) || prepared.body.length < 1 || prepared.body.length > 402_653_184) fail('publication payload exceeds its bound')
537
+ const base = httpsBase(registry.locator, 'registry locator'); const target = new URL(base)
538
+ target.pathname = `${base.pathname.replace(/\/+$/u, '')}/${artifact.packageName.replace('/', '%2f')}`
539
+ if (prepared.url !== target.href) fail('publication helper selected an unauthorized endpoint')
540
+ const tokenPath = privateFile(registry.tokenPath, 'npm publisher token', 16_384)
541
+ const token = new TextDecoder('utf-8', { fatal: true }).decode(readBounded(tokenPath, 'npm publisher token', 16_384)).trim()
542
+ if (!token || /[^\x21-\x7e]/u.test(token)) fail('npm publisher token is invalid')
543
+ assertUnexpired(request, 'npm publish preparation')
544
+ immutableJson(markerPath, { schemaVersion: 1, operationId: request.operationId, requestDigest: context.requestDigest,
545
+ registryReference: reference, bodySha256: sha256Bytes(prepared.body), dispatchedAt: Date.now() }, 0o600)
546
+ let result
547
+ try {
548
+ const timeoutMs = Math.min(registry.timeoutMs, request.authorization.expiresAt - Date.now())
549
+ if (timeoutMs < 1) return ambiguous('authorization-expired-after-dispatch-mark')
550
+ result = await helper.sendNpmPublish({ url: prepared.url, body: prepared.body, token, caPins: registry.caPins, timeoutMs })
551
+ } catch { return ambiguous('publication-transport-did-not-confirm') }
552
+ if (result?.outcome !== 'accepted' || !DIGEST.test(result.detailDigest)) return ambiguous('publication-acknowledgment-is-unknown')
553
+ return signedReceipt(request, config, context.requestDigest, { kind: 'publish', registryId: registry.id,
554
+ registryReference: reference, packageName: artifact.packageName, packageVersion: artifact.packageVersion,
555
+ tarballSha256: artifact.tarballSha256, tarballIntegrity: artifact.tarballIntegrity,
556
+ artifactStatementDigest: signed.statementDigest, artifactSignatureDigest: signed.signatureDigest,
557
+ signEvidenceDigest: request.input.signEvidenceDigest, immutable: true })
558
+ } finally { try { verifyPinnedFile(pinned, 'npm publication helper') } finally { closePinnedFile(pinned) } }
559
+ }
560
+ function readRequest() {
561
+ const chunks = []; let total = 0
562
+ while (true) {
563
+ const chunk = Buffer.alloc(16_384); const count = readSync(0, chunk, 0, chunk.length, null)
564
+ if (count === 0) break
565
+ total += count; if (total > 1_048_576) fail('request exceeds input limit')
566
+ chunks.push(chunk.subarray(0, count))
567
+ }
568
+ return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks)))
569
+ }
570
+ export async function runNpmRegistryAdapter(argv = process.argv.slice(2), environment = process.env) {
571
+ if (argv.length === 1 && argv[0] === '--version') { process.stdout.write(`${NPM_REGISTRY_ADAPTER_VERSION}\n`); return }
572
+ if (argv.length === 1 && argv[0] === '--capabilities') { process.stdout.write('{"schemaVersion":1,"artifactInput":"inherited-fd-v1"}\n'); return }
573
+ if (argv.length !== 1 || !['release', 'reconcile'].includes(argv[0])) fail('usage: dsh-npm-registry-adapter <--version|--capabilities|release|reconcile>')
574
+ const request = readRequest(); const reconcile = argv[0] === 'reconcile'
575
+ const phase = reconcile ? 'registry-verify' : request.phase
576
+ if (!PHASES.has(phase)) fail('unsupported npm registry phase')
577
+ const config = loadConfig(environment, phase)
578
+ if (reconcile) validateReconciliationRequest(request, config)
579
+ else validateRequest(request, config)
580
+ validateBindings(request, config, reconcile)
581
+ const signed = reconcile ? undefined : verifySignedArtifact(request.input, config.registryValue)
582
+ const context = verifierContext(request, config, reconcile)
583
+ if (context.cached !== undefined) {
584
+ if (context.cached.expiresAt <= Date.now()) fail('cached observation receipt expired')
585
+ process.stdout.write(`${JSON.stringify(context.cached)}\n`); return
586
+ }
587
+ try {
588
+ let receipt
589
+ if (!reconcile && request.phase === 'publish') receipt = await publishReceipt(request, config, context, signed)
590
+ else {
591
+ const observed = await observation(request, config)
592
+ if (reconcile) receipt = signedReconciliationReceipt(request, config, context.requestDigest, reconciliationEvidence(request, observed))
593
+ else {
594
+ if (observed === null || observed.observedTarballSha256 !== signed.artifact.tarballSha256
595
+ || observed.observedTarballIntegrity !== signed.artifact.tarballIntegrity || observed.downloadedBytes !== signed.artifact.tarballBytes) fail('npm download does not match the exact signed artifact')
596
+ receipt = signedReceipt(request, config, context.requestDigest, { kind: 'registry-verify', registryId: request.registry.id,
597
+ registryReference: observed.registryReference, independentlyDownloaded: true, downloadedBytes: observed.downloadedBytes,
598
+ downloadedSha256: observed.observedTarballSha256, downloadedIntegrity: observed.observedTarballIntegrity,
599
+ artifactStatementDigest: signed.statementDigest, artifactSignatureDigest: signed.signatureDigest, publishEvidenceDigest: request.input.publishEvidenceDigest })
600
+ }
601
+ }
602
+ immutableJson(context.receiptPath, { requestDigest: context.requestDigest, receipt }, 0o600)
603
+ const persisted = readOwnerJson(context.receiptPath, 'persisted verifier receipt')
604
+ if (persisted.requestDigest !== context.requestDigest || digest(persisted.receipt) !== digest(receipt)) fail('receipt persistence raced')
605
+ process.stdout.write(`${JSON.stringify(receipt)}\n`)
606
+ } finally { context.releaseLock?.() }
607
+ }
608
+ const invokedDirectly = process.argv[1] !== undefined
609
+ && (/^\/proc\/self\/fd\/\d+$/u.test(process.argv[1]) || realpathSync(process.argv[1]) === fileURLToPath(import.meta.url))
610
+ if (invokedDirectly) void runNpmRegistryAdapter().catch(error => {
611
+ process.stderr.write(`${error instanceof Error ? error.message : 'npm registry adapter failed'}\n`)
612
+ process.exitCode = 1
613
+ })
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import { realpathSync } from 'node:fs'
3
+ import { dirname, join } from 'node:path'
4
+ import { pathToFileURL } from 'node:url'
5
+ try {
6
+ const wrapper = realpathSync(process.argv[1])
7
+ const { runSourceAdoptionAuthority } = await import(pathToFileURL(join(dirname(wrapper), '../lib/source-adoption-authority.js')).href)
8
+ await runSourceAdoptionAuthority()
9
+ } catch {
10
+ process.stderr.write('source adoption authority refused the request\n')
11
+ process.exitCode = 1
12
+ }