@mnemonik/shared 7.97.1 → 7.98.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/dist/ambientPointer.d.ts.map +1 -1
  2. package/dist/ambientPointer.js +2 -1
  3. package/dist/ambientPointer.js.map +1 -1
  4. package/dist/apiOrigin.d.ts +3 -0
  5. package/dist/apiOrigin.d.ts.map +1 -0
  6. package/dist/apiOrigin.js +5 -0
  7. package/dist/apiOrigin.js.map +1 -0
  8. package/dist/ast/astChunker.d.ts +12 -12
  9. package/dist/ast/astChunker.js +32 -32
  10. package/dist/ast/grammars.d.ts +9 -9
  11. package/dist/ast/grammars.js +15 -15
  12. package/dist/codeScanner.d.ts +56 -43
  13. package/dist/codeScanner.d.ts.map +1 -1
  14. package/dist/codeScanner.js +262 -143
  15. package/dist/codeScanner.js.map +1 -1
  16. package/dist/contextBudget.js +2 -2
  17. package/dist/contextBudget.js.map +1 -1
  18. package/dist/gitEditDetector.d.ts +6 -6
  19. package/dist/gitEditDetector.js +7 -7
  20. package/dist/hookRuntime.d.ts +35 -2
  21. package/dist/hookRuntime.d.ts.map +1 -1
  22. package/dist/hookRuntime.js +127 -33
  23. package/dist/hookRuntime.js.map +1 -1
  24. package/dist/hookTimeouts.d.ts +6 -6
  25. package/dist/hookTimeouts.js +6 -6
  26. package/dist/hostAdapter.d.ts +135 -0
  27. package/dist/hostAdapter.d.ts.map +1 -0
  28. package/dist/hostAdapter.js +323 -0
  29. package/dist/hostAdapter.js.map +1 -0
  30. package/dist/hostBinary.d.ts +29 -0
  31. package/dist/hostBinary.d.ts.map +1 -0
  32. package/dist/hostBinary.js +179 -0
  33. package/dist/hostBinary.js.map +1 -0
  34. package/dist/index.d.ts +11 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +10 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/instructions.d.ts +26 -26
  39. package/dist/instructions.js +26 -26
  40. package/dist/jitDeliveryAck.d.ts.map +1 -1
  41. package/dist/jitDeliveryAck.js +2 -0
  42. package/dist/jitDeliveryAck.js.map +1 -1
  43. package/dist/projectIdentityFile.d.ts +25 -0
  44. package/dist/projectIdentityFile.d.ts.map +1 -0
  45. package/dist/projectIdentityFile.js +87 -0
  46. package/dist/projectIdentityFile.js.map +1 -0
  47. package/dist/projectSetupHandoff.d.ts +24 -0
  48. package/dist/projectSetupHandoff.d.ts.map +1 -0
  49. package/dist/projectSetupHandoff.js +249 -0
  50. package/dist/projectSetupHandoff.js.map +1 -0
  51. package/dist/protectedPaths.d.ts +9 -0
  52. package/dist/protectedPaths.d.ts.map +1 -0
  53. package/dist/protectedPaths.js +66 -0
  54. package/dist/protectedPaths.js.map +1 -0
  55. package/dist/readiness.d.ts +143 -0
  56. package/dist/readiness.d.ts.map +1 -0
  57. package/dist/readiness.js +304 -0
  58. package/dist/readiness.js.map +1 -0
  59. package/dist/repositoryFingerprint.d.ts +50 -0
  60. package/dist/repositoryFingerprint.d.ts.map +1 -0
  61. package/dist/repositoryFingerprint.js +160 -0
  62. package/dist/repositoryFingerprint.js.map +1 -0
  63. package/dist/repositoryRoot.d.ts +56 -0
  64. package/dist/repositoryRoot.d.ts.map +1 -0
  65. package/dist/repositoryRoot.js +226 -0
  66. package/dist/repositoryRoot.js.map +1 -0
  67. package/dist/runtimeReader.d.ts +102 -0
  68. package/dist/runtimeReader.d.ts.map +1 -0
  69. package/dist/runtimeReader.js +408 -0
  70. package/dist/runtimeReader.js.map +1 -0
  71. package/dist/runtimeSigners.d.ts +37 -0
  72. package/dist/runtimeSigners.d.ts.map +1 -0
  73. package/dist/runtimeSigners.js +507 -0
  74. package/dist/runtimeSigners.js.map +1 -0
  75. package/dist/scannerSupervisor.d.ts +35 -0
  76. package/dist/scannerSupervisor.d.ts.map +1 -0
  77. package/dist/scannerSupervisor.js +2 -0
  78. package/dist/scannerSupervisor.js.map +1 -0
  79. package/dist/secretPatterns.d.ts +9 -9
  80. package/dist/secretPatterns.d.ts.map +1 -1
  81. package/dist/secretPatterns.js +27 -25
  82. package/dist/secretPatterns.js.map +1 -1
  83. package/dist/testing/hostAdapterConformance.d.ts +17 -0
  84. package/dist/testing/hostAdapterConformance.d.ts.map +1 -0
  85. package/dist/testing/hostAdapterConformance.js +477 -0
  86. package/dist/testing/hostAdapterConformance.js.map +1 -0
  87. package/dist/usageGuide.d.ts +1 -1
  88. package/dist/usageGuide.d.ts.map +1 -1
  89. package/dist/usageGuide.js +4 -4
  90. package/package.json +7 -2
  91. package/queries/bash.tags.scm +1 -1
  92. package/queries/groovy.tags.scm +1 -1
  93. package/scripts/vendor-grammars.ts +13 -13
  94. package/src/ambientPointer.ts +2 -1
  95. package/src/apiOrigin.ts +4 -0
  96. package/src/ast/astChunker.ts +39 -39
  97. package/src/ast/grammars.ts +16 -16
  98. package/src/codeScanner.ts +274 -149
  99. package/src/contextBudget.ts +2 -2
  100. package/src/gitEditDetector.ts +7 -7
  101. package/src/hookRuntime.ts +162 -34
  102. package/src/hookTimeouts.ts +6 -6
  103. package/src/hostAdapter.ts +468 -0
  104. package/src/hostBinary.ts +205 -0
  105. package/src/index.ts +20 -0
  106. package/src/instructions.ts +26 -26
  107. package/src/jitDeliveryAck.ts +2 -0
  108. package/src/projectIdentityFile.ts +100 -0
  109. package/src/projectSetupHandoff.ts +275 -0
  110. package/src/protectedPaths.ts +88 -0
  111. package/src/readiness.ts +516 -0
  112. package/src/repositoryFingerprint.ts +199 -0
  113. package/src/repositoryRoot.ts +261 -0
  114. package/src/runtimeReader.ts +495 -0
  115. package/src/runtimeSigners.ts +577 -0
  116. package/src/scannerSupervisor.ts +26 -0
  117. package/src/secretPatterns.ts +27 -25
  118. package/src/testing/hostAdapterConformance.ts +538 -0
  119. package/src/usageGuide.ts +4 -4
  120. package/test-fixtures/identity/runner.d.mts +12 -0
  121. package/test-fixtures/identity/runner.mjs +282 -0
  122. package/tests/fixtures/repository-fingerprint-v1.golden.json +834 -0
  123. package/tests/projectIdentity.test.ts +56 -0
  124. package/tests/readiness.test.ts +158 -0
  125. package/tests/repositoryFingerprint.test.ts +26 -0
  126. package/tsconfig.test.json +9 -0
  127. package/wasm/bash.tags.scm +1 -1
  128. package/wasm/groovy.tags.scm +1 -1
  129. package/wasm/javascript.tags.scm +2 -2
  130. package/wasm/solidity.tags.scm +1 -1
@@ -0,0 +1,495 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { constants, type Stats } from 'node:fs';
3
+ import { lstat, open, readdir } from 'node:fs/promises';
4
+ import { dirname, join, parse, resolve, sep } from 'node:path';
5
+ import {
6
+ verifySigner,
7
+ verifyWindowsPermission,
8
+ verifyWindowsAcl,
9
+ prepareWindowsAclDirectory,
10
+ auditWindowsPermissions,
11
+ type WindowsPermission,
12
+ type Execute,
13
+ type Signer,
14
+ } from './runtimeSigners.js';
15
+
16
+ export type HostArtifact = 'claude-code' | 'codex' | 'cursor' | 'grok';
17
+ export type Artifact = 'cli' | 'scanner' | HostArtifact;
18
+ export type Reason =
19
+ | 'digest_mismatch'
20
+ | 'manifest_missing'
21
+ | 'unsigned'
22
+ | 'permission'
23
+ | 'acl_unavailable'
24
+ | 'lock_held';
25
+ export class RuntimeError extends Error {
26
+ constructor(public readonly reason: Reason) {
27
+ super(reason);
28
+ this.name = 'RuntimeError';
29
+ }
30
+ }
31
+ export const hash = (bytes: Buffer | string): string =>
32
+ createHash('sha256').update(bytes).digest('hex');
33
+ export function safePath(path: string): string {
34
+ if (
35
+ typeof path !== 'string' ||
36
+ !/^[a-zA-Z0-9_@.+/-]+$/.test(path) ||
37
+ path.split('/').some((p) => !p || p === '.' || p === '..') ||
38
+ path.includes(':')
39
+ )
40
+ throw new RuntimeError('permission');
41
+ return path;
42
+ }
43
+ export interface NpmReceipt {
44
+ name: string;
45
+ version: string;
46
+ integrity: string;
47
+ tarball: string;
48
+ tarballSha256: string;
49
+ }
50
+ export interface Manifest {
51
+ schemaVersion: 1;
52
+ artifact: Artifact;
53
+ version: string;
54
+ entry: string;
55
+ files: Record<string, { sha256: string; size: number; executable: boolean }>;
56
+ totalSize: number;
57
+ source:
58
+ | { kind: 'npm'; packages: NpmReceipt[]; launchedFrom: string }
59
+ | { kind: 'release'; url: string };
60
+ signer?: Signer;
61
+ signingStatus?: 'unsigned' | 'signed';
62
+ disclosureVersion?: string;
63
+ }
64
+ export interface RuntimeSource {
65
+ manifest: Manifest;
66
+ files: Record<string, Buffer>;
67
+ }
68
+ export interface Reference {
69
+ version: string;
70
+ manifestSha256: string;
71
+ }
72
+ interface Pointer {
73
+ current: Reference;
74
+ previous?: Reference;
75
+ }
76
+ export interface Verified {
77
+ directory: string;
78
+ manifest: Manifest;
79
+ entry: string;
80
+ reference: Reference;
81
+ }
82
+ export const missing = (e: unknown): boolean => (e as NodeJS.ErrnoException).code === 'ENOENT';
83
+ export const versionName = (v: string): string => {
84
+ if (!/^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(v)) throw new RuntimeError('permission');
85
+ return v;
86
+ };
87
+
88
+ const statIdentity = (stat: Stats): string =>
89
+ [stat.dev, stat.ino, stat.mode, stat.uid, stat.size, stat.mtimeMs, stat.ctimeMs].join(':');
90
+
91
+ /** Only stdlib and the accepted bootstrap's signer adapter may load before verifyRuntime returns. */
92
+ export class RuntimeReader {
93
+ readonly state: string;
94
+ private static readonly windowsSignatures = new Map<string, Promise<void>>();
95
+ // Omitted batch verdicts use the per-path fallback before entering the cache.
96
+ private readonly windowsAudit = new Map<
97
+ string,
98
+ { verdict: WindowsPermission; checkedAt: number }
99
+ >();
100
+ private readonly windowsStats = new Map<string, string>();
101
+ private readonly windowsDirectories = new Set<string>();
102
+ private audits: Promise<void> = Promise.resolve();
103
+ private readonly auditsInFlight = new Map<string, Promise<void>>();
104
+ private activeOperation = false;
105
+ private operations: Promise<unknown> = Promise.resolve();
106
+ constructor(
107
+ state: string,
108
+ readonly run?: Execute,
109
+ readonly options: { allowUnsigned?: boolean; cacheDirectories?: boolean } = {},
110
+ private readonly now = () => performance.now()
111
+ ) {
112
+ this.state = resolve(state);
113
+ }
114
+ pointerPath(artifact: Artifact): string {
115
+ if (!['cli', 'scanner', 'claude-code', 'codex', 'cursor', 'grok'].includes(artifact))
116
+ throw new RuntimeError('permission');
117
+ return join(this.state, 'runtimes', artifact, 'current');
118
+ }
119
+ private guardedPath(path: string): string {
120
+ const absolute = resolve(path);
121
+ if (
122
+ (absolute !== this.state && !absolute.startsWith(this.state + sep)) ||
123
+ (process.platform === 'win32' && /[\r\n\t]/.test(absolute))
124
+ )
125
+ throw new RuntimeError('permission');
126
+ return absolute;
127
+ }
128
+ private auditRoot(path: string): string {
129
+ const [area, artifact] = path.slice(this.state.length + 1).split(sep);
130
+ return area === 'runtimes' ? join(this.state, area, ...(artifact ? [artifact] : [])) : path;
131
+ }
132
+ private traceAudit(event: Record<string, unknown>): void {
133
+ const level = process.env.MNEMONIK_AUDIT_TRACE;
134
+ if (level !== '1' && (level !== 'misses' || event.cache === 'hit')) return;
135
+ process.stderr.write(
136
+ JSON.stringify({
137
+ type: 'runtime-audit',
138
+ pid: process.pid,
139
+ module: import.meta.url,
140
+ activeOperation: this.activeOperation,
141
+ ...event,
142
+ }) + '\n'
143
+ );
144
+ }
145
+ private invalidateAudit(path: string): void {
146
+ const root = this.auditRoot(path);
147
+ for (const key of this.windowsAudit.keys())
148
+ if (
149
+ key === root ||
150
+ ((!this.options.cacheDirectories || root.startsWith(join(this.state, 'runtimes') + sep)) &&
151
+ key.startsWith(root + sep))
152
+ )
153
+ this.windowsAudit.delete(key);
154
+ }
155
+ private audit(paths: string[]): Promise<void> {
156
+ const root = this.auditRoot(paths.at(-1) ?? this.state);
157
+ const pending = this.auditsInFlight.get(root);
158
+ if (pending) return pending;
159
+ const audit = this.audits
160
+ .then(() => this.auditPending(paths))
161
+ .finally(() => this.auditsInFlight.delete(root));
162
+ this.auditsInFlight.set(root, audit);
163
+ this.audits = audit.catch(() => {});
164
+ return audit;
165
+ }
166
+ private async auditPending(paths: string[]): Promise<void> {
167
+ if (process.platform !== 'win32') return;
168
+ const pending = new Set<string>();
169
+ for (const path of paths) {
170
+ let part = this.guardedPath(path);
171
+ while (true) {
172
+ const cached = this.windowsAudit.get(part);
173
+ const expired =
174
+ !!cached &&
175
+ !this.activeOperation &&
176
+ !(this.options.cacheDirectories && this.windowsDirectories.has(part)) &&
177
+ this.now() - cached.checkedAt >= 2_000;
178
+ this.traceAudit({
179
+ event: 'cache',
180
+ requested: path,
181
+ path: part,
182
+ root: this.auditRoot(path),
183
+ cache: !cached ? 'miss' : expired ? 'ttl-expired' : 'hit',
184
+ });
185
+ if (!cached || expired) pending.add(part);
186
+ if (part === this.state) break;
187
+ part = dirname(part);
188
+ }
189
+ }
190
+ if (!pending.size) return;
191
+ try {
192
+ const root = this.auditRoot(paths.at(-1) ?? this.state);
193
+ const ancestors = [...pending].filter(
194
+ (path) => path !== root && !path.startsWith(root + sep)
195
+ );
196
+ const tree = [...pending].filter((path) => !ancestors.includes(path));
197
+ for (const part of [...ancestors, ...(tree.length ? [root] : [])]) {
198
+ const recursive = part === root && root.startsWith(join(this.state, 'runtimes') + sep);
199
+ this.traceAudit({ event: 'audit', root: part, recursive });
200
+ const requested = part === root ? tree : [part];
201
+ if (
202
+ this.options.cacheDirectories &&
203
+ !this.windowsDirectories.has(part) &&
204
+ this.windowsAudit.get(part)?.verdict === 'ok'
205
+ ) {
206
+ await verifyWindowsAcl(part, this.run, this.state);
207
+ this.windowsAudit.set(part, { verdict: 'ok', checkedAt: this.now() });
208
+ continue;
209
+ }
210
+ const verdicts = await auditWindowsPermissions(
211
+ part,
212
+ requested,
213
+ this.run,
214
+ recursive,
215
+ this.state
216
+ );
217
+ const checkedAt = this.now();
218
+ for (const path of new Set([...requested, ...verdicts.keys()])) {
219
+ let verdict = verdicts.get(path);
220
+ if (verdict === undefined) {
221
+ await verifyWindowsPermission(path, this.run, this.state);
222
+ verdict = 'ok';
223
+ }
224
+ this.windowsAudit.set(path, { verdict, checkedAt });
225
+ }
226
+ }
227
+ } catch (error) {
228
+ throw new RuntimeError(
229
+ error instanceof Error && error.message === 'acl_permissions'
230
+ ? 'permission'
231
+ : 'acl_unavailable'
232
+ );
233
+ }
234
+ }
235
+ async inspect(
236
+ path: string,
237
+ directory = false,
238
+ allowMissing = false,
239
+ replacing = false
240
+ ): Promise<void> {
241
+ const absolute = this.guardedPath(path);
242
+ const windowsPaths: string[] = [];
243
+ let part = parse(absolute).root;
244
+ for (const name of absolute.slice(part.length).split(sep)) {
245
+ part = join(part, name);
246
+ const stat = await lstat(part).catch((e) => {
247
+ if (allowMissing && missing(e)) return null;
248
+ throw e;
249
+ });
250
+ if (!stat) break;
251
+ if (
252
+ stat.isSymbolicLink() ||
253
+ (part === absolute
254
+ ? directory
255
+ ? !stat.isDirectory()
256
+ : !stat.isFile()
257
+ : !stat.isDirectory())
258
+ )
259
+ throw new RuntimeError('permission');
260
+ if (part !== this.state && !part.startsWith(this.state + sep)) continue;
261
+ if (process.platform === 'win32') {
262
+ if (replacing && part === absolute) continue;
263
+ if (part === this.state && !this.windowsStats.has(part)) {
264
+ await prepareWindowsAclDirectory(this.state, this.run).catch((error: unknown) => {
265
+ throw new RuntimeError(
266
+ error instanceof Error && error.message === 'acl_permissions'
267
+ ? 'permission'
268
+ : 'acl_unavailable'
269
+ );
270
+ });
271
+ // Initial creation is our own write, before this directory enters the cache.
272
+ Object.assign(stat, await lstat(part));
273
+ }
274
+ if (stat.isDirectory()) this.windowsDirectories.add(part);
275
+ const identity = statIdentity(stat);
276
+ const previous = this.windowsStats.get(part);
277
+ if (previous !== undefined && previous !== identity) {
278
+ this.traceAudit({
279
+ event: 'invalidate',
280
+ requested: absolute,
281
+ path: part,
282
+ root: this.auditRoot(part),
283
+ reason: 'stat-identity',
284
+ previous,
285
+ current: identity,
286
+ });
287
+ this.invalidateAudit(part);
288
+ }
289
+ this.windowsStats.set(part, identity);
290
+ windowsPaths.push(part);
291
+ } else if (stat.uid !== process.getuid?.() || (stat.mode & 0o077) !== 0)
292
+ throw new RuntimeError('permission');
293
+ }
294
+ await this.audit(windowsPaths);
295
+ for (const part of windowsPaths) {
296
+ const verdict = this.windowsAudit.get(part)?.verdict;
297
+ if (verdict !== 'ok')
298
+ throw new RuntimeError(
299
+ verdict === undefined || verdict === 'acl_unavailable' ? 'acl_unavailable' : 'permission'
300
+ );
301
+ }
302
+ }
303
+ /** Call only after an owned atomic replacement: its parent ctime is our own write. */
304
+ async recordReplacement(path: string): Promise<void> {
305
+ if (process.platform !== 'win32' || !this.options.cacheDirectories) return;
306
+ const absolute = this.guardedPath(path);
307
+ const parent = dirname(absolute);
308
+ const stat = await lstat(parent);
309
+ if (
310
+ !stat.isDirectory() ||
311
+ stat.isSymbolicLink() ||
312
+ !this.windowsStats.get(parent)?.startsWith(`${stat.dev}:${stat.ino}:`)
313
+ )
314
+ throw new RuntimeError('permission');
315
+ this.windowsStats.set(parent, statIdentity(stat));
316
+ this.windowsAudit.delete(absolute);
317
+ this.windowsStats.delete(absolute);
318
+ }
319
+ async bytes(path: string): Promise<Buffer> {
320
+ await this.inspect(path);
321
+ const fd = await open(
322
+ path,
323
+ constants.O_RDONLY | (process.platform === 'win32' ? 0 : constants.O_NOFOLLOW)
324
+ );
325
+ try {
326
+ return await fd.readFile();
327
+ } finally {
328
+ await fd.close();
329
+ }
330
+ }
331
+ /** An owned inheritable ACL update can change every cached descendant's ctime. */
332
+ protected async recordAclWrite(path: string): Promise<void> {
333
+ const root = this.guardedPath(path);
334
+ for (const known of this.windowsStats.keys())
335
+ if (known === root || known.startsWith(root + sep))
336
+ this.windowsStats.set(known, statIdentity(await lstat(known)));
337
+ this.invalidateAudit(root);
338
+ }
339
+ protected async pointer(artifact: Artifact): Promise<Pointer | undefined> {
340
+ try {
341
+ const p = JSON.parse((await this.bytes(this.pointerPath(artifact))).toString()) as Pointer;
342
+ for (const ref of [p.current, ...(p.previous ? [p.previous] : [])]) {
343
+ versionName(ref.version);
344
+ if (!/^[a-f0-9]{64}$/.test(ref.manifestSha256)) throw new RuntimeError('digest_mismatch');
345
+ }
346
+ return p;
347
+ } catch (e) {
348
+ if (missing(e)) return undefined;
349
+ throw e instanceof RuntimeError ? e : new RuntimeError('digest_mismatch');
350
+ }
351
+ }
352
+ private async operation<T>(work: () => Promise<T>): Promise<T> {
353
+ const operation = this.operations.then(async () => {
354
+ // Reuse this process's audit across inspect/install/verify; stat drift invalidates it.
355
+ this.activeOperation = true;
356
+ try {
357
+ return await work();
358
+ } finally {
359
+ this.activeOperation = false;
360
+ }
361
+ });
362
+ this.operations = operation.catch(() => undefined);
363
+ return operation;
364
+ }
365
+ protected verifyAt(artifact: Artifact, ref: Reference, directory: string): Promise<Verified> {
366
+ return this.operation(() => this.verifyOpen(artifact, ref, directory));
367
+ }
368
+ private async verifyOpen(
369
+ artifact: Artifact,
370
+ ref: Reference,
371
+ directory: string
372
+ ): Promise<Verified> {
373
+ let bytes: Buffer;
374
+ try {
375
+ bytes = await this.bytes(join(directory, 'manifest.json'));
376
+ } catch (e) {
377
+ if (missing(e)) throw new RuntimeError('manifest_missing');
378
+ throw e;
379
+ }
380
+ if (hash(bytes) !== ref.manifestSha256) throw new RuntimeError('digest_mismatch');
381
+ let m: Manifest;
382
+ try {
383
+ m = JSON.parse(bytes.toString()) as Manifest;
384
+ } catch {
385
+ throw new RuntimeError('digest_mismatch');
386
+ }
387
+ if (
388
+ !m ||
389
+ m.schemaVersion !== 1 ||
390
+ !m.source ||
391
+ m.artifact !== artifact ||
392
+ m.version !== ref.version ||
393
+ !m.files ||
394
+ !m.files[safePath(m.entry)]
395
+ )
396
+ throw new RuntimeError('digest_mismatch');
397
+ if (
398
+ artifact === 'scanner' &&
399
+ (m.source.kind !== 'release' ||
400
+ typeof m.source.url !== 'string' ||
401
+ !m.source.url.startsWith('https://') ||
402
+ ((!m.signer || m.signer.platform !== process.platform) &&
403
+ !(this.options.allowUnsigned && !m.signer && m.signingStatus === 'unsigned')))
404
+ )
405
+ throw new RuntimeError('unsigned');
406
+ if (
407
+ artifact === 'cli' &&
408
+ (m.source.kind !== 'npm' ||
409
+ !Array.isArray(m.source.packages) ||
410
+ !m.source.packages.some((p) => p.name === '@mnemonik/cli' && p.version === m.version))
411
+ )
412
+ throw new RuntimeError('unsigned');
413
+ if (process.platform === 'win32')
414
+ await this.audit(Object.keys(m.files).map((name) => join(directory, safePath(name))));
415
+ const actual: string[] = [];
416
+ const walk = async (dir: string, prefix = ''): Promise<void> => {
417
+ await this.inspect(dir, true);
418
+ for (const name of await readdir(dir)) {
419
+ const key = prefix + name;
420
+ if (!prefix && name === 'manifest.json') continue;
421
+ const path = join(dir, name);
422
+ if ((await lstat(path)).isDirectory()) await walk(path, key + '/');
423
+ else {
424
+ await this.inspect(path);
425
+ actual.push(key);
426
+ }
427
+ }
428
+ };
429
+ await walk(directory);
430
+ if (actual.sort().join('\n') !== Object.keys(m.files).sort().join('\n'))
431
+ throw new RuntimeError('digest_mismatch');
432
+ let total = 0;
433
+ for (const [name, expected] of Object.entries(m.files)) {
434
+ if (
435
+ !expected ||
436
+ !/^[a-f0-9]{64}$/.test(expected.sha256) ||
437
+ !Number.isSafeInteger(expected.size) ||
438
+ expected.size < 0 ||
439
+ typeof expected.executable !== 'boolean'
440
+ )
441
+ throw new RuntimeError('digest_mismatch');
442
+ const path = join(directory, safePath(name));
443
+ const contents = await this.bytes(path);
444
+ if (hash(contents) !== expected.sha256 || contents.length !== expected.size)
445
+ throw new RuntimeError('digest_mismatch');
446
+ if (
447
+ process.platform !== 'win32' &&
448
+ ((await lstat(path)).mode & 0o777) !== (expected.executable ? 0o700 : 0o600)
449
+ )
450
+ throw new RuntimeError('permission');
451
+ total += contents.length;
452
+ }
453
+ if (total !== m.totalSize) throw new RuntimeError('digest_mismatch');
454
+ if (m.signer) {
455
+ const signer =
456
+ m.signer.platform === 'linux'
457
+ ? { ...m.signer, signature: join(directory, safePath(m.signer.signature)) }
458
+ : m.signer;
459
+ if (m.signer.platform === 'linux' && !m.files[m.signer.signature])
460
+ throw new RuntimeError('unsigned');
461
+ try {
462
+ const entry = join(directory, m.entry);
463
+ if (signer.platform === 'win32') {
464
+ const key = JSON.stringify([entry, m.files[m.entry]?.sha256, signer.identity]);
465
+ let verification = RuntimeReader.windowsSignatures.get(key);
466
+ if (!verification) {
467
+ verification = verifySigner(entry, signer, this.run).catch((error) => {
468
+ RuntimeReader.windowsSignatures.delete(key);
469
+ throw error;
470
+ });
471
+ RuntimeReader.windowsSignatures.set(key, verification);
472
+ }
473
+ await verification;
474
+ } else await verifySigner(entry, signer, this.run);
475
+ } catch {
476
+ throw new RuntimeError('unsigned');
477
+ }
478
+ }
479
+ return { directory, manifest: m, entry: join(directory, m.entry), reference: ref };
480
+ }
481
+ async verifyRuntime(artifact: Artifact): Promise<Verified> {
482
+ return this.operation(async () => {
483
+ const p = await this.pointer(artifact);
484
+ if (!p) throw new RuntimeError('manifest_missing');
485
+ // Reading the pointer is not permission to import its target. Only return a verified path.
486
+ return this.verifyOpen(
487
+ artifact,
488
+ p.current,
489
+ join(dirname(this.pointerPath(artifact)), p.current.version)
490
+ );
491
+ });
492
+ }
493
+ }
494
+
495
+ export * from './runtimeSigners.js';