@neottia/repository-store 0.3.0 → 0.3.3

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.
@@ -0,0 +1,289 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import {
3
+ closeSync,
4
+ fsyncSync,
5
+ lstatSync,
6
+ mkdirSync,
7
+ openSync,
8
+ readFileSync,
9
+ renameSync,
10
+ rmSync,
11
+ writeFileSync,
12
+ } from 'node:fs';
13
+ import { dirname, join } from 'node:path';
14
+
15
+ export const PACKAGE_NAME = '@neottia/repository-store';
16
+ export const MANIFEST_SCHEMA_VERSION = 1;
17
+ export const RECEIPT_SCHEMA_VERSION = 1;
18
+ export const SIGSTORE_CERTIFICATE_ISSUER = 'https://token.actions.githubusercontent.com';
19
+ export const SIGSTORE_CERTIFICATE_IDENTITY =
20
+ 'https://github.com/dragoscirjan/neottia/.github/workflows/ci.yml@refs/heads/main';
21
+ export const SIGSTORE_SOURCE_DIGEST_OID = '1.3.6.1.4.1.57264.1.13';
22
+ export const SIGSTORE_RUNNER_ENVIRONMENT_OID = '1.3.6.1.4.1.57264.1.11';
23
+ export const GITHUB_REPOSITORY = 'dragoscirjan/neottia';
24
+ export const NATIVE_BINARY_RELATIVE_PATH = join('build', 'Release', 'repository_store_native.node');
25
+ export const NATIVE_RECEIPT_RELATIVE_PATH = join('build', 'Release', 'repository_store_native.integrity.json');
26
+
27
+ export const certifiedTuples = Object.freeze([
28
+ Object.freeze({ platform: 'linux', architecture: 'x64' }),
29
+ Object.freeze({ platform: 'linux', architecture: 'arm64' }),
30
+ Object.freeze({ platform: 'darwin', architecture: 'x64' }),
31
+ Object.freeze({ platform: 'darwin', architecture: 'arm64' }),
32
+ Object.freeze({ platform: 'win32', architecture: 'x64' }),
33
+ Object.freeze({ platform: 'freebsd', architecture: 'x64' }),
34
+ ]);
35
+
36
+ export class NativePrebuildAvailabilityError extends Error {
37
+ constructor(message, options) {
38
+ super(message, options);
39
+ this.name = 'NativePrebuildAvailabilityError';
40
+ }
41
+ }
42
+
43
+ export class NativePrebuildIntegrityError extends Error {
44
+ constructor(message, options) {
45
+ super(message, options);
46
+ this.name = 'NativePrebuildIntegrityError';
47
+ }
48
+ }
49
+
50
+ export function assertSigstoreCertificateOids(signer, expectedOids) {
51
+ const actualOids = signer?.identity?.oids;
52
+ if (!Array.isArray(actualOids))
53
+ throw new NativePrebuildIntegrityError('The Sigstore certificate contains no verifiable OID claims.');
54
+ for (const [oid, expectedValue] of Object.entries(expectedOids)) {
55
+ const claim = actualOids.find(
56
+ (candidate) => candidate?.oid?.id?.join('.') === oid && Buffer.isBuffer(candidate.value),
57
+ );
58
+ const expected = encodeDerUtf8String(Buffer.from(expectedValue, 'utf8'));
59
+ if (claim === undefined || !claim.value.equals(expected))
60
+ throw new NativePrebuildIntegrityError(`The Sigstore certificate does not pin ${oid} to ${expectedValue}.`);
61
+ }
62
+ }
63
+
64
+ export function sigstoreCertificateIdentityPattern(identity) {
65
+ return `^${identity.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`;
66
+ }
67
+
68
+ export function tupleKey(platform, architecture) {
69
+ return `${platform}-${architecture}`;
70
+ }
71
+
72
+ export function isCertifiedTuple(platform, architecture) {
73
+ return certifiedTuples.some((tuple) => tuple.platform === platform && tuple.architecture === architecture);
74
+ }
75
+
76
+ export function releaseTag(version) {
77
+ return `${PACKAGE_NAME}@${version}`;
78
+ }
79
+
80
+ export function nativeArtifactName(version, platform, architecture) {
81
+ return `repository-store-native-v${version}-${platform}-${architecture}.node`;
82
+ }
83
+
84
+ export function nativeBundleName(version, platform, architecture) {
85
+ return `${nativeArtifactName(version, platform, architecture)}.sigstore.json`;
86
+ }
87
+
88
+ export function nativeAttestationName(version, platform, architecture) {
89
+ return `${nativeArtifactName(version, platform, architecture)}.intoto.jsonl`;
90
+ }
91
+
92
+ export function nativeMetadataName(version, platform, architecture) {
93
+ return `${nativeArtifactName(version, platform, architecture)}.metadata.json`;
94
+ }
95
+
96
+ export function sha256(bytes) {
97
+ return createHash('sha256').update(bytes).digest('hex');
98
+ }
99
+
100
+ export function readPackageVersion(packageRoot) {
101
+ const packageJson = parseJson(readFileSync(join(packageRoot, 'package.json'), 'utf8'), 'package.json');
102
+ if (packageJson.name !== PACKAGE_NAME || !isVersion(packageJson.version))
103
+ throw new NativePrebuildIntegrityError('Repository Store package metadata is invalid.');
104
+ return packageJson.version;
105
+ }
106
+
107
+ export function readNativeManifest(packageRoot, expectedVersion = readPackageVersion(packageRoot)) {
108
+ let source;
109
+ try {
110
+ source = readFileSync(join(packageRoot, 'native-prebuilds.json'), 'utf8');
111
+ } catch (error) {
112
+ if (hasCode(error, 'ENOENT')) return undefined;
113
+ throw error;
114
+ }
115
+ const manifest = parseJson(source, 'native-prebuilds.json');
116
+ validateManifestHeader(manifest, expectedVersion);
117
+ if (!Array.isArray(manifest.artifacts))
118
+ throw new NativePrebuildIntegrityError('Native prebuild manifest artifacts are invalid.');
119
+ return manifest;
120
+ }
121
+
122
+ export function validateManifestArtifact(manifest, platform, architecture) {
123
+ const artifact = manifest.artifacts.find(
124
+ (candidate) => candidate?.platform === platform && candidate?.architecture === architecture,
125
+ );
126
+ if (artifact === undefined)
127
+ throw new NativePrebuildIntegrityError(`Native prebuild manifest is missing ${tupleKey(platform, architecture)}.`);
128
+ const expectedName = nativeArtifactName(manifest.packageVersion, platform, architecture);
129
+ const expectedBundle = nativeBundleName(manifest.packageVersion, platform, architecture);
130
+ const expectedAttestation = nativeAttestationName(manifest.packageVersion, platform, architecture);
131
+ if (
132
+ artifact.name !== expectedName ||
133
+ artifact.bundle !== expectedBundle ||
134
+ artifact.attestation !== expectedAttestation ||
135
+ !isSha256(artifact.sha256) ||
136
+ !Number.isSafeInteger(artifact.size) ||
137
+ artifact.size <= 0 ||
138
+ artifact.sourceCommit !== manifest.sourceCommit
139
+ )
140
+ throw new NativePrebuildIntegrityError(
141
+ `Native prebuild metadata is invalid for ${tupleKey(platform, architecture)}.`,
142
+ );
143
+ return artifact;
144
+ }
145
+
146
+ export function createManifest(packageVersion, sourceCommit, artifacts) {
147
+ if (!isVersion(packageVersion) || !isCommit(sourceCommit))
148
+ throw new NativePrebuildIntegrityError('Cannot create a native prebuild manifest with invalid release metadata.');
149
+ return {
150
+ schemaVersion: MANIFEST_SCHEMA_VERSION,
151
+ packageName: PACKAGE_NAME,
152
+ packageVersion,
153
+ releaseTag: releaseTag(packageVersion),
154
+ sourceCommit,
155
+ signature: {
156
+ format: 'sigstore-bundle-v0.3',
157
+ certificateIssuer: SIGSTORE_CERTIFICATE_ISSUER,
158
+ certificateIdentityURI: SIGSTORE_CERTIFICATE_IDENTITY,
159
+ sourceDigestOid: SIGSTORE_SOURCE_DIGEST_OID,
160
+ runnerEnvironmentOid: SIGSTORE_RUNNER_ENVIRONMENT_OID,
161
+ },
162
+ artifacts: [...artifacts].sort((left, right) =>
163
+ tupleKey(left.platform, left.architecture).localeCompare(tupleKey(right.platform, right.architecture)),
164
+ ),
165
+ };
166
+ }
167
+
168
+ export function writeIntegrityReceipt(packageRoot, origin, details = {}) {
169
+ const binaryPath = join(packageRoot, NATIVE_BINARY_RELATIVE_PATH);
170
+ const stat = lstatSync(binaryPath);
171
+ if (!stat.isFile() || stat.isSymbolicLink())
172
+ throw new NativePrebuildIntegrityError('Native addon output is not a regular file.');
173
+ const bytes = readFileSync(binaryPath);
174
+ const receipt = {
175
+ schemaVersion: RECEIPT_SCHEMA_VERSION,
176
+ origin,
177
+ algorithm: 'sha256',
178
+ sha256: sha256(bytes),
179
+ size: bytes.byteLength,
180
+ ...details,
181
+ };
182
+ atomicWriteJson(join(packageRoot, NATIVE_RECEIPT_RELATIVE_PATH), receipt);
183
+ return receipt;
184
+ }
185
+
186
+ export function installVerifiedBinary(packageRoot, bytes, artifact) {
187
+ if (bytes.byteLength !== artifact.size || sha256(bytes) !== artifact.sha256)
188
+ throw new NativePrebuildIntegrityError('Verified native prebuild bytes do not match their manifest.');
189
+ const binaryPath = join(packageRoot, NATIVE_BINARY_RELATIVE_PATH);
190
+ const temporary = `${binaryPath}.${randomUUID()}.tmp`;
191
+ mkdirSync(dirname(binaryPath), { recursive: true });
192
+ try {
193
+ writeSynchronizedFile(temporary, bytes, 0o600);
194
+ renameSync(temporary, binaryPath);
195
+ return writeIntegrityReceipt(packageRoot, 'verified-prebuilt', {
196
+ artifact: artifact.name,
197
+ sourceCommit: artifact.sourceCommit,
198
+ signature: 'sigstore',
199
+ });
200
+ } finally {
201
+ rmSync(temporary, { force: true });
202
+ }
203
+ }
204
+
205
+ export function writeJson(path, value) {
206
+ atomicWriteJson(path, value);
207
+ }
208
+
209
+ export function assertArtifactBytes(bytes, artifact) {
210
+ if (bytes.byteLength !== artifact.size)
211
+ throw new NativePrebuildIntegrityError(`Native prebuild ${artifact.name} has an unexpected size.`);
212
+ if (sha256(bytes) !== artifact.sha256)
213
+ throw new NativePrebuildIntegrityError(`Native prebuild ${artifact.name} failed its SHA-256 check.`);
214
+ }
215
+
216
+ function validateManifestHeader(manifest, expectedVersion) {
217
+ if (
218
+ !isRecord(manifest) ||
219
+ manifest.schemaVersion !== MANIFEST_SCHEMA_VERSION ||
220
+ manifest.packageName !== PACKAGE_NAME ||
221
+ manifest.packageVersion !== expectedVersion ||
222
+ manifest.releaseTag !== releaseTag(expectedVersion) ||
223
+ !isCommit(manifest.sourceCommit) ||
224
+ !isRecord(manifest.signature) ||
225
+ manifest.signature.format !== 'sigstore-bundle-v0.3' ||
226
+ manifest.signature.certificateIssuer !== SIGSTORE_CERTIFICATE_ISSUER ||
227
+ manifest.signature.certificateIdentityURI !== SIGSTORE_CERTIFICATE_IDENTITY ||
228
+ manifest.signature.sourceDigestOid !== SIGSTORE_SOURCE_DIGEST_OID ||
229
+ manifest.signature.runnerEnvironmentOid !== SIGSTORE_RUNNER_ENVIRONMENT_OID
230
+ )
231
+ throw new NativePrebuildIntegrityError('Native prebuild manifest header is invalid.');
232
+ }
233
+
234
+ function atomicWriteJson(path, value) {
235
+ const temporary = `${path}.${randomUUID()}.tmp`;
236
+ mkdirSync(dirname(path), { recursive: true });
237
+ try {
238
+ writeSynchronizedFile(temporary, Buffer.from(`${JSON.stringify(value, null, 2)}\n`), 0o600);
239
+ renameSync(temporary, path);
240
+ } finally {
241
+ rmSync(temporary, { force: true });
242
+ }
243
+ }
244
+
245
+ function writeSynchronizedFile(path, bytes, mode) {
246
+ const descriptor = openSync(path, 'wx', mode);
247
+ try {
248
+ writeFileSync(descriptor, bytes);
249
+ fsyncSync(descriptor);
250
+ } finally {
251
+ closeSync(descriptor);
252
+ }
253
+ }
254
+
255
+ function parseJson(source, label) {
256
+ try {
257
+ const parsed = JSON.parse(source);
258
+ if (!isRecord(parsed)) throw new Error('expected an object');
259
+ return parsed;
260
+ } catch (error) {
261
+ throw new NativePrebuildIntegrityError(`${label} is not valid JSON metadata.`, { cause: error });
262
+ }
263
+ }
264
+
265
+ function encodeDerUtf8String(value) {
266
+ if (value.byteLength >= 128)
267
+ throw new NativePrebuildIntegrityError('The Sigstore certificate OID claim is too large.');
268
+ return Buffer.concat([Buffer.from([0x0c, value.byteLength]), value]);
269
+ }
270
+
271
+ function isRecord(value) {
272
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
273
+ }
274
+
275
+ function isVersion(value) {
276
+ return typeof value === 'string' && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(value);
277
+ }
278
+
279
+ function isCommit(value) {
280
+ return typeof value === 'string' && /^[0-9a-f]{40}$/.test(value);
281
+ }
282
+
283
+ function isSha256(value) {
284
+ return typeof value === 'string' && /^[0-9a-f]{64}$/.test(value);
285
+ }
286
+
287
+ function hasCode(error, code) {
288
+ return typeof error === 'object' && error !== null && 'code' in error && error.code === code;
289
+ }