@dsh-enhanced/plugin-control-plane 0.1.32 → 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.
- package/README.md +352 -4
- package/bin/dsh-local-release-adapter.js +80 -16
- package/bin/dsh-npm-registry-adapter.js +613 -0
- package/bin/dsh-source-adoption-authority.js +12 -0
- package/bin/dsh-source-approval-authority.js +16 -0
- package/bin/dsh-source-release-authority.js +12 -0
- package/bin/dsh-systemd-host-attestor.js +707 -0
- package/bin/dsh-task-observation-authority.js +15 -0
- package/lib/adapter-process.d.ts +23 -0
- package/lib/adapter-process.d.ts.map +1 -0
- package/lib/adapter-process.js +305 -0
- package/lib/adapter-process.js.map +1 -0
- package/lib/adoption-coordinator.d.ts +63 -0
- package/lib/adoption-coordinator.d.ts.map +1 -0
- package/lib/adoption-coordinator.js +241 -0
- package/lib/adoption-coordinator.js.map +1 -0
- package/lib/adoption-handoff.d.ts +16 -0
- package/lib/adoption-handoff.d.ts.map +1 -0
- package/lib/adoption-handoff.js +11 -0
- package/lib/adoption-handoff.js.map +1 -0
- package/lib/attestation.d.ts.map +1 -1
- package/lib/attestation.js +49 -1
- package/lib/attestation.js.map +1 -1
- package/lib/catalog.d.ts.map +1 -1
- package/lib/catalog.js +29 -2
- package/lib/catalog.js.map +1 -1
- package/lib/cli.d.ts +33 -9
- package/lib/cli.d.ts.map +1 -1
- package/lib/cli.js +624 -178
- package/lib/cli.js.map +1 -1
- package/lib/deployment-readiness.d.ts +38 -0
- package/lib/deployment-readiness.d.ts.map +1 -0
- package/lib/deployment-readiness.js +154 -0
- package/lib/deployment-readiness.js.map +1 -0
- package/lib/effect-blocked-replay.d.ts +81 -0
- package/lib/effect-blocked-replay.d.ts.map +1 -0
- package/lib/effect-blocked-replay.js +259 -0
- package/lib/effect-blocked-replay.js.map +1 -0
- package/lib/errors.d.ts +6 -0
- package/lib/errors.d.ts.map +1 -0
- package/lib/errors.js +9 -0
- package/lib/errors.js.map +1 -0
- package/lib/foreground-deployment-runtime.d.ts +20 -0
- package/lib/foreground-deployment-runtime.d.ts.map +1 -0
- package/lib/foreground-deployment-runtime.js +79 -0
- package/lib/foreground-deployment-runtime.js.map +1 -0
- package/lib/foreground-deployment.d.ts +17 -0
- package/lib/foreground-deployment.d.ts.map +1 -0
- package/lib/foreground-deployment.js +43 -0
- package/lib/foreground-deployment.js.map +1 -0
- package/lib/host-attestor.d.ts +1 -1
- package/lib/host-attestor.d.ts.map +1 -1
- package/lib/host-attestor.js +51 -57
- package/lib/host-attestor.js.map +1 -1
- package/lib/index.d.ts +25 -2
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +11 -2
- package/lib/index.js.map +1 -1
- package/lib/npm-publish.d.ts +25 -0
- package/lib/npm-publish.d.ts.map +1 -0
- package/lib/npm-publish.js +280 -0
- package/lib/npm-publish.js.map +1 -0
- package/lib/owner-task-gap-types.d.ts +21 -0
- package/lib/owner-task-gap-types.d.ts.map +1 -0
- package/lib/owner-task-gap-types.js +2 -0
- package/lib/owner-task-gap-types.js.map +1 -0
- package/lib/owner-task-gaps.d.ts +24 -0
- package/lib/owner-task-gaps.d.ts.map +1 -0
- package/lib/owner-task-gaps.js +74 -0
- package/lib/owner-task-gaps.js.map +1 -0
- package/lib/post-activation.d.ts +33 -0
- package/lib/post-activation.d.ts.map +1 -0
- package/lib/post-activation.js +219 -0
- package/lib/post-activation.js.map +1 -0
- package/lib/registry-fetch.d.ts +50 -0
- package/lib/registry-fetch.d.ts.map +1 -0
- package/lib/registry-fetch.js +267 -0
- package/lib/registry-fetch.js.map +1 -0
- package/lib/release.d.ts +4 -4
- package/lib/release.d.ts.map +1 -1
- package/lib/release.js +142 -56
- package/lib/release.js.map +1 -1
- package/lib/replay-endpoint-protocol.d.ts +65 -0
- package/lib/replay-endpoint-protocol.d.ts.map +1 -0
- package/lib/replay-endpoint-protocol.js +183 -0
- package/lib/replay-endpoint-protocol.js.map +1 -0
- package/lib/replay-endpoint.d.ts +7 -0
- package/lib/replay-endpoint.d.ts.map +1 -0
- package/lib/replay-endpoint.js +193 -0
- package/lib/replay-endpoint.js.map +1 -0
- package/lib/replay-grant.d.ts +62 -0
- package/lib/replay-grant.d.ts.map +1 -0
- package/lib/replay-grant.js +208 -0
- package/lib/replay-grant.js.map +1 -0
- package/lib/replay-journal.d.ts +25 -0
- package/lib/replay-journal.d.ts.map +1 -0
- package/lib/replay-journal.js +275 -0
- package/lib/replay-journal.js.map +1 -0
- package/lib/runtime-observer-protocol.d.ts +63 -0
- package/lib/runtime-observer-protocol.d.ts.map +1 -0
- package/lib/runtime-observer-protocol.js +280 -0
- package/lib/runtime-observer-protocol.js.map +1 -0
- package/lib/runtime-observer.d.ts +8 -0
- package/lib/runtime-observer.d.ts.map +1 -0
- package/lib/runtime-observer.js +182 -0
- package/lib/runtime-observer.js.map +1 -0
- package/lib/service.d.ts +145 -1
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +734 -6
- package/lib/service.js.map +1 -1
- package/lib/source-adoption-authority.d.ts +65 -0
- package/lib/source-adoption-authority.d.ts.map +1 -0
- package/lib/source-adoption-authority.js +271 -0
- package/lib/source-adoption-authority.js.map +1 -0
- package/lib/source-adoption-runner.d.ts +28 -0
- package/lib/source-adoption-runner.d.ts.map +1 -0
- package/lib/source-adoption-runner.js +197 -0
- package/lib/source-adoption-runner.js.map +1 -0
- package/lib/source-approval-authority.d.ts +50 -0
- package/lib/source-approval-authority.d.ts.map +1 -0
- package/lib/source-approval-authority.js +412 -0
- package/lib/source-approval-authority.js.map +1 -0
- package/lib/source-approval-client.d.ts +32 -0
- package/lib/source-approval-client.d.ts.map +1 -0
- package/lib/source-approval-client.js +175 -0
- package/lib/source-approval-client.js.map +1 -0
- package/lib/source-build.d.ts +60 -0
- package/lib/source-build.d.ts.map +1 -0
- package/lib/source-build.js +311 -0
- package/lib/source-build.js.map +1 -0
- package/lib/source-context.d.ts +24 -0
- package/lib/source-context.d.ts.map +1 -0
- package/lib/source-context.js +99 -0
- package/lib/source-context.js.map +1 -0
- package/lib/source-job-types.d.ts +87 -0
- package/lib/source-job-types.d.ts.map +1 -0
- package/lib/source-job-types.js +2 -0
- package/lib/source-job-types.js.map +1 -0
- package/lib/source-jobs.d.ts +113 -0
- package/lib/source-jobs.d.ts.map +1 -0
- package/lib/source-jobs.js +535 -0
- package/lib/source-jobs.js.map +1 -0
- package/lib/source-release-authority.d.ts +35 -0
- package/lib/source-release-authority.d.ts.map +1 -0
- package/lib/source-release-authority.js +271 -0
- package/lib/source-release-authority.js.map +1 -0
- package/lib/source-release-client.d.ts +10 -0
- package/lib/source-release-client.d.ts.map +1 -0
- package/lib/source-release-client.js +24 -0
- package/lib/source-release-client.js.map +1 -0
- package/lib/source-release-runner.d.ts +31 -0
- package/lib/source-release-runner.d.ts.map +1 -0
- package/lib/source-release-runner.js +125 -0
- package/lib/source-release-runner.js.map +1 -0
- package/lib/source-versioning.d.ts +24 -0
- package/lib/source-versioning.d.ts.map +1 -0
- package/lib/source-versioning.js +213 -0
- package/lib/source-versioning.js.map +1 -0
- package/lib/source-workspace.d.ts +117 -0
- package/lib/source-workspace.d.ts.map +1 -0
- package/lib/source-workspace.js +458 -0
- package/lib/source-workspace.js.map +1 -0
- package/lib/sqlite.d.ts +1 -1
- package/lib/sqlite.d.ts.map +1 -1
- package/lib/sqlite.js +509 -7
- package/lib/sqlite.js.map +1 -1
- package/lib/store.d.ts +229 -6
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +2431 -383
- package/lib/store.js.map +1 -1
- package/lib/task-observation-authority.d.ts +29 -0
- package/lib/task-observation-authority.d.ts.map +1 -0
- package/lib/task-observation-authority.js +370 -0
- package/lib/task-observation-authority.js.map +1 -0
- package/lib/task-observation-client.d.ts +4 -0
- package/lib/task-observation-client.d.ts.map +1 -0
- package/lib/task-observation-client.js +20 -0
- package/lib/task-observation-client.js.map +1 -0
- package/lib/task-observation-runtime.d.ts +59 -0
- package/lib/task-observation-runtime.d.ts.map +1 -0
- package/lib/task-observation-runtime.js +331 -0
- package/lib/task-observation-runtime.js.map +1 -0
- package/lib/task-observation-store.d.ts +17 -0
- package/lib/task-observation-store.d.ts.map +1 -0
- package/lib/task-observation-store.js +115 -0
- package/lib/task-observation-store.js.map +1 -0
- package/lib/task-observation-types.d.ts +67 -0
- package/lib/task-observation-types.d.ts.map +1 -0
- package/lib/task-observation-types.js +2 -0
- package/lib/task-observation-types.js.map +1 -0
- package/lib/trust.d.ts +3 -0
- package/lib/trust.d.ts.map +1 -1
- package/lib/trust.js +0 -0
- package/lib/trust.js.map +1 -1
- package/lib/types.d.ts +275 -8
- package/lib/types.d.ts.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- 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
|
+
}
|