@holoscript/holosystem 0.2.2 → 0.2.4

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,578 @@
1
+ import { createHash, createPublicKey } from 'node:crypto';
2
+
3
+ export const HOLOSYSTEM_SUBSTRATE_IMPORT_SCHEMA = 'holoscript.holosystem.substrate-import.v1';
4
+
5
+ const PORTABLE_SOURCE_PATTERN = /^(?:git\+https|https|holorepo|npm):\/\//u;
6
+ const FLOATING_VERSION_PATTERN =
7
+ /(?:^|[\s:])(?:latest|main|master|next|workspace|file|link|portal|git|https?)(?:$|[\s:])|[*^~<>=|]/iu;
8
+
9
+ function isRecord(value) {
10
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
11
+ }
12
+
13
+ function compareText(left, right) {
14
+ return String(left || '').localeCompare(String(right || ''));
15
+ }
16
+
17
+ function issue(issues, code, path, message) {
18
+ issues.push({ code, path, message });
19
+ }
20
+
21
+ function validId(value) {
22
+ return typeof value === 'string' && /^[a-z0-9][a-z0-9._-]{0,127}$/iu.test(value);
23
+ }
24
+
25
+ function pinnedVersion(value) {
26
+ return (
27
+ typeof value === 'string' &&
28
+ value.trim().length > 0 &&
29
+ value.length <= 160 &&
30
+ !FLOATING_VERSION_PATTERN.test(value)
31
+ );
32
+ }
33
+
34
+ function portableSource(value) {
35
+ if (
36
+ typeof value !== 'string' ||
37
+ value.length > 512 ||
38
+ !PORTABLE_SOURCE_PATTERN.test(value) ||
39
+ value.includes('..')
40
+ ) {
41
+ return false;
42
+ }
43
+ try {
44
+ const parsed = new URL(value);
45
+ return !parsed.username && !parsed.password && !parsed.search && !parsed.hash;
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
50
+
51
+ function pinnedRevision(value) {
52
+ return (
53
+ typeof value === 'string' &&
54
+ value.trim().length > 0 &&
55
+ value.length <= 200 &&
56
+ !/^(?:latest|main|master|next|head)$/iu.test(value.trim())
57
+ );
58
+ }
59
+
60
+ function stableValue(value) {
61
+ if (Array.isArray(value)) return value.map(stableValue);
62
+ if (!isRecord(value)) return value;
63
+ return Object.fromEntries(
64
+ Object.keys(value)
65
+ .sort(compareText)
66
+ .map((key) => [key, stableValue(value[key])])
67
+ );
68
+ }
69
+
70
+ function hashJson(value) {
71
+ return `sha256:${createHash('sha256')
72
+ .update(JSON.stringify(stableValue(value)))
73
+ .digest('hex')}`;
74
+ }
75
+
76
+ function hashReceipt(value) {
77
+ return `sha256:${createHash('sha256').update(JSON.stringify(value)).digest('hex')}`;
78
+ }
79
+
80
+ function parseIntegrity(value) {
81
+ if (typeof value !== 'string' || value.length > 1024) return null;
82
+ const tokens = value.trim().split(/\s+/u);
83
+ for (const algorithm of ['sha512', 'sha256']) {
84
+ const token = tokens.find((candidate) => candidate.startsWith(`${algorithm}-`));
85
+ if (!token) continue;
86
+ const encoded = token.slice(algorithm.length + 1);
87
+ if (
88
+ encoded.length === 0 ||
89
+ encoded.length % 4 !== 0 ||
90
+ !/^[A-Za-z0-9+/]+={0,2}$/u.test(encoded)
91
+ ) {
92
+ continue;
93
+ }
94
+ const decoded = Buffer.from(encoded, 'base64');
95
+ const expectedBytes = algorithm === 'sha512' ? 64 : 32;
96
+ if (decoded.length !== expectedBytes || decoded.toString('base64') !== encoded) continue;
97
+ return `${algorithm}:${decoded.toString('hex')}`;
98
+ }
99
+ return null;
100
+ }
101
+
102
+ function sanitizeVerificationPolicy(value, issues) {
103
+ if (value == null) return null;
104
+ const minimum = value?.minimumIndependentRebuilds;
105
+ if (!Number.isInteger(minimum) || minimum < 1 || minimum > 16) {
106
+ issue(
107
+ issues,
108
+ 'verification-policy-minimum-invalid',
109
+ 'verificationPolicy.minimumIndependentRebuilds',
110
+ 'minimumIndependentRebuilds must be an integer from 1 to 16.'
111
+ );
112
+ }
113
+ if (!Array.isArray(value?.trustRoots) || value.trustRoots.length === 0) {
114
+ issue(
115
+ issues,
116
+ 'verification-policy-roots-missing',
117
+ 'verificationPolicy.trustRoots',
118
+ 'Verification policy must contain at least one Ed25519 public trust root.'
119
+ );
120
+ }
121
+
122
+ const trustRoots = [];
123
+ for (const [index, root] of (Array.isArray(value?.trustRoots)
124
+ ? value.trustRoots
125
+ : []
126
+ ).entries()) {
127
+ const path = `verificationPolicy.trustRoots[${index}]`;
128
+ const verifier = validId(root?.verifier) ? root.verifier : null;
129
+ const trustDomain = validId(root?.trustDomain) ? root.trustDomain : null;
130
+ let publicKey = null;
131
+ if (!verifier) {
132
+ issue(
133
+ issues,
134
+ 'verification-policy-verifier-invalid',
135
+ `${path}.verifier`,
136
+ 'Verifier id must be portable.'
137
+ );
138
+ }
139
+ if (!trustDomain) {
140
+ issue(
141
+ issues,
142
+ 'verification-policy-domain-invalid',
143
+ `${path}.trustDomain`,
144
+ 'Trust domain must be portable.'
145
+ );
146
+ }
147
+ try {
148
+ if (
149
+ typeof root?.publicKey !== 'string' ||
150
+ root.publicKey.length > 8192 ||
151
+ !root.publicKey.startsWith('-----BEGIN PUBLIC KEY-----') ||
152
+ !root.publicKey.trimEnd().endsWith('-----END PUBLIC KEY-----')
153
+ ) {
154
+ throw new TypeError('Expected SPKI public key PEM.');
155
+ }
156
+ const key = createPublicKey(root.publicKey);
157
+ if (key.asymmetricKeyType !== 'ed25519') throw new TypeError('Expected Ed25519 key.');
158
+ publicKey = root.publicKey;
159
+ } catch {
160
+ issue(
161
+ issues,
162
+ 'verification-policy-key-invalid',
163
+ `${path}.publicKey`,
164
+ 'Trust root must contain an Ed25519 public key; private key material is rejected.'
165
+ );
166
+ }
167
+ trustRoots.push({ verifier, trustDomain, publicKey });
168
+ }
169
+ trustRoots.sort(
170
+ (left, right) =>
171
+ compareText(left.verifier, right.verifier) || compareText(left.trustDomain, right.trustDomain)
172
+ );
173
+ return {
174
+ minimumIndependentRebuilds:
175
+ Number.isInteger(minimum) && minimum >= 1 && minimum <= 16 ? minimum : null,
176
+ trustRoots,
177
+ };
178
+ }
179
+
180
+ export const _substrateImportInternals = Object.freeze({
181
+ compareText,
182
+ hashJson,
183
+ hashReceipt,
184
+ isRecord,
185
+ issue,
186
+ pinnedRevision,
187
+ pinnedVersion,
188
+ portableSource,
189
+ sanitizeVerificationPolicy,
190
+ validId,
191
+ });
192
+
193
+ function packageNameFromPath(path) {
194
+ const marker = 'node_modules/';
195
+ const index = path.lastIndexOf(marker);
196
+ return index === -1 ? path : path.slice(index + marker.length);
197
+ }
198
+
199
+ function resolvedSource(entry, path) {
200
+ if (entry?.resolved === 'registry.npmjs.org' && pinnedVersion(entry?.version)) {
201
+ return `npm://configured-registry/${encodeURIComponent(packageNameFromPath(path))}/${encodeURIComponent(entry.version)}`;
202
+ }
203
+ return entry?.link !== true && portableSource(entry?.resolved) ? entry.resolved : null;
204
+ }
205
+
206
+ function componentId(path) {
207
+ const name = packageNameFromPath(path)
208
+ .toLowerCase()
209
+ .replace(/^@/u, '')
210
+ .replace(/[^a-z0-9._-]+/gu, '-')
211
+ .replace(/^-+|-+$/gu, '')
212
+ .slice(0, 80);
213
+ const suffix = createHash('sha256').update(path).digest('hex').slice(0, 12);
214
+ return `npm-${name || 'package'}-${suffix}`;
215
+ }
216
+
217
+ function missingComponentId(fromPath, dependency) {
218
+ const suffix = createHash('sha256')
219
+ .update(`${fromPath}\0${dependency}`)
220
+ .digest('hex')
221
+ .slice(0, 12);
222
+ return `npm-missing-${suffix}`;
223
+ }
224
+
225
+ function validPackagePath(path) {
226
+ return (
227
+ typeof path === 'string' &&
228
+ path.length <= 512 &&
229
+ path.startsWith('node_modules/') &&
230
+ !path.includes('\\') &&
231
+ !path.split('/').includes('..')
232
+ );
233
+ }
234
+
235
+ function parentPackagePath(path) {
236
+ const index = path.lastIndexOf('/node_modules/');
237
+ return index === -1 ? '' : path.slice(0, index);
238
+ }
239
+
240
+ function resolveDependencyPath(packages, fromPath, dependency) {
241
+ let current = fromPath;
242
+ while (true) {
243
+ const candidate = current
244
+ ? `${current}/node_modules/${dependency}`
245
+ : `node_modules/${dependency}`;
246
+ if (Object.hasOwn(packages, candidate)) return candidate;
247
+ if (!current) return null;
248
+ current = parentPackagePath(current);
249
+ }
250
+ }
251
+
252
+ function dependencyRelations(entry, { includeDev, root }) {
253
+ const relations = new Map();
254
+ const add = (values, type, optional = false) => {
255
+ if (!isRecord(values)) return;
256
+ for (const name of Object.keys(values).sort(compareText)) {
257
+ if (!relations.has(name)) relations.set(name, { name, type, optional });
258
+ }
259
+ };
260
+ add(entry?.optionalDependencies, 'optional', true);
261
+ add(entry?.dependencies, 'runtime');
262
+ if (!root) {
263
+ add(entry?.peerDependencies, 'peer');
264
+ for (const [name, meta] of Object.entries(entry?.peerDependenciesMeta || {})) {
265
+ if (meta?.optional === true && relations.get(name)?.type === 'peer') {
266
+ relations.set(name, { name, type: 'peer', optional: true });
267
+ }
268
+ }
269
+ }
270
+ if (root && includeDev) add(entry?.devDependencies, 'development');
271
+ return [...relations.values()].sort((left, right) => compareText(left.name, right.name));
272
+ }
273
+
274
+ function normalizeRoot(root, rootEntry, lockfile, issues) {
275
+ const id = validId(root?.id) ? root.id : null;
276
+ const versionCandidate = root?.version || rootEntry?.version || lockfile?.version;
277
+ const version = pinnedVersion(versionCandidate) ? versionCandidate : null;
278
+ const custodyMode = root?.custody?.mode;
279
+ const custodyOwner = validId(root?.custody?.owner) ? root.custody.owner : null;
280
+ const trustDomain = validId(root?.custody?.trustDomain) ? root.custody.trustDomain : null;
281
+ const sourceUri = portableSource(root?.source?.uri) ? root.source.uri : null;
282
+ const sourceRevision = pinnedRevision(root?.source?.revision) ? root.source.revision : null;
283
+
284
+ if (!id) issue(issues, 'root-id-invalid', 'root.id', 'Root id must be a portable identifier.');
285
+ if (!version) {
286
+ issue(issues, 'root-version-not-pinned', 'root.version', 'Root version must be exact.');
287
+ }
288
+ if (custodyMode !== 'owned' && custodyMode !== 'external') {
289
+ issue(issues, 'root-custody-invalid', 'root.custody.mode', 'Root custody must be explicit.');
290
+ }
291
+ if (!custodyOwner) {
292
+ issue(issues, 'root-owner-invalid', 'root.custody.owner', 'Root custody owner is required.');
293
+ }
294
+ if (!trustDomain) {
295
+ issue(
296
+ issues,
297
+ 'root-trust-domain-invalid',
298
+ 'root.custody.trustDomain',
299
+ 'Root trust domain is required.'
300
+ );
301
+ }
302
+ if (!sourceUri) {
303
+ issue(
304
+ issues,
305
+ 'root-source-not-portable',
306
+ 'root.source.uri',
307
+ 'Root source must be a portable URI without credentials.'
308
+ );
309
+ }
310
+ if (!sourceRevision) {
311
+ issue(
312
+ issues,
313
+ 'root-revision-not-pinned',
314
+ 'root.source.revision',
315
+ 'Root source revision must be pinned.'
316
+ );
317
+ }
318
+
319
+ return {
320
+ id,
321
+ kind: 'npm-lock-root',
322
+ version,
323
+ custody: {
324
+ mode: custodyMode === 'owned' || custodyMode === 'external' ? custodyMode : null,
325
+ owner: custodyOwner,
326
+ trustDomain,
327
+ },
328
+ source: { uri: sourceUri, revision: sourceRevision },
329
+ artifact: { digest: hashJson(lockfile) },
330
+ execution: { installScripts: rootEntry?.hasInstallScript === true ? 'present' : 'none' },
331
+ requires: [],
332
+ verification: { rebuilds: [] },
333
+ };
334
+ }
335
+
336
+ export function importNpmPackageLock({
337
+ lockfile,
338
+ root,
339
+ verificationPolicy = null,
340
+ includeDev = false,
341
+ externalCustody = { owner: 'npm-registry', trustDomain: 'npm-registry' },
342
+ now = new Date(),
343
+ } = {}) {
344
+ const issues = [];
345
+ const sanitizedVerificationPolicy = sanitizeVerificationPolicy(verificationPolicy, issues);
346
+ const supportedVersion = lockfile?.lockfileVersion === 2 || lockfile?.lockfileVersion === 3;
347
+ const packages = isRecord(lockfile?.packages) ? lockfile.packages : {};
348
+ if (!supportedVersion || !isRecord(lockfile?.packages)) {
349
+ issue(
350
+ issues,
351
+ 'lockfile-version-unsupported',
352
+ 'lockfile.lockfileVersion',
353
+ 'Only npm package-lock v2 and v3 files with a packages graph are supported.'
354
+ );
355
+ }
356
+ const rootEntry = isRecord(packages['']) ? packages[''] : null;
357
+ if (supportedVersion && !rootEntry) {
358
+ issue(
359
+ issues,
360
+ 'lockfile-root-missing',
361
+ 'lockfile.packages',
362
+ 'The package-lock packages graph must contain the root entry.'
363
+ );
364
+ }
365
+
366
+ const normalizedRoot = normalizeRoot(root, rootEntry, lockfile, issues);
367
+ const custodyOwner = validId(externalCustody?.owner) ? externalCustody.owner : null;
368
+ const custodyTrustDomain = validId(externalCustody?.trustDomain)
369
+ ? externalCustody.trustDomain
370
+ : null;
371
+ if (!custodyOwner || !custodyTrustDomain) {
372
+ issue(
373
+ issues,
374
+ 'external-custody-invalid',
375
+ 'externalCustody',
376
+ 'External registry custody owner and trust domain must be portable identifiers.'
377
+ );
378
+ }
379
+
380
+ const idsByPath = new Map();
381
+ for (const path of Object.keys(packages).sort(compareText)) {
382
+ if (path === '') continue;
383
+ if (!validPackagePath(path)) {
384
+ issue(
385
+ issues,
386
+ 'package-path-invalid',
387
+ 'lockfile.packages',
388
+ 'A package-lock entry uses a non-portable package path.'
389
+ );
390
+ continue;
391
+ }
392
+ idsByPath.set(path, componentId(path));
393
+ }
394
+
395
+ const reachable = new Set(['']);
396
+ const queue = rootEntry ? [''] : [];
397
+ const requirementsByPath = new Map();
398
+ let skippedOptionalDependencies = 0;
399
+ for (let queueIndex = 0; queueIndex < queue.length; queueIndex += 1) {
400
+ const fromPath = queue[queueIndex];
401
+ const entry = packages[fromPath];
402
+ const requirements = [];
403
+ for (const relation of dependencyRelations(entry, {
404
+ includeDev: includeDev === true,
405
+ root: fromPath === '',
406
+ })) {
407
+ const resolvedPath = resolveDependencyPath(packages, fromPath, relation.name);
408
+ if (!resolvedPath) {
409
+ if (relation.optional) {
410
+ skippedOptionalDependencies += 1;
411
+ continue;
412
+ }
413
+ const pathLabel = fromPath || '<root>';
414
+ issue(
415
+ issues,
416
+ 'dependency-lock-entry-missing',
417
+ `lockfile.packages.${pathLabel}.dependencies`,
418
+ 'A required dependency does not resolve to a package-lock entry.'
419
+ );
420
+ requirements.push({
421
+ id: missingComponentId(fromPath, relation.name),
422
+ type: relation.type,
423
+ });
424
+ continue;
425
+ }
426
+ const dependencyId = idsByPath.get(resolvedPath);
427
+ if (!dependencyId) {
428
+ issue(
429
+ issues,
430
+ 'dependency-package-path-invalid',
431
+ 'lockfile.packages',
432
+ 'A dependency resolves through a non-portable package path.'
433
+ );
434
+ continue;
435
+ }
436
+ requirements.push({ id: dependencyId, type: relation.type });
437
+ if (!reachable.has(resolvedPath)) {
438
+ reachable.add(resolvedPath);
439
+ queue.push(resolvedPath);
440
+ }
441
+ }
442
+ requirements.sort(
443
+ (left, right) => compareText(left.id, right.id) || compareText(left.type, right.type)
444
+ );
445
+ requirementsByPath.set(fromPath, requirements);
446
+ }
447
+
448
+ normalizedRoot.requires = requirementsByPath.get('') || [];
449
+ const components = [normalizedRoot];
450
+ for (const path of [...reachable].filter(Boolean).sort(compareText)) {
451
+ const entry = packages[path];
452
+ const packagePath = `lockfile.packages.${path}`;
453
+ const version = pinnedVersion(entry?.version) ? entry.version : null;
454
+ const sourceUri = resolvedSource(entry, path);
455
+ const artifactDigest = parseIntegrity(entry?.integrity);
456
+ if (!version) {
457
+ issue(
458
+ issues,
459
+ 'package-version-not-pinned',
460
+ `${packagePath}.version`,
461
+ 'Resolved package version must be exact.'
462
+ );
463
+ }
464
+ if (!sourceUri) {
465
+ issue(
466
+ issues,
467
+ 'package-source-not-portable',
468
+ `${packagePath}.resolved`,
469
+ 'Resolved package source must be a portable URI without credentials.'
470
+ );
471
+ }
472
+ if (!artifactDigest) {
473
+ issue(
474
+ issues,
475
+ 'package-integrity-invalid',
476
+ `${packagePath}.integrity`,
477
+ 'Resolved package integrity must contain a canonical sha256 or sha512 digest.'
478
+ );
479
+ }
480
+ components.push({
481
+ id: idsByPath.get(path),
482
+ kind: 'npm-package',
483
+ version,
484
+ custody: {
485
+ mode: 'external',
486
+ owner: custodyOwner,
487
+ trustDomain: custodyTrustDomain,
488
+ },
489
+ source: { uri: sourceUri, revision: artifactDigest },
490
+ artifact: { digest: artifactDigest },
491
+ execution: { installScripts: entry?.hasInstallScript === true ? 'present' : 'none' },
492
+ requires: requirementsByPath.get(path) || [],
493
+ verification: { rebuilds: [] },
494
+ });
495
+ }
496
+ components.sort((left, right) => compareText(left.id, right.id));
497
+
498
+ if (
499
+ normalizedRoot.id &&
500
+ components.some(
501
+ (component) => component !== normalizedRoot && component.id === normalizedRoot.id
502
+ )
503
+ ) {
504
+ issue(
505
+ issues,
506
+ 'component-id-collision',
507
+ 'root.id',
508
+ 'Root id collides with a generated package component id.'
509
+ );
510
+ }
511
+ issues.sort(
512
+ (left, right) => compareText(left.path, right.path) || compareText(left.code, right.code)
513
+ );
514
+
515
+ const dependencies = components.reduce(
516
+ (count, component) => count + component.requires.length,
517
+ 0
518
+ );
519
+ const installScriptPackages = components.filter(
520
+ (component) => component.execution.installScripts === 'present'
521
+ ).length;
522
+ const configuredRegistryReferences = components.filter((component) =>
523
+ component.source.uri?.startsWith('npm://configured-registry/')
524
+ ).length;
525
+ const importable = issues.length === 0;
526
+ const input = {
527
+ root: normalizedRoot.id,
528
+ coverage: {
529
+ includedLayers: ['npm'],
530
+ missingLayers: ['native-build', 'operating-system'],
531
+ },
532
+ verificationPolicy: sanitizedVerificationPolicy,
533
+ components,
534
+ };
535
+ const receipt = {
536
+ schema: HOLOSYSTEM_SUBSTRATE_IMPORT_SCHEMA,
537
+ generatedAt: now.toISOString(),
538
+ status: importable
539
+ ? installScriptPackages > 0
540
+ ? 'execution-policy-required'
541
+ : 'coverage-and-attestation-required'
542
+ : 'blocked',
543
+ importable,
544
+ source: {
545
+ format: 'npm-package-lock',
546
+ lockfileVersion: supportedVersion ? lockfile.lockfileVersion : null,
547
+ lockfileHash: hashJson(lockfile),
548
+ includeDev: includeDev === true,
549
+ },
550
+ summary: {
551
+ components: components.length,
552
+ dependencies,
553
+ missingAttestations: components.length,
554
+ installScriptPackages,
555
+ configuredRegistryReferences,
556
+ omittedDevRootDependencies:
557
+ includeDev === true || !isRecord(rootEntry?.devDependencies)
558
+ ? 0
559
+ : Object.keys(rootEntry.devDependencies).length,
560
+ skippedOptionalDependencies,
561
+ issues: issues.length,
562
+ },
563
+ input,
564
+ issues,
565
+ boundaries: {
566
+ lockfileProvesResolutionNotRebuild: true,
567
+ generatedComponentsRequireSignedAttestations: true,
568
+ registryCustodyRemainsExternal: true,
569
+ rootArtifactDigestRepresentsLockfile: true,
570
+ optionalBranchesAreConservativeSuperset: true,
571
+ lifecycleScriptsBlockSubstrateClosure: true,
572
+ nativeAndOsDependenciesAreNotDerived: true,
573
+ installScriptsWereNotExecuted: true,
574
+ },
575
+ };
576
+ receipt.receiptHash = hashReceipt({ ...receipt, generatedAt: null });
577
+ return receipt;
578
+ }