cli4ai 1.0.2 → 1.0.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.
- package/package.json +1 -1
- package/src/commands/add.ts +2 -25
- package/src/core/execute.ts +0 -22
- package/src/core/lockfile.ts +2 -125
package/package.json
CHANGED
package/src/commands/add.ts
CHANGED
|
@@ -16,9 +16,8 @@ import {
|
|
|
16
16
|
LOCAL_PACKAGES_DIR,
|
|
17
17
|
loadConfig
|
|
18
18
|
} from '../core/config.js';
|
|
19
|
-
import { lockPackage,
|
|
19
|
+
import { lockPackage, type LockedPackage } from '../core/lockfile.js';
|
|
20
20
|
import { linkPackageDirect, isBinInPath, getPathInstructions } from '../core/link.js';
|
|
21
|
-
import { getRegistryIntegrity, verifyPackageIntegrity } from '../core/registry.js';
|
|
22
21
|
|
|
23
22
|
interface AddOptions {
|
|
24
23
|
local?: boolean;
|
|
@@ -417,27 +416,6 @@ export async function addCommand(packages: string[], options: AddOptions): Promi
|
|
|
417
416
|
await installNpmDependencies(result.path, plan.manifest.dependencies);
|
|
418
417
|
}
|
|
419
418
|
|
|
420
|
-
// SECURITY: Verify integrity against registry
|
|
421
|
-
let integrity: string | undefined;
|
|
422
|
-
try {
|
|
423
|
-
const verification = await verifyPackageIntegrity(result.name, result.path);
|
|
424
|
-
integrity = verification.actual;
|
|
425
|
-
|
|
426
|
-
if (verification.expected) {
|
|
427
|
-
if (verification.valid) {
|
|
428
|
-
log(` integrity: ${integrity.slice(0, 20)}... ✓`);
|
|
429
|
-
} else {
|
|
430
|
-
log(` ⚠️ Integrity mismatch!`);
|
|
431
|
-
log(` Expected: ${verification.expected.slice(0, 30)}...`);
|
|
432
|
-
log(` Actual: ${verification.actual.slice(0, 30)}...`);
|
|
433
|
-
}
|
|
434
|
-
} else {
|
|
435
|
-
log(` integrity: ${integrity.slice(0, 20)}...`);
|
|
436
|
-
}
|
|
437
|
-
} catch (err) {
|
|
438
|
-
log(` warning: could not compute integrity hash`);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
419
|
// Link to PATH for global installs
|
|
442
420
|
if (options.global) {
|
|
443
421
|
result.binPath = linkPackageDirect(plan.manifest, result.path);
|
|
@@ -449,8 +427,7 @@ export async function addCommand(packages: string[], options: AddOptions): Promi
|
|
|
449
427
|
const lockedPkg: LockedPackage = {
|
|
450
428
|
name: result.name,
|
|
451
429
|
version: result.version,
|
|
452
|
-
resolved: result.source === 'local' ? result.path : result.path
|
|
453
|
-
integrity
|
|
430
|
+
resolved: result.source === 'local' ? result.path : result.path
|
|
454
431
|
};
|
|
455
432
|
lockPackage(projectDir, lockedPkg);
|
|
456
433
|
}
|
package/src/core/execute.ts
CHANGED
|
@@ -15,7 +15,6 @@ import { log } from '../lib/cli.js';
|
|
|
15
15
|
import { findPackage } from './config.js';
|
|
16
16
|
import { loadManifest, type Manifest } from './manifest.js';
|
|
17
17
|
import { getSecret } from './secrets.js';
|
|
18
|
-
import { checkPackageIntegrity } from './lockfile.js';
|
|
19
18
|
|
|
20
19
|
/**
|
|
21
20
|
* Expand ~ to home directory in paths
|
|
@@ -389,27 +388,6 @@ export async function executeTool(options: ExecuteToolOptions): Promise<ExecuteT
|
|
|
389
388
|
|
|
390
389
|
const manifest = loadManifest(pkg.path);
|
|
391
390
|
|
|
392
|
-
const integrityCheck = checkPackageIntegrity(invocationDir, options.packageName, pkg.path);
|
|
393
|
-
if (!integrityCheck.valid) {
|
|
394
|
-
log(`\n⚠️ SECURITY WARNING: Package integrity check failed for ${options.packageName}`);
|
|
395
|
-
if (integrityCheck.error) log(` ${integrityCheck.error}`);
|
|
396
|
-
if (integrityCheck.expected && integrityCheck.actual) {
|
|
397
|
-
log(` Expected: ${integrityCheck.expected.slice(0, 30)}...`);
|
|
398
|
-
log(` Actual: ${integrityCheck.actual.slice(0, 30)}...`);
|
|
399
|
-
}
|
|
400
|
-
log(`\n The package may have been tampered with or modified since installation.`);
|
|
401
|
-
log(` To fix: reinstall with "cli4ai remove ${options.packageName} && cli4ai add ${options.packageName}"\n`);
|
|
402
|
-
|
|
403
|
-
const shouldContinue = await confirm('Continue anyway? (NOT RECOMMENDED)');
|
|
404
|
-
if (!shouldContinue) {
|
|
405
|
-
throw new ExecuteToolError('INTEGRITY_ERROR', 'Package integrity verification failed', {
|
|
406
|
-
package: options.packageName,
|
|
407
|
-
hint: `Reinstall the package with: cli4ai remove ${options.packageName} && cli4ai add ${options.packageName}`
|
|
408
|
-
});
|
|
409
|
-
}
|
|
410
|
-
log('');
|
|
411
|
-
}
|
|
412
|
-
|
|
413
391
|
await ensureRuntimeAvailable();
|
|
414
392
|
|
|
415
393
|
await checkPeerDependencies(pkg.path);
|
package/src/core/lockfile.ts
CHANGED
|
@@ -3,14 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Tracks installed packages with exact versions and sources
|
|
5
5
|
* for reproducible installations.
|
|
6
|
-
*
|
|
7
|
-
* SECURITY: Includes SRI (Subresource Integrity) hash verification
|
|
8
|
-
* to detect tampered packages.
|
|
9
6
|
*/
|
|
10
7
|
|
|
11
|
-
import { readFileSync, writeFileSync, renameSync, existsSync
|
|
12
|
-
import { resolve
|
|
13
|
-
import { createHash } from 'crypto';
|
|
8
|
+
import { readFileSync, writeFileSync, renameSync, existsSync } from 'fs';
|
|
9
|
+
import { resolve } from 'path';
|
|
14
10
|
|
|
15
11
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
16
12
|
// TYPES
|
|
@@ -36,125 +32,6 @@ export interface Lockfile {
|
|
|
36
32
|
export const LOCKFILE_NAME = 'cli4ai.lock';
|
|
37
33
|
export const LOCKFILE_VERSION = 1;
|
|
38
34
|
|
|
39
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
40
|
-
// INTEGRITY HASHING (SRI - Subresource Integrity)
|
|
41
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Compute SHA-512 hash of a file and return SRI format string
|
|
45
|
-
*/
|
|
46
|
-
export function computeFileIntegrity(filePath: string): string {
|
|
47
|
-
const content = readFileSync(filePath);
|
|
48
|
-
const hash = createHash('sha512').update(content).digest('base64');
|
|
49
|
-
return `sha512-${hash}`;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Compute SHA-512 hash of a directory's contents (deterministic).
|
|
54
|
-
* Hashes all files in sorted order to produce a reproducible hash.
|
|
55
|
-
*/
|
|
56
|
-
export function computeDirectoryIntegrity(dirPath: string): string {
|
|
57
|
-
const hash = createHash('sha512');
|
|
58
|
-
const files: string[] = [];
|
|
59
|
-
|
|
60
|
-
// Recursively collect all files
|
|
61
|
-
function collectFiles(dir: string): void {
|
|
62
|
-
const entries = readdirSync(dir, { withFileTypes: true });
|
|
63
|
-
for (const entry of entries) {
|
|
64
|
-
// Skip node_modules and hidden directories
|
|
65
|
-
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
|
|
66
|
-
|
|
67
|
-
const fullPath = join(dir, entry.name);
|
|
68
|
-
if (entry.isDirectory()) {
|
|
69
|
-
collectFiles(fullPath);
|
|
70
|
-
} else if (entry.isFile()) {
|
|
71
|
-
files.push(fullPath);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
collectFiles(dirPath);
|
|
77
|
-
|
|
78
|
-
// Sort files for deterministic ordering
|
|
79
|
-
files.sort();
|
|
80
|
-
|
|
81
|
-
// Hash each file's relative path and content
|
|
82
|
-
for (const filePath of files) {
|
|
83
|
-
const relativePath = relative(dirPath, filePath);
|
|
84
|
-
const content = readFileSync(filePath);
|
|
85
|
-
|
|
86
|
-
// Include path in hash for structural integrity
|
|
87
|
-
hash.update(relativePath);
|
|
88
|
-
hash.update(content);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return `sha512-${hash.digest('base64')}`;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Verify a package's integrity against stored hash
|
|
96
|
-
*/
|
|
97
|
-
export function verifyIntegrity(dirPath: string, expectedIntegrity: string): boolean {
|
|
98
|
-
if (!expectedIntegrity) return true; // No integrity check if not stored
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
const actualIntegrity = computeDirectoryIntegrity(dirPath);
|
|
102
|
-
return actualIntegrity === expectedIntegrity;
|
|
103
|
-
} catch {
|
|
104
|
-
return false;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Result of integrity verification
|
|
110
|
-
*/
|
|
111
|
-
export interface IntegrityCheckResult {
|
|
112
|
-
valid: boolean;
|
|
113
|
-
expected?: string;
|
|
114
|
-
actual?: string;
|
|
115
|
-
error?: string;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Check integrity of a locked package
|
|
120
|
-
*/
|
|
121
|
-
export function checkPackageIntegrity(
|
|
122
|
-
projectDir: string,
|
|
123
|
-
packageName: string,
|
|
124
|
-
packagePath: string
|
|
125
|
-
): IntegrityCheckResult {
|
|
126
|
-
const locked = getLockedPackage(projectDir, packageName);
|
|
127
|
-
|
|
128
|
-
if (!locked) {
|
|
129
|
-
return { valid: true }; // No lock entry, can't verify
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (!locked.integrity) {
|
|
133
|
-
return { valid: true }; // No integrity hash stored
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
const actualIntegrity = computeDirectoryIntegrity(packagePath);
|
|
138
|
-
|
|
139
|
-
if (actualIntegrity === locked.integrity) {
|
|
140
|
-
return { valid: true, expected: locked.integrity, actual: actualIntegrity };
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return {
|
|
144
|
-
valid: false,
|
|
145
|
-
expected: locked.integrity,
|
|
146
|
-
actual: actualIntegrity,
|
|
147
|
-
error: 'Integrity mismatch - package may have been tampered with'
|
|
148
|
-
};
|
|
149
|
-
} catch (err) {
|
|
150
|
-
return {
|
|
151
|
-
valid: false,
|
|
152
|
-
expected: locked.integrity,
|
|
153
|
-
error: `Failed to compute integrity: ${err instanceof Error ? err.message : String(err)}`
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
35
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
159
36
|
// FUNCTIONS
|
|
160
37
|
// ═══════════════════════════════════════════════════════════════════════════
|