@cyberhub/shieldpm 0.1.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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +239 -0
  3. package/dist/analyzer/static.d.ts +35 -0
  4. package/dist/analyzer/static.d.ts.map +1 -0
  5. package/dist/analyzer/static.js +416 -0
  6. package/dist/analyzer/static.js.map +1 -0
  7. package/dist/analyzer/typosquat.d.ts +30 -0
  8. package/dist/analyzer/typosquat.d.ts.map +1 -0
  9. package/dist/analyzer/typosquat.js +211 -0
  10. package/dist/analyzer/typosquat.js.map +1 -0
  11. package/dist/cli.d.ts +10 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +621 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/diff/dependency.d.ts +51 -0
  16. package/dist/diff/dependency.d.ts.map +1 -0
  17. package/dist/diff/dependency.js +222 -0
  18. package/dist/diff/dependency.js.map +1 -0
  19. package/dist/fingerprint/profile.d.ts +68 -0
  20. package/dist/fingerprint/profile.d.ts.map +1 -0
  21. package/dist/fingerprint/profile.js +233 -0
  22. package/dist/fingerprint/profile.js.map +1 -0
  23. package/dist/index.d.ts +21 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +22 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/monitor/permissions.d.ts +45 -0
  28. package/dist/monitor/permissions.d.ts.map +1 -0
  29. package/dist/monitor/permissions.js +265 -0
  30. package/dist/monitor/permissions.js.map +1 -0
  31. package/dist/sandbox/runner.d.ts +46 -0
  32. package/dist/sandbox/runner.d.ts.map +1 -0
  33. package/dist/sandbox/runner.js +216 -0
  34. package/dist/sandbox/runner.js.map +1 -0
  35. package/dist/utils/colors.d.ts +31 -0
  36. package/dist/utils/colors.d.ts.map +1 -0
  37. package/dist/utils/colors.js +54 -0
  38. package/dist/utils/colors.js.map +1 -0
  39. package/dist/utils/logger.d.ts +26 -0
  40. package/dist/utils/logger.d.ts.map +1 -0
  41. package/dist/utils/logger.js +77 -0
  42. package/dist/utils/logger.js.map +1 -0
  43. package/package.json +24 -0
  44. package/src/analyzer/static.ts +483 -0
  45. package/src/analyzer/typosquat.ts +272 -0
  46. package/src/cli.ts +700 -0
  47. package/src/diff/dependency.ts +297 -0
  48. package/src/fingerprint/profile.ts +333 -0
  49. package/src/index.ts +34 -0
  50. package/src/monitor/permissions.ts +330 -0
  51. package/src/sandbox/runner.ts +302 -0
  52. package/src/utils/colors.ts +58 -0
  53. package/src/utils/logger.ts +87 -0
  54. package/tsconfig.json +19 -0
@@ -0,0 +1,330 @@
1
+ /**
2
+ * ShieldPM — Permission Manifest System
3
+ * Defines, loads, validates, and generates shieldpm.json permission manifests.
4
+ */
5
+
6
+ import { readFile, writeFile, readdir, stat } from 'node:fs/promises';
7
+ import { join, resolve } from 'node:path';
8
+ import { analyzePackage } from '../analyzer/static.js';
9
+
10
+ // ── Types ────────────────────────────────────────────────────────────────
11
+
12
+ export interface PackagePermissions {
13
+ /** Allowed network destinations (glob patterns), or false to block all */
14
+ net: string[] | false;
15
+ /** Allowed filesystem paths (relative or absolute), or false to block all */
16
+ fs: string[] | false;
17
+ /** Whether native/C++ addons are allowed */
18
+ native?: boolean;
19
+ /** Whether child_process spawning is allowed */
20
+ exec?: boolean;
21
+ /** Whether environment variable access is allowed */
22
+ env?: string[] | boolean;
23
+ }
24
+
25
+ export interface PermissionManifest {
26
+ /** Manifest format version */
27
+ version: 1;
28
+ /** Per-package permission declarations */
29
+ permissions: Record<string, PackagePermissions>;
30
+ }
31
+
32
+ export type ResourceType = 'net' | 'fs' | 'native' | 'exec' | 'env';
33
+
34
+ export interface AccessCheck {
35
+ allowed: boolean;
36
+ rule: string;
37
+ details: string;
38
+ }
39
+
40
+ // ── Default manifest path ────────────────────────────────────────────────
41
+
42
+ const MANIFEST_FILENAME = 'shieldpm.json';
43
+
44
+ function resolveManifestPath(dir?: string): string {
45
+ return join(dir ?? process.cwd(), MANIFEST_FILENAME);
46
+ }
47
+
48
+ // ── Load / Save ──────────────────────────────────────────────────────────
49
+
50
+ /**
51
+ * Load the permission manifest from disk.
52
+ */
53
+ export async function loadManifest(dir?: string): Promise<PermissionManifest | null> {
54
+ const path = resolveManifestPath(dir);
55
+ try {
56
+ const raw = await readFile(path, 'utf-8');
57
+ const parsed = JSON.parse(raw);
58
+
59
+ // Basic shape validation
60
+ if (!parsed.permissions || typeof parsed.permissions !== 'object') {
61
+ throw new Error('Invalid manifest: missing "permissions" object');
62
+ }
63
+
64
+ return {
65
+ version: parsed.version ?? 1,
66
+ permissions: parsed.permissions,
67
+ };
68
+ } catch (err) {
69
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
70
+ return null; // No manifest yet
71
+ }
72
+ throw err;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Save a permission manifest to disk.
78
+ */
79
+ export async function saveManifest(manifest: PermissionManifest, dir?: string): Promise<string> {
80
+ const path = resolveManifestPath(dir);
81
+ const json = JSON.stringify(manifest, null, 2) + '\n';
82
+ await writeFile(path, json, 'utf-8');
83
+ return path;
84
+ }
85
+
86
+ // ── Access validation ────────────────────────────────────────────────────
87
+
88
+ /**
89
+ * Check whether a package is allowed to access a resource.
90
+ */
91
+ export function validateAccess(
92
+ manifest: PermissionManifest,
93
+ packageName: string,
94
+ resource: ResourceType,
95
+ target?: string
96
+ ): AccessCheck {
97
+ const perms = manifest.permissions[packageName];
98
+
99
+ // No entry in manifest — default deny
100
+ if (!perms) {
101
+ return {
102
+ allowed: false,
103
+ rule: 'no-manifest-entry',
104
+ details: `Package "${packageName}" has no entry in the permission manifest`,
105
+ };
106
+ }
107
+
108
+ switch (resource) {
109
+ case 'net': {
110
+ if (perms.net === false) {
111
+ return {
112
+ allowed: false,
113
+ rule: 'net-blocked',
114
+ details: `Network access is blocked for "${packageName}"`,
115
+ };
116
+ }
117
+ if (!target) {
118
+ return {
119
+ allowed: Array.isArray(perms.net) && perms.net.length > 0,
120
+ rule: 'net-general',
121
+ details: Array.isArray(perms.net)
122
+ ? `Network allowed to: ${perms.net.join(', ')}`
123
+ : 'Network access not configured',
124
+ };
125
+ }
126
+ // Check target against allowed patterns
127
+ const allowed = matchesAnyPattern(target, perms.net);
128
+ return {
129
+ allowed,
130
+ rule: allowed ? 'net-allowed' : 'net-denied',
131
+ details: allowed
132
+ ? `"${target}" matches allowed network pattern`
133
+ : `"${target}" does not match any allowed network pattern for "${packageName}"`,
134
+ };
135
+ }
136
+
137
+ case 'fs': {
138
+ if (perms.fs === false) {
139
+ return {
140
+ allowed: false,
141
+ rule: 'fs-blocked',
142
+ details: `Filesystem access is blocked for "${packageName}"`,
143
+ };
144
+ }
145
+ if (!target) {
146
+ return {
147
+ allowed: Array.isArray(perms.fs) && perms.fs.length > 0,
148
+ rule: 'fs-general',
149
+ details: Array.isArray(perms.fs)
150
+ ? `FS allowed in: ${perms.fs.join(', ')}`
151
+ : 'FS access not configured',
152
+ };
153
+ }
154
+ const resolvedTarget = resolve(target);
155
+ const allowed = perms.fs.some((pattern) => {
156
+ const resolvedPattern = resolve(pattern);
157
+ return resolvedTarget.startsWith(resolvedPattern);
158
+ });
159
+ return {
160
+ allowed,
161
+ rule: allowed ? 'fs-allowed' : 'fs-denied',
162
+ details: allowed
163
+ ? `"${target}" is within allowed filesystem paths`
164
+ : `"${target}" is not within any allowed filesystem path for "${packageName}"`,
165
+ };
166
+ }
167
+
168
+ case 'native': {
169
+ const allowed = perms.native === true;
170
+ return {
171
+ allowed,
172
+ rule: allowed ? 'native-allowed' : 'native-denied',
173
+ details: allowed
174
+ ? `Native modules allowed for "${packageName}"`
175
+ : `Native modules blocked for "${packageName}"`,
176
+ };
177
+ }
178
+
179
+ case 'exec': {
180
+ const allowed = perms.exec === true;
181
+ return {
182
+ allowed,
183
+ rule: allowed ? 'exec-allowed' : 'exec-denied',
184
+ details: allowed
185
+ ? `Process execution allowed for "${packageName}"`
186
+ : `Process execution blocked for "${packageName}"`,
187
+ };
188
+ }
189
+
190
+ case 'env': {
191
+ if (perms.env === false || perms.env === undefined) {
192
+ return {
193
+ allowed: false,
194
+ rule: 'env-blocked',
195
+ details: `Environment variable access blocked for "${packageName}"`,
196
+ };
197
+ }
198
+ if (perms.env === true) {
199
+ return {
200
+ allowed: true,
201
+ rule: 'env-allowed-all',
202
+ details: `All environment variables allowed for "${packageName}"`,
203
+ };
204
+ }
205
+ if (!target) {
206
+ return {
207
+ allowed: true,
208
+ rule: 'env-general',
209
+ details: `Env access allowed for: ${perms.env.join(', ')}`,
210
+ };
211
+ }
212
+ const allowed = perms.env.includes(target);
213
+ return {
214
+ allowed,
215
+ rule: allowed ? 'env-allowed' : 'env-denied',
216
+ details: allowed
217
+ ? `Env var "${target}" is allowed for "${packageName}"`
218
+ : `Env var "${target}" is not allowed for "${packageName}"`,
219
+ };
220
+ }
221
+
222
+ default:
223
+ return {
224
+ allowed: false,
225
+ rule: 'unknown-resource',
226
+ details: `Unknown resource type: ${resource}`,
227
+ };
228
+ }
229
+ }
230
+
231
+ // ── Pattern matching ─────────────────────────────────────────────────────
232
+
233
+ /**
234
+ * Match a string against an array of glob-like patterns.
235
+ * Supports: * (any), *.domain.com, exact match.
236
+ */
237
+ function matchesAnyPattern(value: string, patterns: string[]): boolean {
238
+ for (const pattern of patterns) {
239
+ if (pattern === '*') return true;
240
+
241
+ // Convert glob pattern to regex
242
+ const regexStr = pattern
243
+ .replace(/\./g, '\\.')
244
+ .replace(/\*/g, '.*');
245
+ const regex = new RegExp(`^${regexStr}$`, 'i');
246
+
247
+ if (regex.test(value)) return true;
248
+ }
249
+ return false;
250
+ }
251
+
252
+ // ── Manifest generation ──────────────────────────────────────────────────
253
+
254
+ /**
255
+ * Auto-generate a permission manifest by scanning installed packages.
256
+ */
257
+ export async function generateManifest(projectDir: string): Promise<PermissionManifest> {
258
+ const manifest: PermissionManifest = {
259
+ version: 1,
260
+ permissions: {},
261
+ };
262
+
263
+ const nodeModules = join(projectDir, 'node_modules');
264
+ let entries: string[];
265
+
266
+ try {
267
+ entries = await readdir(nodeModules);
268
+ } catch {
269
+ return manifest; // No node_modules
270
+ }
271
+
272
+ // Collect package directories (including scoped packages)
273
+ const packageDirs: { name: string; dir: string }[] = [];
274
+
275
+ for (const entry of entries) {
276
+ if (entry.startsWith('.')) continue;
277
+
278
+ const fullPath = join(nodeModules, entry);
279
+ const entryStat = await stat(fullPath).catch(() => null);
280
+ if (!entryStat?.isDirectory()) continue;
281
+
282
+ if (entry.startsWith('@')) {
283
+ // Scoped package — look one level deeper
284
+ const scopedEntries = await readdir(fullPath).catch(() => [] as string[]);
285
+ for (const scopedEntry of scopedEntries) {
286
+ const scopedPath = join(fullPath, scopedEntry);
287
+ const scopedStat = await stat(scopedPath).catch(() => null);
288
+ if (scopedStat?.isDirectory()) {
289
+ packageDirs.push({ name: `${entry}/${scopedEntry}`, dir: scopedPath });
290
+ }
291
+ }
292
+ } else {
293
+ packageDirs.push({ name: entry, dir: fullPath });
294
+ }
295
+ }
296
+
297
+ // Analyze each package and build permissions
298
+ for (const { name, dir } of packageDirs) {
299
+ const report = await analyzePackage(dir);
300
+
301
+ const perms: PackagePermissions = {
302
+ net: false,
303
+ fs: false,
304
+ };
305
+
306
+ // If the package uses network, allow it (but default to restrictive)
307
+ if (report.categoryCounts['network']) {
308
+ perms.net = []; // User must fill in allowed destinations
309
+ }
310
+
311
+ // If the package uses filesystem
312
+ if (report.categoryCounts['filesystem']) {
313
+ perms.fs = []; // User must fill in allowed paths
314
+ }
315
+
316
+ // If the package uses child_process
317
+ if (report.categoryCounts['process']) {
318
+ perms.exec = false; // Default deny, user opts in
319
+ }
320
+
321
+ // If the package accesses env
322
+ if (report.categoryCounts['environment']) {
323
+ perms.env = []; // User must fill in allowed vars
324
+ }
325
+
326
+ manifest.permissions[name] = perms;
327
+ }
328
+
329
+ return manifest;
330
+ }
@@ -0,0 +1,302 @@
1
+ /**
2
+ * ShieldPM — Sandbox Runner
3
+ * Executes commands (especially postinstall scripts) in a restricted environment
4
+ * with network blocking, timeout enforcement, and output capture.
5
+ */
6
+
7
+ import { spawn, type ChildProcess } from 'node:child_process';
8
+ import { platform } from 'node:os';
9
+
10
+ // ── Types ────────────────────────────────────────────────────────────────
11
+
12
+ export interface SandboxOptions {
13
+ /** Working directory for the command */
14
+ cwd?: string;
15
+ /** Timeout in milliseconds (default: 30000) */
16
+ timeout?: number;
17
+ /** Block network access (default: true) */
18
+ blockNetwork?: boolean;
19
+ /** Block environment variables (default: true) */
20
+ blockEnv?: boolean;
21
+ /** Allowed environment variable names to pass through */
22
+ allowedEnvVars?: string[];
23
+ /** Maximum stdout/stderr size in bytes (default: 1MB) */
24
+ maxOutputSize?: number;
25
+ /** Enable verbose logging of sandbox decisions */
26
+ verbose?: boolean;
27
+ }
28
+
29
+ export interface SandboxResult {
30
+ /** Process exit code (null if killed) */
31
+ exitCode: number | null;
32
+ /** Captured stdout */
33
+ stdout: string;
34
+ /** Captured stderr */
35
+ stderr: string;
36
+ /** Warnings generated during execution */
37
+ warnings: string[];
38
+ /** Actions that were blocked */
39
+ blocked: string[];
40
+ /** Whether the process was killed due to timeout */
41
+ timedOut: boolean;
42
+ /** Duration in milliseconds */
43
+ durationMs: number;
44
+ }
45
+
46
+ // ── Safe environment builder ─────────────────────────────────────────────
47
+
48
+ const SAFE_ENV_VARS = new Set([
49
+ 'PATH',
50
+ 'HOME',
51
+ 'USER',
52
+ 'SHELL',
53
+ 'LANG',
54
+ 'LC_ALL',
55
+ 'TERM',
56
+ 'TMPDIR',
57
+ 'TMP',
58
+ 'TEMP',
59
+ 'NODE_ENV',
60
+ 'NODE_PATH',
61
+ ]);
62
+
63
+ const SENSITIVE_ENV_VARS = new Set([
64
+ 'AWS_ACCESS_KEY_ID',
65
+ 'AWS_SECRET_ACCESS_KEY',
66
+ 'AWS_SESSION_TOKEN',
67
+ 'GITHUB_TOKEN',
68
+ 'GH_TOKEN',
69
+ 'NPM_TOKEN',
70
+ 'NPM_AUTH_TOKEN',
71
+ 'DOCKER_PASSWORD',
72
+ 'SSH_AUTH_SOCK',
73
+ 'GPG_TTY',
74
+ 'DATABASE_URL',
75
+ 'REDIS_URL',
76
+ 'API_KEY',
77
+ 'SECRET_KEY',
78
+ 'PRIVATE_KEY',
79
+ ]);
80
+
81
+ function buildSandboxEnv(
82
+ blockNetwork: boolean,
83
+ blockEnv: boolean,
84
+ allowedEnvVars: string[]
85
+ ): Record<string, string> {
86
+ const env: Record<string, string> = {};
87
+
88
+ if (blockEnv) {
89
+ // Only pass through safe variables
90
+ const allowed = new Set([...SAFE_ENV_VARS, ...allowedEnvVars]);
91
+ for (const key of allowed) {
92
+ if (process.env[key] !== undefined) {
93
+ env[key] = process.env[key]!;
94
+ }
95
+ }
96
+ } else {
97
+ // Pass through everything except sensitive vars
98
+ for (const [key, value] of Object.entries(process.env)) {
99
+ if (!SENSITIVE_ENV_VARS.has(key) && value !== undefined) {
100
+ env[key] = value;
101
+ }
102
+ }
103
+ // Also pass explicitly allowed
104
+ for (const key of allowedEnvVars) {
105
+ if (process.env[key] !== undefined) {
106
+ env[key] = process.env[key]!;
107
+ }
108
+ }
109
+ }
110
+
111
+ // Block network via proxy settings
112
+ if (blockNetwork) {
113
+ env['HTTP_PROXY'] = 'http://blocked.shieldpm.local:0';
114
+ env['HTTPS_PROXY'] = 'http://blocked.shieldpm.local:0';
115
+ env['http_proxy'] = 'http://blocked.shieldpm.local:0';
116
+ env['https_proxy'] = 'http://blocked.shieldpm.local:0';
117
+ env['no_proxy'] = '';
118
+ env['NODE_OPTIONS'] = [
119
+ env['NODE_OPTIONS'] ?? '',
120
+ '--dns-result-order=verbatim',
121
+ ].filter(Boolean).join(' ');
122
+ }
123
+
124
+ // Prevent spawning of sub-shells from modifying real config
125
+ env['npm_config_ignore_scripts'] = 'true';
126
+
127
+ return env;
128
+ }
129
+
130
+ // ── Platform-specific restrictions ───────────────────────────────────────
131
+
132
+ function buildPlatformArgs(): string[] {
133
+ const args: string[] = [];
134
+
135
+ if (platform() === 'linux') {
136
+ // On Linux, we could use unshare for network namespace isolation
137
+ // For now, we rely on proxy blocking; future: seccomp/landlock
138
+ }
139
+
140
+ return args;
141
+ }
142
+
143
+ // ── Output truncation ────────────────────────────────────────────────────
144
+
145
+ function truncateOutput(output: string, maxSize: number): string {
146
+ if (Buffer.byteLength(output) <= maxSize) return output;
147
+
148
+ const truncated = output.slice(0, maxSize);
149
+ return truncated + `\n... [output truncated at ${Math.round(maxSize / 1024)}KB]`;
150
+ }
151
+
152
+ // ── Main runner ──────────────────────────────────────────────────────────
153
+
154
+ /**
155
+ * Run a command inside a restricted sandbox environment.
156
+ */
157
+ export async function runSandboxed(
158
+ command: string,
159
+ args: string[] = [],
160
+ options: SandboxOptions = {}
161
+ ): Promise<SandboxResult> {
162
+ const {
163
+ cwd = process.cwd(),
164
+ timeout = 30_000,
165
+ blockNetwork = true,
166
+ blockEnv = true,
167
+ allowedEnvVars = [],
168
+ maxOutputSize = 1024 * 1024, // 1MB
169
+ verbose = false,
170
+ } = options;
171
+
172
+ const warnings: string[] = [];
173
+ const blocked: string[] = [];
174
+
175
+ // Build restricted environment
176
+ const env = buildSandboxEnv(blockNetwork, blockEnv, allowedEnvVars);
177
+
178
+ if (blockNetwork) {
179
+ blocked.push('network: HTTP/HTTPS proxied to blocked endpoint');
180
+ }
181
+ if (blockEnv) {
182
+ const removedCount = Object.keys(process.env).length - Object.keys(env).length;
183
+ blocked.push(`environment: ${removedCount} env vars stripped`);
184
+ }
185
+
186
+ const startTime = Date.now();
187
+ let timedOut = false;
188
+
189
+ return new Promise<SandboxResult>((resolve) => {
190
+ let child: ChildProcess;
191
+ let stdoutChunks: Buffer[] = [];
192
+ let stderrChunks: Buffer[] = [];
193
+ let stdoutSize = 0;
194
+ let stderrSize = 0;
195
+
196
+ try {
197
+ child = spawn(command, args, {
198
+ cwd,
199
+ env,
200
+ stdio: ['pipe', 'pipe', 'pipe'],
201
+ shell: true,
202
+ // Kill the entire process group on timeout
203
+ detached: false,
204
+ });
205
+ } catch (err) {
206
+ resolve({
207
+ exitCode: 1,
208
+ stdout: '',
209
+ stderr: `Failed to spawn process: ${err instanceof Error ? err.message : String(err)}`,
210
+ warnings,
211
+ blocked,
212
+ timedOut: false,
213
+ durationMs: Date.now() - startTime,
214
+ });
215
+ return;
216
+ }
217
+
218
+ // Capture stdout
219
+ child.stdout?.on('data', (chunk: Buffer) => {
220
+ if (stdoutSize < maxOutputSize) {
221
+ stdoutChunks.push(chunk);
222
+ stdoutSize += chunk.length;
223
+ }
224
+ });
225
+
226
+ // Capture stderr
227
+ child.stderr?.on('data', (chunk: Buffer) => {
228
+ if (stderrSize < maxOutputSize) {
229
+ stderrChunks.push(chunk);
230
+ stderrSize += chunk.length;
231
+ }
232
+
233
+ // Watch for suspicious patterns in stderr
234
+ const text = chunk.toString();
235
+ if (/ECONNREFUSED|ENOTFOUND|blocked\.shieldpm/.test(text)) {
236
+ warnings.push('Process attempted network access (blocked by sandbox)');
237
+ }
238
+ });
239
+
240
+ // Timeout
241
+ const timer = setTimeout(() => {
242
+ timedOut = true;
243
+ warnings.push(`Process killed: exceeded ${timeout}ms timeout`);
244
+ child.kill('SIGKILL');
245
+ }, timeout);
246
+
247
+ // Completion
248
+ child.on('close', (code) => {
249
+ clearTimeout(timer);
250
+
251
+ const stdout = truncateOutput(Buffer.concat(stdoutChunks).toString('utf-8'), maxOutputSize);
252
+ const stderr = truncateOutput(Buffer.concat(stderrChunks).toString('utf-8'), maxOutputSize);
253
+
254
+ if (code !== 0 && !timedOut) {
255
+ warnings.push(`Process exited with code ${code}`);
256
+ }
257
+
258
+ resolve({
259
+ exitCode: code,
260
+ stdout,
261
+ stderr,
262
+ warnings,
263
+ blocked,
264
+ timedOut,
265
+ durationMs: Date.now() - startTime,
266
+ });
267
+ });
268
+
269
+ child.on('error', (err) => {
270
+ clearTimeout(timer);
271
+ resolve({
272
+ exitCode: 1,
273
+ stdout: Buffer.concat(stdoutChunks).toString('utf-8'),
274
+ stderr: err.message,
275
+ warnings: [...warnings, `Spawn error: ${err.message}`],
276
+ blocked,
277
+ timedOut: false,
278
+ durationMs: Date.now() - startTime,
279
+ });
280
+ });
281
+
282
+ // Close stdin immediately
283
+ child.stdin?.end();
284
+ });
285
+ }
286
+
287
+ /**
288
+ * Run an npm postinstall script in the sandbox.
289
+ */
290
+ export async function runPostInstall(
291
+ packageDir: string,
292
+ script: string,
293
+ options: SandboxOptions = {}
294
+ ): Promise<SandboxResult> {
295
+ return runSandboxed('sh', ['-c', script], {
296
+ cwd: packageDir,
297
+ timeout: 30_000,
298
+ blockNetwork: true,
299
+ blockEnv: true,
300
+ ...options,
301
+ });
302
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * ShieldPM — Terminal color helpers (zero dependencies)
3
+ * Simple ANSI escape code wrapper, no chalk needed.
4
+ */
5
+
6
+ const isColorSupported = (): boolean => {
7
+ if (process.env.NO_COLOR !== undefined) return false;
8
+ if (process.env.FORCE_COLOR !== undefined) return true;
9
+ if (!process.stdout.isTTY) return false;
10
+ return true;
11
+ };
12
+
13
+ const enabled = isColorSupported();
14
+
15
+ const wrap = (open: string, close: string) => {
16
+ return (text: string): string => {
17
+ if (!enabled) return text;
18
+ return `\x1b[${open}m${text}\x1b[${close}m`;
19
+ };
20
+ };
21
+
22
+ // Foreground colors
23
+ export const red = wrap('31', '39');
24
+ export const green = wrap('32', '39');
25
+ export const yellow = wrap('33', '39');
26
+ export const blue = wrap('34', '39');
27
+ export const magenta = wrap('35', '39');
28
+ export const cyan = wrap('36', '39');
29
+ export const white = wrap('37', '39');
30
+ export const gray = wrap('90', '39');
31
+
32
+ // Styles
33
+ export const bold = wrap('1', '22');
34
+ export const dim = wrap('2', '22');
35
+ export const italic = wrap('3', '23');
36
+ export const underline = wrap('4', '24');
37
+
38
+ // Reset
39
+ export const reset = wrap('0', '0');
40
+
41
+ // Bright variants
42
+ export const redBright = wrap('91', '39');
43
+ export const greenBright = wrap('92', '39');
44
+ export const yellowBright = wrap('93', '39');
45
+ export const blueBright = wrap('94', '39');
46
+ export const cyanBright = wrap('96', '39');
47
+
48
+ // Background colors
49
+ export const bgRed = wrap('41', '49');
50
+ export const bgGreen = wrap('42', '49');
51
+ export const bgYellow = wrap('43', '49');
52
+
53
+ // Composable: bold + color
54
+ export const boldRed = (t: string) => bold(red(t));
55
+ export const boldGreen = (t: string) => bold(green(t));
56
+ export const boldYellow = (t: string) => bold(yellow(t));
57
+ export const boldCyan = (t: string) => bold(cyan(t));
58
+ export const boldBlue = (t: string) => bold(blue(t));