anhedral 0.8.5 → 0.9.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.
- package/README.md +140 -34
- package/assets/anhedral-cli-init-technical.svg +8 -8
- package/assets/anhedral-cli-init.svg +9 -9
- package/assets/security/LICENSES.txt +63 -0
- package/assets/security/audit-deps.mjs +295 -0
- package/assets/security/braces-regression.mjs +102 -0
- package/assets/security/braces@3.0.3.patch +285 -0
- package/assets/security/decode-uri-component@0.2.2.patch +149 -0
- package/assets/security/decoder-regression.mjs +37 -0
- package/assets/security/forge-regression.mjs +46 -0
- package/assets/security/manifest.json +53 -0
- package/assets/security/node-forge@1.4.0.patch +26 -0
- package/dist/access.d.ts +10 -0
- package/dist/access.js +93 -0
- package/dist/auth-schema-snapshot.d.ts +346 -0
- package/dist/auth-schema-snapshot.js +347 -0
- package/dist/auth-schema.d.ts +1 -0
- package/dist/auth-schema.js +57 -0
- package/dist/bin.js +353 -10
- package/dist/bounded-file.d.ts +2 -0
- package/dist/bounded-file.js +30 -0
- package/dist/capabilities.d.ts +19 -33
- package/dist/capabilities.js +73 -71
- package/dist/cloudflare.d.ts +14 -0
- package/dist/cloudflare.js +66 -0
- package/dist/command.js +12 -3
- package/dist/delivery.d.ts +118 -0
- package/dist/delivery.js +611 -0
- package/dist/dependencies.d.ts +22 -0
- package/dist/dependencies.js +109 -67
- package/dist/dependency-order.d.ts +2 -0
- package/dist/dependency-order.js +24 -0
- package/dist/index.d.ts +18 -1
- package/dist/index.js +12 -1
- package/dist/managed-hosting.d.ts +49 -0
- package/dist/managed-hosting.js +373 -0
- package/dist/management.d.ts +3 -0
- package/dist/management.js +47 -0
- package/dist/platforms/desktop.d.ts +2 -2
- package/dist/platforms/desktop.js +22 -12
- package/dist/platforms/extension.d.ts +2 -2
- package/dist/platforms/extension.js +12 -4
- package/dist/platforms/mobile.d.ts +2 -2
- package/dist/platforms/mobile.js +13 -7
- package/dist/pnpm-policy.d.ts +4 -0
- package/dist/pnpm-policy.js +66 -0
- package/dist/project-state.d.ts +28 -0
- package/dist/project-state.js +226 -0
- package/dist/provider-readiness.d.ts +30 -0
- package/dist/provider-readiness.js +87 -0
- package/dist/provision.d.ts +46 -0
- package/dist/provision.js +197 -0
- package/dist/readiness.d.ts +4 -1
- package/dist/readiness.js +80 -8
- package/dist/registry/add.d.ts +22 -0
- package/dist/registry/add.js +198 -0
- package/dist/registry/bootstrap.d.ts +2 -0
- package/dist/registry/bootstrap.js +14 -0
- package/dist/registry/commands.d.ts +2 -0
- package/dist/registry/commands.js +99 -0
- package/dist/registry/context.d.ts +3 -0
- package/dist/registry/context.js +21 -0
- package/dist/registry/options.d.ts +2 -0
- package/dist/registry/options.js +65 -0
- package/dist/registry/receipt.d.ts +3 -0
- package/dist/registry/receipt.js +73 -0
- package/dist/registry/remote.d.ts +25 -0
- package/dist/registry/remote.js +168 -0
- package/dist/registry/resolve.d.ts +10 -0
- package/dist/registry/resolve.js +98 -0
- package/dist/registry/types.d.ts +64 -0
- package/dist/registry/usage.d.ts +1 -0
- package/dist/registry/usage.js +38 -0
- package/dist/render.js +3 -3
- package/dist/repository.d.ts +24 -0
- package/dist/repository.js +96 -0
- package/dist/scaffold/api.d.ts +2 -0
- package/dist/scaffold/api.js +13 -0
- package/dist/scaffold/auth.d.ts +3 -0
- package/dist/scaffold/auth.js +95 -0
- package/dist/scaffold/bootstrap.d.ts +2 -0
- package/dist/scaffold/bootstrap.js +34 -0
- package/dist/scaffold/compose.d.ts +4 -0
- package/dist/scaffold/compose.js +34 -0
- package/dist/scaffold/database.d.ts +2 -0
- package/dist/scaffold/database.js +43 -0
- package/dist/scaffold/documentation.d.ts +3 -0
- package/dist/scaffold/documentation.js +91 -0
- package/dist/scaffold/environment.d.ts +2 -0
- package/dist/scaffold/environment.js +5 -0
- package/dist/scaffold/files.d.ts +25 -0
- package/dist/scaffold/files.js +108 -0
- package/dist/scaffold/integrations.d.ts +3 -0
- package/dist/scaffold/integrations.js +7 -0
- package/dist/scaffold/services.d.ts +5 -0
- package/dist/scaffold/services.js +84 -0
- package/dist/scaffold/web.d.ts +2 -0
- package/dist/scaffold/web.js +102 -0
- package/dist/scaffold/workspace.d.ts +3 -0
- package/dist/scaffold/workspace.js +150 -0
- package/dist/security-backports.d.ts +3 -0
- package/dist/security-backports.js +26 -0
- package/dist/stack-registry.d.ts +80 -0
- package/dist/stack-registry.js +424 -0
- package/dist/standard.d.ts +6 -17
- package/dist/standard.js +41 -585
- package/dist/transaction.js +46 -26
- package/dist/util.js +12 -2
- package/dist/vercel.d.ts +98 -0
- package/dist/vercel.js +796 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/vinext.d.ts +1 -0
- package/dist/vinext.js +83 -0
- package/dist/workspace-graph.d.ts +7 -0
- package/dist/workspace-graph.js +73 -0
- package/docs/application-stack-standard.md +75 -674
- package/docs/delivery-workflow.md +83 -0
- package/docs/managed-hosting-architecture.md +87 -0
- package/docs/sponsorships.md +25 -0
- package/package.json +23 -28
- package/dist/project.d.ts +0 -4
- package/dist/standard-products.d.ts +0 -2
- package/dist/standard-products.js +0 -6
- /package/assets/{images/svg/logo-white-subtract.svg → anhedral-mark.svg} +0 -0
- /package/dist/{project.js → registry/types.js} +0 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import console from 'node:console';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { readdir, readFile, lstat, writeFile, realpath } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { setTimeout, clearTimeout } from 'node:timers';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { verifyForge } from './forge-regression.mjs';
|
|
9
|
+
import { verifyBraces } from './braces-regression.mjs';
|
|
10
|
+
import { verifyDecoder } from './decoder-regression.mjs';
|
|
11
|
+
|
|
12
|
+
const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
13
|
+
const assetRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const severities = ['info', 'low', 'moderate', 'high', 'critical'];
|
|
15
|
+
const supported = {
|
|
16
|
+
'node-forge': { version: '1.4.0', advisory: 'GHSA-86w9-cpqp-85rv', verify: verifyForge },
|
|
17
|
+
braces: { version: '3.0.3', advisory: 'GHSA-vfj7-8cjw-p6xm', verify: verifyBraces },
|
|
18
|
+
'decode-uri-component': { version: '0.2.2', advisory: 'GHSA-vcc3-ghjq-m6fr', verify: verifyDecoder },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function ensure(condition, message) {
|
|
22
|
+
if (!condition) throw new Error(message);
|
|
23
|
+
}
|
|
24
|
+
const nonemptyString = (value) => typeof value === 'string' && value.length > 0;
|
|
25
|
+
const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
26
|
+
const strings = (value) => Array.isArray(value) && value.length > 0 && value.every(nonemptyString);
|
|
27
|
+
|
|
28
|
+
async function collectPackageFiles(directory, relative, files) {
|
|
29
|
+
const names = (await readdir(directory)).sort().filter((name) => relative || name !== 'node_modules');
|
|
30
|
+
for (const name of names) {
|
|
31
|
+
const key = relative ? `${relative}/${name}` : name;
|
|
32
|
+
await collectPackageEntry(path.join(directory, name), key, files);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function collectPackageEntry(location, key, files) {
|
|
37
|
+
const entry = await lstat(location);
|
|
38
|
+
ensure(!entry.isSymbolicLink(), `Unexpected package symlink: ${key}`);
|
|
39
|
+
if (entry.isDirectory()) return collectPackageFiles(location, key, files);
|
|
40
|
+
ensure(entry.isFile(), `Unexpected package file type: ${key}`);
|
|
41
|
+
files.push([key, sha256(await readFile(location))]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function hashPackageTree(packageRoot) {
|
|
45
|
+
const files = [];
|
|
46
|
+
await collectPackageFiles(packageRoot, '', files);
|
|
47
|
+
// Use code-unit ordering independently of the host locale.
|
|
48
|
+
const sortedNames = files.map(([name]) => name).sort();
|
|
49
|
+
const hashes = new Map(files);
|
|
50
|
+
return sha256(sortedNames.map((name) => `${name}\0${hashes.get(name)}\n`).join(''));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function optionalStat(location) {
|
|
54
|
+
try { return await lstat(location); }
|
|
55
|
+
catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function discoverCopy(directory, name, copies, links) {
|
|
59
|
+
const candidate = path.join(directory, name);
|
|
60
|
+
const stat = await optionalStat(candidate);
|
|
61
|
+
if (!stat) return;
|
|
62
|
+
if (stat.isSymbolicLink()) return links.push(candidate);
|
|
63
|
+
ensure(stat.isDirectory(), `Unexpected package location: ${candidate}`);
|
|
64
|
+
copies.add(await realpath(candidate));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function insideRoot(root, target) {
|
|
68
|
+
const relative = path.relative(root, target);
|
|
69
|
+
return relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function discoverLinkedDirectory(location, name, copies, links, state) {
|
|
73
|
+
const target = await realpath(location);
|
|
74
|
+
const stat = await lstat(target);
|
|
75
|
+
if (!stat.isDirectory()) return;
|
|
76
|
+
ensure(insideRoot(state.root, target), `Directory link escapes dependency inspection: ${location}`);
|
|
77
|
+
await discoverPackages(location, name, copies, links, state);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function discoverPackages(directory, name, copies, links, state) {
|
|
81
|
+
// Inspect logical node_modules aliases before deduplicating physical traversal.
|
|
82
|
+
if (path.basename(directory) === 'node_modules') await discoverCopy(directory, name, copies, links);
|
|
83
|
+
const physical = await realpath(directory);
|
|
84
|
+
if (state.visited.has(physical)) return;
|
|
85
|
+
state.visited.add(physical);
|
|
86
|
+
const entries = (await readdir(directory, { withFileTypes: true })).filter((entry) => entry.name !== '.git');
|
|
87
|
+
for (const entry of entries) await discoverEntry(directory, entry, name, copies, links, state);
|
|
88
|
+
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function discoverEntry(directory, entry, name, copies, links, state) {
|
|
92
|
+
const location = path.join(directory, entry.name);
|
|
93
|
+
if (entry.isDirectory()) return discoverPackages(location, name, copies, links, state);
|
|
94
|
+
if (entry.isSymbolicLink()) return discoverLinkedDirectory(location, name, copies, links, state);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function packageCopies(projectRoot, name) {
|
|
98
|
+
const copies = new Set();
|
|
99
|
+
const links = [];
|
|
100
|
+
const state = { root: await realpath(projectRoot), visited: new Set() };
|
|
101
|
+
await discoverPackages(projectRoot, name, copies, links, state);
|
|
102
|
+
const storeCopies = new Set([...copies].filter((copy) => copy.split(path.sep).includes('.pnpm')));
|
|
103
|
+
for (const link of links) ensure(storeCopies.has(await realpath(link)), `Package link escapes verified pnpm copies: ${link}`);
|
|
104
|
+
return [...copies];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function validateFinding(finding) {
|
|
108
|
+
ensure(nonemptyString(finding?.version), 'Malformed pnpm advisory');
|
|
109
|
+
ensure(strings(finding.paths), 'Malformed pnpm advisory');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function validateAdvisory(advisory) {
|
|
113
|
+
ensure(nonemptyString(advisory?.module_name), 'Malformed pnpm advisory');
|
|
114
|
+
ensure(severities.includes(advisory.severity), 'Malformed pnpm advisory');
|
|
115
|
+
ensure(Array.isArray(advisory.findings), 'Malformed pnpm advisory');
|
|
116
|
+
ensure(advisory.findings.length > 0, 'Malformed pnpm advisory');
|
|
117
|
+
advisory.findings.forEach(validateFinding);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function validateCount(advisories, severity, count) {
|
|
121
|
+
ensure(Number.isInteger(count) && count >= 0, 'Malformed pnpm vulnerability counts');
|
|
122
|
+
const details = advisories.filter((advisory) => advisory.severity === severity).length;
|
|
123
|
+
ensure(count === details, `Missing ${severity} advisory details: count ${count}, details ${details}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function validateReport(report) {
|
|
127
|
+
ensure(record(report), 'Malformed pnpm audit report');
|
|
128
|
+
ensure(record(report.advisories), 'Malformed pnpm audit report');
|
|
129
|
+
ensure(record(report.metadata?.vulnerabilities), 'Malformed pnpm audit report');
|
|
130
|
+
ensure(!report.error, 'Malformed pnpm audit report');
|
|
131
|
+
const advisories = Object.values(report.advisories);
|
|
132
|
+
advisories.forEach(validateAdvisory);
|
|
133
|
+
for (const severity of severities) validateCount(advisories, severity, report.metadata.vulnerabilities[severity]);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function validateEntry(entry) {
|
|
137
|
+
const message = 'Malformed remediation entry';
|
|
138
|
+
ensure(record(entry), message);
|
|
139
|
+
ensure(Object.hasOwn(supported, entry.name), message);
|
|
140
|
+
const expected = supported[entry.name];
|
|
141
|
+
ensure(entry.version === expected.version, message);
|
|
142
|
+
ensure(Array.isArray(entry.advisoryIds), message);
|
|
143
|
+
ensure(entry.advisoryIds.length === 1, message);
|
|
144
|
+
ensure(entry.advisoryIds[0] === expected.advisory, message);
|
|
145
|
+
ensure(/^[a-f0-9]{64}$/.test(entry.patchSha256 ?? ''), message);
|
|
146
|
+
ensure(/^[a-f0-9]{64}$/.test(entry.installedTreeSha256 ?? ''), message);
|
|
147
|
+
ensure(entry.patch === `${entry.name}@${entry.version}.patch`, message);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function validateManifest(manifest) {
|
|
151
|
+
ensure(manifest?.schemaVersion === 1, 'Malformed remediation manifest');
|
|
152
|
+
ensure(Array.isArray(manifest.packages), 'Malformed remediation manifest');
|
|
153
|
+
manifest.packages.forEach(validateEntry);
|
|
154
|
+
const names = manifest.packages.map((entry) => entry.name);
|
|
155
|
+
ensure(new Set(names).size === names.length, 'Duplicate remediation entries');
|
|
156
|
+
ensure(names.length === Object.keys(supported).length, 'Incomplete remediation manifest');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function optionalFile(file) {
|
|
160
|
+
try { return await readFile(file, 'utf8'); }
|
|
161
|
+
catch (error) { if (error.code === 'ENOENT') return ''; throw error; }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function parsePatchLine(line) {
|
|
165
|
+
const match = line.match(/^\s+['"]?([^'"\s]+)['"]?:\s*['"]?([^'"\s#]+)['"]?\s*(?:#.*)?$/);
|
|
166
|
+
return match?.slice(1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function patchSection(workspace) {
|
|
170
|
+
const lines = workspace.split(/\r?\n/);
|
|
171
|
+
const headers = lines.flatMap((line, index) => /^(?:patchedDependencies|"patchedDependencies"|'patchedDependencies')\s*:\s*(?:#.*)?$/.test(line) ? [index] : []);
|
|
172
|
+
ensure(headers.length <= 1, 'Duplicate patchedDependencies sections');
|
|
173
|
+
if (headers.length === 0) return [];
|
|
174
|
+
const remainder = lines.slice(headers[0] + 1);
|
|
175
|
+
const end = remainder.findIndex((line) => /^\S/.test(line) && !line.startsWith('#'));
|
|
176
|
+
return (end < 0 ? remainder : remainder.slice(0, end)).map(parsePatchLine).filter(Boolean);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function hasPatchMapping(projectRoot, entry) {
|
|
180
|
+
const section = patchSection(await optionalFile(path.join(projectRoot, 'pnpm-workspace.yaml')));
|
|
181
|
+
const key = `${entry.name}@${entry.version}`;
|
|
182
|
+
const matches = section.filter(([name]) => name === key);
|
|
183
|
+
ensure(matches.length <= 1, `Duplicate patch mapping: ${key}`);
|
|
184
|
+
return matches[0]?.[1] === path.posix.join('scripts/security', entry.patch);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function verifyPatch(projectRoot, securityRoot, entry) {
|
|
188
|
+
ensure(await hasPatchMapping(projectRoot, entry), `Missing patch mapping: ${entry.name}@${entry.version}`);
|
|
189
|
+
const patch = path.join(securityRoot, entry.patch);
|
|
190
|
+
ensure((await lstat(patch)).isFile(), `Unexpected patch file: ${entry.name}`);
|
|
191
|
+
ensure(sha256(await readFile(patch)) === entry.patchSha256, `Patch checksum mismatch: ${entry.name}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function verifyCopy(copy, entry, verify) {
|
|
195
|
+
const pkg = JSON.parse(await readFile(path.join(copy, 'package.json'), 'utf8'));
|
|
196
|
+
ensure(pkg.name === entry.name && pkg.version === entry.version, `Unexpected installed version: ${entry.name} at ${copy}`);
|
|
197
|
+
ensure(await hashPackageTree(copy) === entry.installedTreeSha256, `Installed package bytes differ: ${copy}`);
|
|
198
|
+
await verify(copy);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function verifyEntry(entry, options) {
|
|
202
|
+
const copies = await packageCopies(options.projectRoot, entry.name);
|
|
203
|
+
if (copies.length === 0) return false;
|
|
204
|
+
await verifyPatch(options.projectRoot, options.securityRoot, entry);
|
|
205
|
+
const verify = options.regressions[entry.name] ?? supported[entry.name].verify;
|
|
206
|
+
ensure(typeof verify === 'function', `Missing regression verifier: ${entry.name}`);
|
|
207
|
+
for (const copy of copies) await verifyCopy(copy, entry, verify);
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function matchesAdvisory(entry, advisory) {
|
|
212
|
+
return entry.name === advisory.module_name && entry.advisoryIds.includes(advisory.github_advisory_id);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function advisoryFixed(entry, advisory, verified) {
|
|
216
|
+
return Boolean(entry && verified.has(entry.name) && advisory.findings.every((finding) => finding.version === entry.version));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function advisoryVerdict(advisory, manifest, verified) {
|
|
220
|
+
const entry = manifest.packages.find((item) => matchesAdvisory(item, advisory));
|
|
221
|
+
const fixed = advisoryFixed(entry, advisory, verified);
|
|
222
|
+
const id = advisory.github_advisory_id ?? String(advisory.id);
|
|
223
|
+
const state = fixed ? 'verified local backport; upstream audit remains affected' : 'unremediated';
|
|
224
|
+
return { text: `${advisory.severity}: ${id} (${advisory.module_name}) — ${state}`, blocked: !fixed && ['high', 'critical'].includes(advisory.severity), id };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function verifyAuditReport(report, { projectRoot, manifest, securityRoot = assetRoot, regressions = {} }) {
|
|
228
|
+
validateReport(report);
|
|
229
|
+
validateManifest(manifest);
|
|
230
|
+
const verified = new Set();
|
|
231
|
+
for (const entry of manifest.packages) {
|
|
232
|
+
if (await verifyEntry(entry, { projectRoot, securityRoot, regressions })) verified.add(entry.name);
|
|
233
|
+
}
|
|
234
|
+
const results = Object.values(report.advisories).map((advisory) => advisoryVerdict(advisory, manifest, verified));
|
|
235
|
+
const verdicts = results.map((result) => result.text);
|
|
236
|
+
const blocked = results.filter((result) => result.blocked).map((result) => result.id);
|
|
237
|
+
ensure(blocked.length === 0, `Unremediated high/critical advisories: ${blocked.join(', ')}\n${verdicts.join('\n')}`);
|
|
238
|
+
return { verdicts, verified: [...verified] };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function parseAuditResult(stdout, stderr, code, signal) {
|
|
242
|
+
ensure(!signal && [0, 1].includes(code), `pnpm audit failed (${signal ?? code}): ${stderr.slice(0, 2000)}`);
|
|
243
|
+
let report;
|
|
244
|
+
try { report = JSON.parse(stdout); }
|
|
245
|
+
catch { throw new Error(`pnpm audit returned malformed JSON: ${stderr.slice(0, 2000)}`); }
|
|
246
|
+
validateReport(report);
|
|
247
|
+
const clean = Object.values(report.metadata.vulnerabilities).every((count) => count === 0);
|
|
248
|
+
ensure((code === 0) === clean, 'pnpm audit exit status disagrees with vulnerability counts');
|
|
249
|
+
return { report, raw: stdout };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function collectAuditProcess(child) {
|
|
253
|
+
return new Promise((resolve, reject) => {
|
|
254
|
+
let stdout = '', stderr = '';
|
|
255
|
+
const fail = (error) => { clearTimeout(timer); child.kill(); reject(error); };
|
|
256
|
+
const timer = setTimeout(() => fail(new Error('pnpm audit timed out')), 120_000);
|
|
257
|
+
child.stdout.on('data', (data) => {
|
|
258
|
+
stdout += data;
|
|
259
|
+
if (stdout.length > 16_000_000) fail(new Error('pnpm audit output exceeds limit'));
|
|
260
|
+
});
|
|
261
|
+
child.stderr.on('data', (data) => { stderr = (stderr + data).slice(-16_000); });
|
|
262
|
+
child.on('error', fail);
|
|
263
|
+
child.on('close', (code, signal) => {
|
|
264
|
+
clearTimeout(timer);
|
|
265
|
+
try { resolve(parseAuditResult(stdout, stderr, code, signal)); }
|
|
266
|
+
catch (error) { reject(error); }
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function runAudit(projectRoot, all = true) {
|
|
272
|
+
const pnpm = process.env.npm_execpath;
|
|
273
|
+
ensure(nonemptyString(pnpm), 'Run this check through pnpm');
|
|
274
|
+
ensure(/pnpm/i.test(path.basename(pnpm)), 'Run this check through pnpm');
|
|
275
|
+
const args = ['audit', '--json', '--audit-level', 'low', ...(all ? [] : ['--prod'])];
|
|
276
|
+
const javascript = /\.[cm]?js$/i.test(pnpm);
|
|
277
|
+
const child = spawn(javascript ? process.execPath : pnpm, javascript ? [pnpm, ...args] : args, { cwd: projectRoot, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
278
|
+
return collectAuditProcess(child);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function main() {
|
|
282
|
+
const projectRoot = process.cwd();
|
|
283
|
+
const { report, raw } = await runAudit(projectRoot);
|
|
284
|
+
const reportPath = path.join(projectRoot, '.anhedral-audit-report.json');
|
|
285
|
+
await writeFile(reportPath, raw);
|
|
286
|
+
console.log(`Unmodified pnpm audit report (all dependencies): ${reportPath}`);
|
|
287
|
+
const manifest = JSON.parse(await readFile(path.join(assetRoot, 'manifest.json'), 'utf8'));
|
|
288
|
+
const result = await verifyAuditReport(report, { projectRoot, manifest });
|
|
289
|
+
result.verdicts.forEach((verdict) => console.log(verdict));
|
|
290
|
+
console.log('Dependency gate passed: no unremediated high/critical findings.');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
294
|
+
main().catch((error) => { console.error(error.message); process.exitCode = 1; });
|
|
295
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
// Golden results were captured from the original registry package braces 3.0.3.
|
|
6
|
+
const fixtures = [
|
|
7
|
+
{"input":"{a,b}","options":{},"expected":{"create":"(a|b)","compile":"(a|b)","expand":["a","b"],"stringify":"{a,b}"},"main":["(a|b)"]},
|
|
8
|
+
{"input":"a/{b,c}/d","options":{},"expected":{"create":"a/(b|c)/d","compile":"a/(b|c)/d","expand":["a/b/d","a/c/d"],"stringify":"a/{b,c}/d"},"main":["a/(b|c)/d"]},
|
|
9
|
+
{"input":"{01..05}","options":{},"expected":{"create":"(0[1-5])","compile":"(0[1-5])","expand":["01","02","03","04","05"],"stringify":"{01..05}"},"main":["(0[1-5])"]},
|
|
10
|
+
{"input":"{-3..3}","options":{},"expected":{"create":"(-[1-3]|[0-3])","compile":"(-[1-3]|[0-3])","expand":["-3","-2","-1","0","1","2","3"],"stringify":"{-3..3}"},"main":["(-[1-3]|[0-3])"]},
|
|
11
|
+
{"input":"{10..2..2}","options":{},"expected":{"create":"(2|4|6|8|10)","compile":"(2|4|6|8|10)","expand":["10","8","6","4","2"],"stringify":"{10..2..2}"},"main":["(2|4|6|8|10)"]},
|
|
12
|
+
{"input":"{a..z..3}","options":{},"expected":{"create":"(a|d|g|j|m|p|s|v|y)","compile":"(a|d|g|j|m|p|s|v|y)","expand":["a","d","g","j","m","p","s","v","y"],"stringify":"{a..z..3}"},"main":["(a|d|g|j|m|p|s|v|y)"]},
|
|
13
|
+
{"input":"{a,{b,c}}","options":{},"expected":{"create":"(a|(b|c))","compile":"(a|(b|c))","expand":["a","b","c"],"stringify":"{a,{b,c}}"},"main":["(a|(b|c))"]},
|
|
14
|
+
{"input":"{,a,,b,}","options":{},"expected":{"create":"(|a|b|)","compile":"(|a|b|)","expand":["","a","","b",""],"stringify":"{,a,,b,}"},"main":["(|a|b|)"]},
|
|
15
|
+
{"input":"\\{literal\\}","options":{"keepEscaping":true},"expected":{"create":"\\{literal\\}","compile":"\\{literal\\}","expand":["\\{literal\\}"],"stringify":"\\{literal\\}"},"main":["\\{literal\\}"]},
|
|
16
|
+
{"input":"file[{}].js","options":{},"expected":{"create":"file[{}].js","compile":"file[{}].js","expand":["file[{}].js"],"stringify":"file[{}].js"},"main":["file[{}].js"]},
|
|
17
|
+
{"input":"(x){a,b}","options":{},"expected":{"create":"(x)(a|b)","compile":"(x)(a|b)","expand":["(x)a","(x)b"],"stringify":"(x){a,b}"},"main":["(x)(a|b)"]},
|
|
18
|
+
{"input":"${name}","options":{},"expected":{"create":"${name}","compile":"${name}","expand":["${name}"],"stringify":"${name}"},"main":["${name}"]},
|
|
19
|
+
{"input":"{unclosed","options":{"escapeInvalid":true},"expected":{"create":"\\{unclosed","compile":"\\{unclosed","expand":["{unclosed"],"stringify":"{unclosed"},"main":["\\{unclosed"]},
|
|
20
|
+
{"input":"\"{a,b}\"","options":{"keepQuotes":true},"expected":{"create":"\"{a,b}\"","compile":"\"{a,b}\"","expand":["\"{a,b}\""],"stringify":"\"{a,b}\""},"main":["\"{a,b}\""]},
|
|
21
|
+
{"input":"{a,a,,b}","options":{"nodupes":true,"noempty":true},"expected":{"create":"(a|a|b)","compile":"(a|a|b)","expand":["a","b"],"stringify":"{a,a,,b}"},"main":["(a|a|b)"]},
|
|
22
|
+
{"input":"user-{200..300}","options":{},"expected":{"create":"user-(20[0-9]|2[1-9][0-9]|300)","compile":"user-(20[0-9]|2[1-9][0-9]|300)","expand":["user-200","user-201","user-202","user-203","user-204","user-205","user-206","user-207","user-208","user-209","user-210","user-211","user-212","user-213","user-214","user-215","user-216","user-217","user-218","user-219","user-220","user-221","user-222","user-223","user-224","user-225","user-226","user-227","user-228","user-229","user-230","user-231","user-232","user-233","user-234","user-235","user-236","user-237","user-238","user-239","user-240","user-241","user-242","user-243","user-244","user-245","user-246","user-247","user-248","user-249","user-250","user-251","user-252","user-253","user-254","user-255","user-256","user-257","user-258","user-259","user-260","user-261","user-262","user-263","user-264","user-265","user-266","user-267","user-268","user-269","user-270","user-271","user-272","user-273","user-274","user-275","user-276","user-277","user-278","user-279","user-280","user-281","user-282","user-283","user-284","user-285","user-286","user-287","user-288","user-289","user-290","user-291","user-292","user-293","user-294","user-295","user-296","user-297","user-298","user-299","user-300"],"stringify":"user-{200..300}"},"main":["user-(20[0-9]|2[1-9][0-9]|300)"]},
|
|
23
|
+
{"input":"","options":{},"expected":{"create":[""],"compile":"","expand":[],"stringify":""},"main":[""]},
|
|
24
|
+
{"input":"x","options":{},"expected":{"create":["x"],"compile":"x","expand":["x"],"stringify":"x"},"main":["x"]},
|
|
25
|
+
{"input":"{..3}","options":{},"expected":{"create":"{..3}","compile":"{..3}","expand":["{..3}"],"stringify":"{..3}"},"main":["{..3}"]},
|
|
26
|
+
{"input":"{1..3}","options":{"step":2},"expected":{"create":"(1|3)","compile":"(1|3)","expand":["1","3"],"stringify":"{1..3}"},"main":["(1|3)"]}
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const normalize = value => JSON.parse(JSON.stringify(value, (key, item) => ['parent', 'prev'].includes(key) ? undefined : item));
|
|
30
|
+
|
|
31
|
+
const bounded = fn => assert.throws(fn, error => error instanceof RangeError && /safe resource limits/.test(error.message));
|
|
32
|
+
|
|
33
|
+
function verifyCompatibility(braces) {
|
|
34
|
+
for (const { input, options, expected, main } of fixtures) {
|
|
35
|
+
assert.deepEqual(braces(input, options), main, `braces ${input}`);
|
|
36
|
+
const parsed = braces.parse(input, options);
|
|
37
|
+
assert.equal(parsed.type, 'root');
|
|
38
|
+
assert.equal(parsed.input, input);
|
|
39
|
+
assert.equal(braces.stringify(parsed, options), expected.stringify);
|
|
40
|
+
for (const method of ['create', 'compile', 'expand', 'stringify']) {
|
|
41
|
+
assert.deepEqual(normalize(braces[method](input, options)), expected[method], `${method} ${input}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
assert.deepEqual(braces(['{a,b}', '{1..3}'], { expand: true }), ['a', 'b', '1', '2', '3']);
|
|
45
|
+
assert.deepEqual(braces(['{a,a}', '{a,b}'], { expand: true, nodupes: true }), ['a', 'b']);
|
|
46
|
+
assert.throws(() => braces.expand('{1..1001}'), /range limit/);
|
|
47
|
+
assert.deepEqual(braces.expand('{1..3}', { rangeLimit: false }), ['1', '2', '3']);
|
|
48
|
+
assert.match(braces.compile('{1..1000000000}'), /./, 'Optimized numeric regex compilation remains available');
|
|
49
|
+
assert.equal(braces.expand('{1..100000}', { rangeLimit: false }).length, 100000);
|
|
50
|
+
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function verifyNestedStrings(braces) {
|
|
54
|
+
for (const method of ['create', 'compile', 'expand', 'parse', 'stringify']) {
|
|
55
|
+
bounded(() => braces[method]('{'.repeat(4000) + 'a,b' + '}'.repeat(4000)));
|
|
56
|
+
bounded(() => braces[method]('('.repeat(4000) + 'x' + ')'.repeat(4000)));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function verifyAstBounds(braces) {
|
|
61
|
+
for (const method of ['compile', 'expand', 'stringify']) {
|
|
62
|
+
let ast = { type: 'text', value: 'x' };
|
|
63
|
+
for (let i = 0; i < 101; i++) ast = { type: 'root', nodes: [ast] };
|
|
64
|
+
bounded(() => braces[method](ast));
|
|
65
|
+
const cycle = { type: 'root', nodes: [] }; cycle.nodes.push(cycle);
|
|
66
|
+
bounded(() => braces[method](cycle));
|
|
67
|
+
const parentCycle = { type: 'root', nodes: [] }; parentCycle.parent = parentCycle;
|
|
68
|
+
bounded(() => braces[method](parentCycle));
|
|
69
|
+
const foreignParents = Array.from({ length: 201 }, () => {
|
|
70
|
+
let parent;
|
|
71
|
+
for (let i = 0; i < 100; i++) parent = { parent };
|
|
72
|
+
return { type: 'text', value: 'x', parent };
|
|
73
|
+
});
|
|
74
|
+
bounded(() => braces[method]({ type: 'root', nodes: foreignParents }));
|
|
75
|
+
const leaf = { type: 'text', value: 'x' };
|
|
76
|
+
const dag = { type: 'root', nodes: [leaf, leaf] };
|
|
77
|
+
assert.deepEqual(braces[method](dag), method === 'expand' ? ['xx'] : 'xx', `${method} accepts an acyclic shared node`);
|
|
78
|
+
bounded(() => braces[method]({ type: 'root', nodes: Array.from({ length: 20001 }, () => ({ type: 'text', value: 'x' })) }));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function verifyExpansionBounds(braces) {
|
|
83
|
+
for (const input of ['{1..100001}', '{100001..1}', '{-100001..-1}', '{a,b}'.repeat(17)]) {
|
|
84
|
+
bounded(() => braces.expand(input, { rangeLimit: false }));
|
|
85
|
+
}
|
|
86
|
+
bounded(() => braces.compile('{1..300000..2}'));
|
|
87
|
+
bounded(() => braces.expand(`{${'0'.repeat(200)}1..${'0'.repeat(200)}100000}`, { rangeLimit: false }));
|
|
88
|
+
bounded(() => braces.expand('{100000000000000000000..100000000000000000001}', { rangeLimit: false }));
|
|
89
|
+
bounded(() => braces(['{a,b}'.repeat(16), '{a,b}'.repeat(16)], { expand: true }));
|
|
90
|
+
bounded(() => braces(Array(100001).fill('a')));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function verifyBraces(packageRoot) {
|
|
94
|
+
const require = createRequire(import.meta.url);
|
|
95
|
+
const braces = require(path.join(packageRoot, 'index.js'));
|
|
96
|
+
assert.equal(require(path.join(packageRoot, 'package.json')).version, '3.0.3');
|
|
97
|
+
verifyCompatibility(braces);
|
|
98
|
+
verifyNestedStrings(braces);
|
|
99
|
+
verifyAstBounds(braces);
|
|
100
|
+
verifyExpansionBounds(braces);
|
|
101
|
+
return { normalPatterns: fixtures.length, depthLimit: 100, nodeLimit: 20000, expansionLimit: 100000, characterLimit: 10485760 };
|
|
102
|
+
}
|