create-next-pro-cli 0.1.30 → 0.1.32
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 +16 -6
- package/create-next-pro-completion.sh +4 -3
- package/create-next-pro-completion.zsh +3 -3
- package/dist/bin.bun.js +514 -118
- package/dist/bin.node.js +579 -135
- package/dist/bin.node.js.map +1 -1
- package/package.json +6 -5
- package/templates/Page/page-ui.tsx +2 -2
- package/templates/Page/page-ui.user.tsx +17 -0
- package/templates/Projects/default/.agents/skills/create-next-pro-addapi/SKILL.md +61 -0
- package/templates/Projects/default/.agents/skills/create-next-pro-addcomponent/SKILL.md +67 -0
- package/templates/Projects/default/.agents/skills/create-next-pro-addlanguage/SKILL.md +68 -0
- package/templates/Projects/default/.agents/skills/create-next-pro-addlib/SKILL.md +65 -0
- package/templates/Projects/default/.agents/skills/create-next-pro-addpage/SKILL.md +89 -0
- package/templates/Projects/default/.agents/skills/create-next-pro-addtext/SKILL.md +63 -0
- package/templates/Projects/default/.agents/skills/create-next-pro-create-project/SKILL.md +76 -0
- package/templates/Projects/default/.agents/skills/create-next-pro-rmpage/SKILL.md +67 -0
- package/templates/Projects/default/.github/workflows/quality.yml +2 -1
- package/templates/Projects/default/AGENTS.md +91 -0
- package/templates/Projects/default/README.md +29 -9
- package/templates/Projects/default/bun.lock +114 -106
- package/templates/Projects/default/package.json +7 -5
- package/templates/Projects/default/pnpm-workspace.yaml +2 -1
- package/templates/Projects/default/scripts/audit-policy.ts +376 -0
- package/templates/Projects/default/scripts/audit.ts +72 -3
- package/templates/Projects/default/scripts/package-manager.ts +37 -0
- package/templates/Projects/default/tests/consumer/validate-template.ts +2 -0
- package/templates/Projects/default/tests/unit/audit-policy.test.ts +152 -0
- package/templates/Projects/default/tests/unit/template-baseline.test.ts +9 -0
|
@@ -28,11 +28,12 @@
|
|
|
28
28
|
"class-variance-authority": "0.7.1",
|
|
29
29
|
"clsx": "2.1.1",
|
|
30
30
|
"lucide-react": "1.24.0",
|
|
31
|
-
"next": "16.2.
|
|
32
|
-
"next-auth": "5.0.0-beta.
|
|
31
|
+
"next": "16.2.11",
|
|
32
|
+
"next-auth": "5.0.0-beta.32",
|
|
33
33
|
"next-intl": "4.13.2",
|
|
34
34
|
"react": "19.2.7",
|
|
35
35
|
"react-dom": "19.2.7",
|
|
36
|
+
"sharp": "0.35.3",
|
|
36
37
|
"simple-icons": "16.26.0",
|
|
37
38
|
"tailwind-merge": "3.6.0",
|
|
38
39
|
"tailwindcss-animate": "1.0.7"
|
|
@@ -43,15 +44,16 @@
|
|
|
43
44
|
"@types/node": "24.13.3",
|
|
44
45
|
"@types/react": "19.2.17",
|
|
45
46
|
"@types/react-dom": "19.2.3",
|
|
46
|
-
"eslint": "9.39.
|
|
47
|
-
"eslint-config-next": "16.2.
|
|
47
|
+
"eslint": "9.39.5",
|
|
48
|
+
"eslint-config-next": "16.2.11",
|
|
48
49
|
"prettier": "3.9.5",
|
|
49
50
|
"tailwindcss": "4.3.3",
|
|
50
51
|
"typescript": "6.0.3",
|
|
51
52
|
"vitest": "4.1.10"
|
|
52
53
|
},
|
|
53
54
|
"overrides": {
|
|
54
|
-
"postcss": "8.5.
|
|
55
|
+
"postcss": "8.5.23",
|
|
56
|
+
"sharp": "0.35.3"
|
|
55
57
|
},
|
|
56
58
|
"allowScripts": {
|
|
57
59
|
"@parcel/watcher": true,
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import type { PackageManager } from "./package-manager.ts";
|
|
4
|
+
|
|
5
|
+
export interface AuditFinding {
|
|
6
|
+
advisoryId: string;
|
|
7
|
+
packageName: string;
|
|
8
|
+
severity: string;
|
|
9
|
+
paths: string[];
|
|
10
|
+
devOnly: boolean | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface NormalizedAuditReport {
|
|
14
|
+
manager: PackageManager;
|
|
15
|
+
findings: AuditFinding[];
|
|
16
|
+
graphPackages: string[];
|
|
17
|
+
graphPaths: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AuditManifest {
|
|
21
|
+
name?: string;
|
|
22
|
+
dependencies?: Record<string, string>;
|
|
23
|
+
devDependencies?: Record<string, string>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface AuditPolicyContext {
|
|
27
|
+
now: Date;
|
|
28
|
+
manifest: AuditManifest;
|
|
29
|
+
bunWhy?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface AuditPolicyDecision {
|
|
33
|
+
accepted: boolean;
|
|
34
|
+
allowedAdvisories: string[];
|
|
35
|
+
errors: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface TemporaryAuditException {
|
|
39
|
+
advisoryId: string;
|
|
40
|
+
packageName: string;
|
|
41
|
+
justification: string;
|
|
42
|
+
expiresOn: string;
|
|
43
|
+
dependencyRoots: string[];
|
|
44
|
+
npm: {
|
|
45
|
+
packageCount: number;
|
|
46
|
+
packageFingerprint: string;
|
|
47
|
+
pathCount: number;
|
|
48
|
+
pathFingerprint: string;
|
|
49
|
+
};
|
|
50
|
+
pnpm: {
|
|
51
|
+
pathCount: number;
|
|
52
|
+
pathFingerprint: string;
|
|
53
|
+
};
|
|
54
|
+
bun: {
|
|
55
|
+
graphFingerprint: string;
|
|
56
|
+
};
|
|
57
|
+
rootBun: {
|
|
58
|
+
projectName: string;
|
|
59
|
+
dependencyRoots: string[];
|
|
60
|
+
graphFingerprint: string;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const TEMPORARY_AUDIT_EXCEPTION: TemporaryAuditException = {
|
|
65
|
+
advisoryId: "GHSA-mh99-v99m-4gvg",
|
|
66
|
+
packageName: "brace-expansion",
|
|
67
|
+
justification:
|
|
68
|
+
"The vulnerable version is reachable only from ESLint development tooling, and no compatible upstream ESLint plugin release is available.",
|
|
69
|
+
expiresOn: "2026-08-08",
|
|
70
|
+
dependencyRoots: ["eslint", "eslint-config-next"],
|
|
71
|
+
npm: {
|
|
72
|
+
packageCount: 9,
|
|
73
|
+
packageFingerprint:
|
|
74
|
+
"5c2f95af65c41494d74ca588887b735d7f53598759c1f4a550a534118ef9e21c",
|
|
75
|
+
pathCount: 9,
|
|
76
|
+
pathFingerprint:
|
|
77
|
+
"266ffc5bacefb7aba510ab245573622ec8a47d32ce6408bfd4c3560f698fd589",
|
|
78
|
+
},
|
|
79
|
+
pnpm: {
|
|
80
|
+
pathCount: 82,
|
|
81
|
+
pathFingerprint:
|
|
82
|
+
"135eeca335243a379f1c232df59dd641cd9d1684de814d4eac9105ded322e30f",
|
|
83
|
+
},
|
|
84
|
+
bun: {
|
|
85
|
+
graphFingerprint:
|
|
86
|
+
"bb8a9e09875f4a36186f4b43b8035cd6baa47b35a2614cd1e110b4129692cc62",
|
|
87
|
+
},
|
|
88
|
+
rootBun: {
|
|
89
|
+
projectName: "create-next-pro-cli",
|
|
90
|
+
dependencyRoots: ["eslint", "typescript-eslint", "tsup"],
|
|
91
|
+
graphFingerprint:
|
|
92
|
+
"9526795395c1d3983bb9d8fbcfce098103420ebe6f48169ab3fb4acc8f3aa769",
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
function asRecord(value: unknown, label: string): Record<string, unknown> {
|
|
97
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
98
|
+
throw new Error(`Invalid ${label} in audit JSON.`);
|
|
99
|
+
}
|
|
100
|
+
return value as Record<string, unknown>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function asArray(value: unknown, label: string): unknown[] {
|
|
104
|
+
if (!Array.isArray(value)) throw new Error(`Invalid ${label} in audit JSON.`);
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function asString(value: unknown, label: string): string {
|
|
109
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
110
|
+
throw new Error(`Invalid ${label} in audit JSON.`);
|
|
111
|
+
}
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function strings(value: unknown, label: string): string[] {
|
|
116
|
+
return asArray(value, label).map((entry) => asString(entry, label));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function advisoryIdFromUrl(value: unknown): string {
|
|
120
|
+
const url = asString(value, "advisory URL");
|
|
121
|
+
const match = /\/(GHSA-[a-z0-9-]+)$/i.exec(url);
|
|
122
|
+
if (!match) throw new Error(`Unsupported advisory URL: ${url}.`);
|
|
123
|
+
return match[1];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function parseJsonPayload(output: string): Record<string, unknown> {
|
|
127
|
+
const start = output.indexOf("{");
|
|
128
|
+
const end = output.lastIndexOf("}");
|
|
129
|
+
if (start < 0 || end < start)
|
|
130
|
+
throw new Error("Audit output contains no JSON object.");
|
|
131
|
+
try {
|
|
132
|
+
return asRecord(JSON.parse(output.slice(start, end + 1)), "audit report");
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (error instanceof SyntaxError)
|
|
135
|
+
throw new Error("Audit output contains invalid JSON.");
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function deduplicateFindings(findings: AuditFinding[]): AuditFinding[] {
|
|
141
|
+
const unique = new Map<string, AuditFinding>();
|
|
142
|
+
for (const finding of findings) {
|
|
143
|
+
const key = `${finding.advisoryId}:${finding.packageName}`;
|
|
144
|
+
const previous = unique.get(key);
|
|
145
|
+
if (!previous) {
|
|
146
|
+
unique.set(key, {
|
|
147
|
+
...finding,
|
|
148
|
+
paths: [...new Set(finding.paths)].sort(),
|
|
149
|
+
});
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
previous.paths = [...new Set([...previous.paths, ...finding.paths])].sort();
|
|
153
|
+
previous.devOnly = previous.devOnly === true && finding.devOnly === true;
|
|
154
|
+
}
|
|
155
|
+
return [...unique.values()].sort((left, right) =>
|
|
156
|
+
left.advisoryId.localeCompare(right.advisoryId),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function parseBunReport(root: Record<string, unknown>): NormalizedAuditReport {
|
|
161
|
+
const findings: AuditFinding[] = [];
|
|
162
|
+
for (const [packageName, advisories] of Object.entries(root)) {
|
|
163
|
+
for (const entry of asArray(
|
|
164
|
+
advisories,
|
|
165
|
+
`Bun advisories for ${packageName}`,
|
|
166
|
+
)) {
|
|
167
|
+
const advisory = asRecord(entry, `Bun advisory for ${packageName}`);
|
|
168
|
+
findings.push({
|
|
169
|
+
advisoryId: advisoryIdFromUrl(advisory.url),
|
|
170
|
+
packageName,
|
|
171
|
+
severity: asString(advisory.severity, "Bun advisory severity"),
|
|
172
|
+
paths: [],
|
|
173
|
+
devOnly: null,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
manager: "bun",
|
|
179
|
+
findings: deduplicateFindings(findings),
|
|
180
|
+
graphPackages: Object.keys(root).sort(),
|
|
181
|
+
graphPaths: [],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function parseNpmReport(root: Record<string, unknown>): NormalizedAuditReport {
|
|
186
|
+
const vulnerabilities = asRecord(root.vulnerabilities, "npm vulnerabilities");
|
|
187
|
+
const findings: AuditFinding[] = [];
|
|
188
|
+
const graphPaths: string[] = [];
|
|
189
|
+
for (const [packageName, value] of Object.entries(vulnerabilities)) {
|
|
190
|
+
const vulnerability = asRecord(
|
|
191
|
+
value,
|
|
192
|
+
`npm vulnerability for ${packageName}`,
|
|
193
|
+
);
|
|
194
|
+
const nodes = strings(vulnerability.nodes, `npm nodes for ${packageName}`);
|
|
195
|
+
graphPaths.push(...nodes);
|
|
196
|
+
for (const viaEntry of asArray(
|
|
197
|
+
vulnerability.via,
|
|
198
|
+
`npm via for ${packageName}`,
|
|
199
|
+
)) {
|
|
200
|
+
if (typeof viaEntry === "string") continue;
|
|
201
|
+
const advisory = asRecord(viaEntry, `npm advisory for ${packageName}`);
|
|
202
|
+
findings.push({
|
|
203
|
+
advisoryId: advisoryIdFromUrl(advisory.url),
|
|
204
|
+
packageName: asString(advisory.dependency, "npm advisory dependency"),
|
|
205
|
+
severity: asString(advisory.severity, "npm advisory severity"),
|
|
206
|
+
paths: nodes,
|
|
207
|
+
devOnly: null,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
manager: "npm",
|
|
213
|
+
findings: deduplicateFindings(findings),
|
|
214
|
+
graphPackages: Object.keys(vulnerabilities).sort(),
|
|
215
|
+
graphPaths: [...new Set(graphPaths)].sort(),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function parsePnpmReport(root: Record<string, unknown>): NormalizedAuditReport {
|
|
220
|
+
const advisories = asRecord(root.advisories, "pnpm advisories");
|
|
221
|
+
const findings: AuditFinding[] = [];
|
|
222
|
+
for (const value of Object.values(advisories)) {
|
|
223
|
+
const advisory = asRecord(value, "pnpm advisory");
|
|
224
|
+
const paths: string[] = [];
|
|
225
|
+
let devOnly = true;
|
|
226
|
+
for (const entry of asArray(advisory.findings, "pnpm findings")) {
|
|
227
|
+
const finding = asRecord(entry, "pnpm finding");
|
|
228
|
+
paths.push(...strings(finding.paths, "pnpm finding paths"));
|
|
229
|
+
devOnly = devOnly && finding.dev === true;
|
|
230
|
+
}
|
|
231
|
+
findings.push({
|
|
232
|
+
advisoryId: asString(advisory.github_advisory_id, "pnpm advisory ID"),
|
|
233
|
+
packageName: asString(advisory.module_name, "pnpm advisory package"),
|
|
234
|
+
severity: asString(advisory.severity, "pnpm advisory severity"),
|
|
235
|
+
paths,
|
|
236
|
+
devOnly,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
const normalized = deduplicateFindings(findings);
|
|
240
|
+
return {
|
|
241
|
+
manager: "pnpm",
|
|
242
|
+
findings: normalized,
|
|
243
|
+
graphPackages: normalized.map((finding) => finding.packageName).sort(),
|
|
244
|
+
graphPaths: normalized.flatMap((finding) => finding.paths).sort(),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function normalizeAuditReport(
|
|
249
|
+
manager: PackageManager,
|
|
250
|
+
output: string,
|
|
251
|
+
): NormalizedAuditReport {
|
|
252
|
+
const root = parseJsonPayload(output);
|
|
253
|
+
if (manager === "bun") return parseBunReport(root);
|
|
254
|
+
if (manager === "npm") return parseNpmReport(root);
|
|
255
|
+
return parsePnpmReport(root);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function fingerprint(values: string[]): string {
|
|
259
|
+
return createHash("sha256")
|
|
260
|
+
.update([...new Set(values)].sort().join("\n"))
|
|
261
|
+
.digest("hex");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function isDevelopmentOnly(
|
|
265
|
+
manifest: AuditManifest,
|
|
266
|
+
packageName: string,
|
|
267
|
+
): boolean {
|
|
268
|
+
return (
|
|
269
|
+
Object.hasOwn(manifest.devDependencies ?? {}, packageName) &&
|
|
270
|
+
!Object.hasOwn(manifest.dependencies ?? {}, packageName)
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function normalizedBunGraph(output: string): string {
|
|
275
|
+
return output
|
|
276
|
+
.replace(/\u001b\[[0-9;]*m/g, "")
|
|
277
|
+
.replace(/dev [^\s(]+/g, "dev <project>")
|
|
278
|
+
.replaceAll("\r\n", "\n")
|
|
279
|
+
.trim();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function validateManagerEvidence(
|
|
283
|
+
report: NormalizedAuditReport,
|
|
284
|
+
context: AuditPolicyContext,
|
|
285
|
+
): string[] {
|
|
286
|
+
const policy = TEMPORARY_AUDIT_EXCEPTION;
|
|
287
|
+
if (report.manager === "pnpm") {
|
|
288
|
+
const finding = report.findings[0];
|
|
289
|
+
if (!finding.devOnly) return ["The pnpm advisory is not development-only."];
|
|
290
|
+
if (
|
|
291
|
+
report.graphPaths.length !== policy.pnpm.pathCount ||
|
|
292
|
+
fingerprint(report.graphPaths) !== policy.pnpm.pathFingerprint
|
|
293
|
+
) {
|
|
294
|
+
return [
|
|
295
|
+
"The pnpm advisory dependency paths do not match the approved graph.",
|
|
296
|
+
];
|
|
297
|
+
}
|
|
298
|
+
return [];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (report.manager === "npm") {
|
|
302
|
+
if (
|
|
303
|
+
report.graphPackages.length !== policy.npm.packageCount ||
|
|
304
|
+
fingerprint(report.graphPackages) !== policy.npm.packageFingerprint ||
|
|
305
|
+
report.graphPaths.length !== policy.npm.pathCount ||
|
|
306
|
+
fingerprint(report.graphPaths) !== policy.npm.pathFingerprint
|
|
307
|
+
) {
|
|
308
|
+
return [
|
|
309
|
+
"The npm advisory dependency graph does not match the approved graph.",
|
|
310
|
+
];
|
|
311
|
+
}
|
|
312
|
+
return [];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const projectName = context.manifest.name;
|
|
316
|
+
if (!projectName || !context.bunWhy) {
|
|
317
|
+
return ["Bun audit policy requires the project name and dependency graph."];
|
|
318
|
+
}
|
|
319
|
+
const graphFingerprint = createHash("sha256")
|
|
320
|
+
.update(normalizedBunGraph(context.bunWhy))
|
|
321
|
+
.digest("hex");
|
|
322
|
+
const expectedFingerprint =
|
|
323
|
+
projectName === policy.rootBun.projectName
|
|
324
|
+
? policy.rootBun.graphFingerprint
|
|
325
|
+
: policy.bun.graphFingerprint;
|
|
326
|
+
if (graphFingerprint !== expectedFingerprint) {
|
|
327
|
+
return [
|
|
328
|
+
"The Bun advisory dependency graph does not match the approved graph.",
|
|
329
|
+
];
|
|
330
|
+
}
|
|
331
|
+
return [];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function evaluateAuditPolicy(
|
|
335
|
+
report: NormalizedAuditReport,
|
|
336
|
+
context: AuditPolicyContext,
|
|
337
|
+
): AuditPolicyDecision {
|
|
338
|
+
if (report.findings.length === 0) {
|
|
339
|
+
return { accepted: true, allowedAdvisories: [], errors: [] };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const policy = TEMPORARY_AUDIT_EXCEPTION;
|
|
343
|
+
const errors: string[] = [];
|
|
344
|
+
const currentDate = context.now.toISOString().slice(0, 10);
|
|
345
|
+
if (currentDate > policy.expiresOn) {
|
|
346
|
+
errors.push(`The audit exception expired on ${policy.expiresOn}.`);
|
|
347
|
+
}
|
|
348
|
+
if (
|
|
349
|
+
report.findings.length !== 1 ||
|
|
350
|
+
report.findings[0].advisoryId !== policy.advisoryId ||
|
|
351
|
+
report.findings[0].packageName !== policy.packageName
|
|
352
|
+
) {
|
|
353
|
+
errors.push(
|
|
354
|
+
"The audit contains an advisory that is not explicitly allowed.",
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
const dependencyRoots =
|
|
358
|
+
report.manager === "bun" &&
|
|
359
|
+
context.manifest.name === policy.rootBun.projectName
|
|
360
|
+
? policy.rootBun.dependencyRoots
|
|
361
|
+
: policy.dependencyRoots;
|
|
362
|
+
for (const dependencyRoot of dependencyRoots) {
|
|
363
|
+
if (!isDevelopmentOnly(context.manifest, dependencyRoot)) {
|
|
364
|
+
errors.push(`${dependencyRoot} is not confined to devDependencies.`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (errors.length === 0) {
|
|
368
|
+
errors.push(...validateManagerEvidence(report, context));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
accepted: errors.length === 0,
|
|
373
|
+
allowedAdvisories: errors.length === 0 ? [policy.advisoryId] : [],
|
|
374
|
+
errors,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
@@ -1,4 +1,73 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
import {
|
|
5
|
+
evaluateAuditPolicy,
|
|
6
|
+
normalizeAuditReport,
|
|
7
|
+
TEMPORARY_AUDIT_EXCEPTION,
|
|
8
|
+
} from "./audit-policy.ts";
|
|
9
|
+
import {
|
|
10
|
+
capturePackageManager,
|
|
11
|
+
resolvePackageManager,
|
|
12
|
+
} from "./package-manager.ts";
|
|
13
|
+
|
|
14
|
+
async function runAudit(): Promise<void> {
|
|
15
|
+
const manager = resolvePackageManager();
|
|
16
|
+
const root = process.cwd();
|
|
17
|
+
const manifest = JSON.parse(
|
|
18
|
+
await readFile(path.join(root, "package.json"), "utf8"),
|
|
19
|
+
) as {
|
|
20
|
+
name?: string;
|
|
21
|
+
dependencies?: Record<string, string>;
|
|
22
|
+
devDependencies?: Record<string, string>;
|
|
23
|
+
};
|
|
24
|
+
const audit = await capturePackageManager(manager, ["audit", "--json"], {
|
|
25
|
+
cwd: root,
|
|
26
|
+
});
|
|
27
|
+
if (audit.exitCode !== 0 && audit.exitCode !== 1) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`${manager} audit failed${audit.signal ? ` with signal ${audit.signal}` : ` with exit code ${audit.exitCode ?? "unknown"}`}: ${audit.stderr.trim() || "no diagnostic was provided"}`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const report = normalizeAuditReport(manager, audit.stdout);
|
|
34
|
+
let bunWhy: string | undefined;
|
|
35
|
+
if (manager === "bun" && report.findings.length > 0) {
|
|
36
|
+
const why = await capturePackageManager(
|
|
37
|
+
manager,
|
|
38
|
+
["why", TEMPORARY_AUDIT_EXCEPTION.packageName, "--depth", "20"],
|
|
39
|
+
{ cwd: root },
|
|
40
|
+
);
|
|
41
|
+
if (why.exitCode !== 0) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`bun why failed with exit code ${why.exitCode ?? "unknown"}: ${why.stderr.trim() || "no diagnostic was provided"}`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
bunWhy = why.stdout;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const decision = evaluateAuditPolicy(report, {
|
|
50
|
+
now: new Date(),
|
|
51
|
+
manifest,
|
|
52
|
+
bunWhy,
|
|
53
|
+
});
|
|
54
|
+
if (!decision.accepted) {
|
|
55
|
+
throw new Error(`Security audit rejected: ${decision.errors.join(" ")}`);
|
|
56
|
+
}
|
|
57
|
+
if (decision.allowedAdvisories.length === 0) {
|
|
58
|
+
process.stdout.write(`Security audit passed for ${manager}.\n`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
process.stdout.write(
|
|
62
|
+
`Security audit passed for ${manager} with temporary development-only exception ${TEMPORARY_AUDIT_EXCEPTION.advisoryId} (${TEMPORARY_AUDIT_EXCEPTION.packageName}); expires ${TEMPORARY_AUDIT_EXCEPTION.expiresOn}.\n`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
await runAudit();
|
|
68
|
+
} catch (error) {
|
|
69
|
+
process.stderr.write(
|
|
70
|
+
`${error instanceof Error ? error.message : String(error)}\n`,
|
|
71
|
+
);
|
|
72
|
+
process.exitCode = 1;
|
|
73
|
+
}
|
|
@@ -66,3 +66,40 @@ export function runPackageManager(
|
|
|
66
66
|
});
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
|
+
|
|
70
|
+
export interface PackageManagerCommandResult {
|
|
71
|
+
exitCode: number | null;
|
|
72
|
+
signal: NodeJS.Signals | null;
|
|
73
|
+
stdout: string;
|
|
74
|
+
stderr: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function capturePackageManager(
|
|
78
|
+
manager: PackageManager,
|
|
79
|
+
args: string[],
|
|
80
|
+
options: { cwd?: string; env?: NodeJS.ProcessEnv } = {},
|
|
81
|
+
): Promise<PackageManagerCommandResult> {
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const executable = packageManagerExecutable(manager);
|
|
84
|
+
const child = spawn(executable, args, {
|
|
85
|
+
cwd: options.cwd,
|
|
86
|
+
env: options.env ?? process.env,
|
|
87
|
+
shell: false,
|
|
88
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
89
|
+
});
|
|
90
|
+
let stdout = "";
|
|
91
|
+
let stderr = "";
|
|
92
|
+
child.stdout.setEncoding("utf8");
|
|
93
|
+
child.stderr.setEncoding("utf8");
|
|
94
|
+
child.stdout.on("data", (chunk: string) => {
|
|
95
|
+
stdout += chunk;
|
|
96
|
+
});
|
|
97
|
+
child.stderr.on("data", (chunk: string) => {
|
|
98
|
+
stderr += chunk;
|
|
99
|
+
});
|
|
100
|
+
child.once("error", reject);
|
|
101
|
+
child.once("close", (exitCode, signal) => {
|
|
102
|
+
resolve({ exitCode, signal, stdout, stderr });
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}
|
|
@@ -17,9 +17,11 @@ const gitignoreSource = await stat(path.join(root, ".gitignore.template")).then(
|
|
|
17
17
|
() => ".gitignore",
|
|
18
18
|
);
|
|
19
19
|
const entries = [
|
|
20
|
+
[".agents", ".agents"],
|
|
20
21
|
[".env.example", ".env.example"],
|
|
21
22
|
[gitignoreSource, ".gitignore"],
|
|
22
23
|
[".prettierignore", ".prettierignore"],
|
|
24
|
+
["AGENTS.md", "AGENTS.md"],
|
|
23
25
|
["README.md", "README.md"],
|
|
24
26
|
["bun.lock", "bun.lock"],
|
|
25
27
|
["eslint.config.mjs", "eslint.config.mjs"],
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
evaluateAuditPolicy,
|
|
5
|
+
normalizeAuditReport,
|
|
6
|
+
type AuditManifest,
|
|
7
|
+
type NormalizedAuditReport,
|
|
8
|
+
} from "../../scripts/audit-policy.ts";
|
|
9
|
+
|
|
10
|
+
const npmGraphPackages = [
|
|
11
|
+
"@eslint/config-array",
|
|
12
|
+
"@eslint/eslintrc",
|
|
13
|
+
"brace-expansion",
|
|
14
|
+
"eslint",
|
|
15
|
+
"eslint-config-next",
|
|
16
|
+
"eslint-plugin-import",
|
|
17
|
+
"eslint-plugin-jsx-a11y",
|
|
18
|
+
"eslint-plugin-react",
|
|
19
|
+
"minimatch",
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const npmGraphPaths = [
|
|
23
|
+
"node_modules/@eslint/config-array",
|
|
24
|
+
"node_modules/@eslint/eslintrc",
|
|
25
|
+
"node_modules/brace-expansion",
|
|
26
|
+
"node_modules/eslint",
|
|
27
|
+
"node_modules/eslint-config-next",
|
|
28
|
+
"node_modules/eslint-plugin-import",
|
|
29
|
+
"node_modules/eslint-plugin-jsx-a11y",
|
|
30
|
+
"node_modules/eslint-plugin-react",
|
|
31
|
+
"node_modules/minimatch",
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const manifest: AuditManifest = {
|
|
35
|
+
name: "fixture",
|
|
36
|
+
dependencies: { next: "16.2.11" },
|
|
37
|
+
devDependencies: {
|
|
38
|
+
eslint: "9.39.5",
|
|
39
|
+
"eslint-config-next": "16.2.11",
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function npmReport(): NormalizedAuditReport {
|
|
44
|
+
return {
|
|
45
|
+
manager: "npm",
|
|
46
|
+
findings: [
|
|
47
|
+
{
|
|
48
|
+
advisoryId: "GHSA-mh99-v99m-4gvg",
|
|
49
|
+
packageName: "brace-expansion",
|
|
50
|
+
severity: "high",
|
|
51
|
+
paths: ["node_modules/brace-expansion"],
|
|
52
|
+
devOnly: null,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
graphPackages: npmGraphPackages,
|
|
56
|
+
graphPaths: npmGraphPaths,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe("temporary audit policy", () => {
|
|
61
|
+
test.each([
|
|
62
|
+
["bun", "{}"],
|
|
63
|
+
["npm", '{"vulnerabilities":{}}'],
|
|
64
|
+
["pnpm", '{"advisories":{}}'],
|
|
65
|
+
] as const)("normalizes a clean %s audit report", (manager, output) => {
|
|
66
|
+
expect(normalizeAuditReport(manager, output)).toEqual({
|
|
67
|
+
manager,
|
|
68
|
+
findings: [],
|
|
69
|
+
graphPackages: [],
|
|
70
|
+
graphPaths: [],
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("accepts only the pinned development dependency graph", () => {
|
|
75
|
+
expect(
|
|
76
|
+
evaluateAuditPolicy(npmReport(), {
|
|
77
|
+
now: new Date("2026-08-08T23:59:59.000Z"),
|
|
78
|
+
manifest,
|
|
79
|
+
}),
|
|
80
|
+
).toEqual({
|
|
81
|
+
accepted: true,
|
|
82
|
+
allowedAdvisories: ["GHSA-mh99-v99m-4gvg"],
|
|
83
|
+
errors: [],
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("rejects the exception after its expiration date", () => {
|
|
88
|
+
const decision = evaluateAuditPolicy(npmReport(), {
|
|
89
|
+
now: new Date("2026-08-09T00:00:00.000Z"),
|
|
90
|
+
manifest,
|
|
91
|
+
});
|
|
92
|
+
expect(decision.accepted).toBe(false);
|
|
93
|
+
expect(decision.errors).toContain(
|
|
94
|
+
"The audit exception expired on 2026-08-08.",
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("rejects an unexpected dependency path", () => {
|
|
99
|
+
const report = npmReport();
|
|
100
|
+
report.graphPaths.push("node_modules/production/brace-expansion");
|
|
101
|
+
const decision = evaluateAuditPolicy(report, {
|
|
102
|
+
now: new Date("2026-07-25T00:00:00.000Z"),
|
|
103
|
+
manifest,
|
|
104
|
+
});
|
|
105
|
+
expect(decision.accepted).toBe(false);
|
|
106
|
+
expect(decision.errors).toContain(
|
|
107
|
+
"The npm advisory dependency graph does not match the approved graph.",
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("rejects a dependency root that enters production dependencies", () => {
|
|
112
|
+
const decision = evaluateAuditPolicy(npmReport(), {
|
|
113
|
+
now: new Date("2026-07-25T00:00:00.000Z"),
|
|
114
|
+
manifest: {
|
|
115
|
+
...manifest,
|
|
116
|
+
dependencies: { eslint: "9.39.5" },
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
expect(decision.accepted).toBe(false);
|
|
120
|
+
expect(decision.errors).toContain(
|
|
121
|
+
"eslint is not confined to devDependencies.",
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("rejects invalid audit JSON", () => {
|
|
126
|
+
expect(() => normalizeAuditReport("bun", "not-json")).toThrow(
|
|
127
|
+
"Audit output contains no JSON object.",
|
|
128
|
+
);
|
|
129
|
+
expect(() => normalizeAuditReport("npm", "{broken}")).toThrow(
|
|
130
|
+
"Audit output contains invalid JSON.",
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("rejects every new advisory", () => {
|
|
135
|
+
const report = npmReport();
|
|
136
|
+
report.findings.push({
|
|
137
|
+
advisoryId: "GHSA-new-advisory",
|
|
138
|
+
packageName: "unexpected-package",
|
|
139
|
+
severity: "critical",
|
|
140
|
+
paths: ["node_modules/unexpected-package"],
|
|
141
|
+
devOnly: true,
|
|
142
|
+
});
|
|
143
|
+
const decision = evaluateAuditPolicy(report, {
|
|
144
|
+
now: new Date("2026-07-25T00:00:00.000Z"),
|
|
145
|
+
manifest,
|
|
146
|
+
});
|
|
147
|
+
expect(decision.accepted).toBe(false);
|
|
148
|
+
expect(decision.errors).toContain(
|
|
149
|
+
"The audit contains an advisory that is not explicitly allowed.",
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
});
|